chore: Move graphl to a single place (#9303)
* chore: Move graphl to a single place * add new line
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { cleanGraphQLSchema } from "../clean-graphql"
|
||||
|
||||
describe("Clean Graphql Schema", function () {
|
||||
it("Should keep the schema intact if all entities are available", function () {
|
||||
const schemaStr = `
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
variants: [Variant]!
|
||||
}
|
||||
type Variant {
|
||||
id: ID!
|
||||
title: String!
|
||||
product_id: ID!
|
||||
product: Product!
|
||||
}
|
||||
`
|
||||
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
|
||||
|
||||
expect(schema.replace(/\s/g, "")).toEqual(schemaStr.replace(/\s/g, ""))
|
||||
expect(notFound).toEqual({})
|
||||
})
|
||||
|
||||
it("Should remove fields where the relation doesn't exist", function () {
|
||||
const schemaStr = `
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
variants: [Variant!]!
|
||||
profile: ShippingProfile!
|
||||
}
|
||||
`
|
||||
const expectedStr = `
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
}
|
||||
`
|
||||
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
|
||||
|
||||
expect(schema.replace(/\s/g, "")).toEqual(expectedStr.replace(/\s/g, ""))
|
||||
expect(notFound).toEqual({
|
||||
Product: { variants: "Variant", profile: "ShippingProfile" },
|
||||
})
|
||||
})
|
||||
|
||||
it("Should remove fields where the relation doesn't exist and flag extended entity where the main entity doesn't exist", function () {
|
||||
const schemaStr = `
|
||||
scalar JSON
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
variants: [Variant!]!
|
||||
profile: ShippingProfile!
|
||||
}
|
||||
|
||||
extend type Variant {
|
||||
metadata: JSON
|
||||
}
|
||||
`
|
||||
const expectedStr = `
|
||||
scalar JSON
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
}
|
||||
`
|
||||
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
|
||||
|
||||
expect(schema.replace(/\s/g, "")).toEqual(expectedStr.replace(/\s/g, ""))
|
||||
expect(notFound).toEqual({
|
||||
Product: { variants: "Variant", profile: "ShippingProfile" },
|
||||
Variant: { __extended: "" },
|
||||
})
|
||||
})
|
||||
|
||||
it("Should remove fields from extend where the relation doesn't exist", function () {
|
||||
const schemaStr = `
|
||||
scalar JSON
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
variants: [Variant!]!
|
||||
profile: ShippingProfile!
|
||||
}
|
||||
|
||||
extend type Product {
|
||||
variants: [Variant!]!
|
||||
profile: ShippingProfile!
|
||||
metadata: JSON
|
||||
}
|
||||
`
|
||||
const expectedStr = `
|
||||
scalar JSON
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
}
|
||||
|
||||
extend type Product {
|
||||
metadata: JSON
|
||||
}
|
||||
`
|
||||
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
|
||||
|
||||
expect(schema.replace(/\s/g, "")).toEqual(expectedStr.replace(/\s/g, ""))
|
||||
expect(notFound).toEqual({
|
||||
Product: { variants: "Variant", profile: "ShippingProfile" },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,358 @@
|
||||
import { GraphQLParser } from "../graphql-parser"
|
||||
|
||||
describe("RemoteJoiner.parseQuery", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("Simple query with fields", async () => {
|
||||
const graphqlQuery = `
|
||||
query {
|
||||
order {
|
||||
id
|
||||
number
|
||||
date
|
||||
}
|
||||
}
|
||||
`
|
||||
const parser = new GraphQLParser(graphqlQuery)
|
||||
const rjQuery = parser.parseQuery()
|
||||
|
||||
expect(rjQuery).toEqual({
|
||||
alias: "order",
|
||||
fields: ["id", "number", "date"],
|
||||
expands: [],
|
||||
})
|
||||
})
|
||||
|
||||
it("Simple query with fields and arguments", async () => {
|
||||
const graphqlQuery = `
|
||||
query {
|
||||
order(
|
||||
id: "ord_123",
|
||||
another_arg: 987,
|
||||
complexArg: {
|
||||
id: "123",
|
||||
name: "test",
|
||||
nestedArg: {
|
||||
nest_id: "abc",
|
||||
num: 123
|
||||
}
|
||||
}
|
||||
) {
|
||||
id
|
||||
number
|
||||
date
|
||||
}
|
||||
}
|
||||
`
|
||||
const parser = new GraphQLParser(graphqlQuery)
|
||||
const rjQuery = parser.parseQuery()
|
||||
|
||||
expect(rjQuery).toEqual({
|
||||
alias: "order",
|
||||
fields: ["id", "number", "date"],
|
||||
expands: [],
|
||||
args: [
|
||||
{
|
||||
name: "id",
|
||||
value: "ord_123",
|
||||
},
|
||||
{
|
||||
name: "another_arg",
|
||||
value: 987,
|
||||
},
|
||||
{
|
||||
name: "complexArg",
|
||||
value: {
|
||||
id: "123",
|
||||
name: "test",
|
||||
nestedArg: {
|
||||
nest_id: "abc",
|
||||
num: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("Simple query with mapping fields to services", async () => {
|
||||
const graphqlQuery = `
|
||||
query {
|
||||
order {
|
||||
id
|
||||
number
|
||||
date
|
||||
products {
|
||||
product_id
|
||||
variant_id
|
||||
order
|
||||
variant {
|
||||
name
|
||||
sku
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const parser = new GraphQLParser(graphqlQuery, {})
|
||||
const rjQuery = parser.parseQuery()
|
||||
|
||||
expect(rjQuery).toEqual({
|
||||
alias: "order",
|
||||
fields: ["id", "number", "date", "products"],
|
||||
expands: [
|
||||
{
|
||||
property: "products",
|
||||
fields: ["product_id", "variant_id", "order", "variant"],
|
||||
},
|
||||
{
|
||||
property: "products.variant",
|
||||
fields: ["name", "sku"],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("Nested query with fields", async () => {
|
||||
const graphqlQuery = `
|
||||
query {
|
||||
order {
|
||||
id
|
||||
number
|
||||
date
|
||||
products {
|
||||
product_id
|
||||
variant_id
|
||||
order
|
||||
variant {
|
||||
name
|
||||
sku
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const parser = new GraphQLParser(graphqlQuery)
|
||||
const rjQuery = parser.parseQuery()
|
||||
|
||||
expect(rjQuery).toEqual({
|
||||
alias: "order",
|
||||
fields: ["id", "number", "date", "products"],
|
||||
expands: [
|
||||
{
|
||||
property: "products",
|
||||
fields: ["product_id", "variant_id", "order", "variant"],
|
||||
},
|
||||
{
|
||||
property: "products.variant",
|
||||
fields: ["name", "sku"],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("Nested query with fields and arguments", async () => {
|
||||
const graphqlQuery = `
|
||||
query {
|
||||
order (order_id: "ord_123") {
|
||||
id
|
||||
number
|
||||
date
|
||||
products (limit: 10) {
|
||||
product_id
|
||||
variant_id
|
||||
order
|
||||
variant (complexArg: { id: "123", name: "test", nestedArg: { nest_id: "abc", num: 123 } }, region_id: "reg_123") {
|
||||
name
|
||||
sku
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const parser = new GraphQLParser(graphqlQuery)
|
||||
const rjQuery = parser.parseQuery()
|
||||
|
||||
expect(rjQuery).toEqual({
|
||||
alias: "order",
|
||||
fields: ["id", "number", "date", "products"],
|
||||
expands: [
|
||||
{
|
||||
property: "products",
|
||||
fields: ["product_id", "variant_id", "order", "variant"],
|
||||
args: [
|
||||
{
|
||||
name: "limit",
|
||||
value: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
property: "products.variant",
|
||||
fields: ["name", "sku"],
|
||||
args: [
|
||||
{
|
||||
name: "complexArg",
|
||||
value: {
|
||||
id: "123",
|
||||
name: "test",
|
||||
nestedArg: {
|
||||
nest_id: "abc",
|
||||
num: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "region_id",
|
||||
value: "reg_123",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
args: [
|
||||
{
|
||||
name: "order_id",
|
||||
value: "ord_123",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("Nested query with fields and arguments using variables", async () => {
|
||||
const graphqlQuery = `
|
||||
query($orderId: ID, $anotherArg: String, $randomVariable: nonValidatedType) {
|
||||
order (order_id: $orderId, anotherArg: $anotherArg) {
|
||||
id
|
||||
number
|
||||
date
|
||||
products (randomValue: $randomVariable) {
|
||||
product_id
|
||||
variant_id
|
||||
order
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const parser = new GraphQLParser(graphqlQuery, {
|
||||
orderId: 123,
|
||||
randomVariable: { complex: { num: 12343, str: "str_123" } },
|
||||
anotherArg: "any string",
|
||||
})
|
||||
const rjQuery = parser.parseQuery()
|
||||
|
||||
expect(rjQuery).toEqual({
|
||||
alias: "order",
|
||||
fields: ["id", "number", "date", "products"],
|
||||
expands: [
|
||||
{
|
||||
property: "products",
|
||||
fields: ["product_id", "variant_id", "order"],
|
||||
args: [
|
||||
{
|
||||
name: "randomValue",
|
||||
value: {
|
||||
complex: {
|
||||
num: 12343,
|
||||
str: "str_123",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
args: [
|
||||
{
|
||||
name: "order_id",
|
||||
value: 123,
|
||||
},
|
||||
{
|
||||
name: "anotherArg",
|
||||
value: "any string",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("Nested query with fields and directives", async () => {
|
||||
const graphqlQuery = `
|
||||
query {
|
||||
order(regularArgs: 123) {
|
||||
id
|
||||
number @include(if: "date > '2020-01-01'")
|
||||
date
|
||||
products {
|
||||
product_id
|
||||
variant_id
|
||||
variant @count {
|
||||
name @lowerCase
|
||||
sku @include(if: "name == 'test'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const parser = new GraphQLParser(graphqlQuery)
|
||||
const rjQuery = parser.parseQuery()
|
||||
|
||||
expect(rjQuery).toEqual({
|
||||
alias: "order",
|
||||
fields: ["id", "number", "date", "products"],
|
||||
expands: [
|
||||
{
|
||||
property: "products",
|
||||
fields: ["product_id", "variant_id", "variant"],
|
||||
directives: {
|
||||
variant: [
|
||||
{
|
||||
name: "count",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
property: "products.variant",
|
||||
fields: ["name", "sku"],
|
||||
directives: {
|
||||
name: [
|
||||
{
|
||||
name: "lowerCase",
|
||||
},
|
||||
],
|
||||
sku: [
|
||||
{
|
||||
name: "include",
|
||||
args: [
|
||||
{
|
||||
name: "if",
|
||||
value: "name == 'test'",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
args: [
|
||||
{
|
||||
name: "regularArgs",
|
||||
value: 123,
|
||||
},
|
||||
],
|
||||
directives: {
|
||||
number: [
|
||||
{
|
||||
name: "include",
|
||||
args: [
|
||||
{
|
||||
name: "if",
|
||||
value: "date > '2020-01-01'",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
import { mergeTypeDefs } from "@graphql-tools/merge"
|
||||
import { makeExecutableSchema } from "@graphql-tools/schema"
|
||||
import { gqlGetFieldsAndRelations } from "../../get-fields-and-relations"
|
||||
|
||||
const userModule = `
|
||||
type User {
|
||||
id: ID!
|
||||
name: String!
|
||||
blabla: WHATEVER
|
||||
}
|
||||
|
||||
type Post {
|
||||
author: User!
|
||||
}
|
||||
`
|
||||
|
||||
const postModule = `
|
||||
type Post {
|
||||
id: ID!
|
||||
title: String!
|
||||
date: String
|
||||
}
|
||||
|
||||
type User {
|
||||
posts: [Post!]!
|
||||
}
|
||||
|
||||
type WHATEVER {
|
||||
random_field: String
|
||||
post: Post
|
||||
}
|
||||
`
|
||||
|
||||
const mergedSchema = mergeTypeDefs([userModule, postModule])
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs: mergedSchema,
|
||||
})
|
||||
|
||||
const types = schema.getTypeMap()
|
||||
|
||||
describe("gqlGetFieldsAndRelations", function () {
|
||||
it("Should get all fields of a given entity", async function () {
|
||||
const fields = gqlGetFieldsAndRelations(types, "User")
|
||||
expect(fields).toEqual(expect.arrayContaining(["id", "name"]))
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and a relation", async function () {
|
||||
const fields = gqlGetFieldsAndRelations(types, "User", ["posts"])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and many relations", async function () {
|
||||
const fields = gqlGetFieldsAndRelations(types, "User", [
|
||||
"posts",
|
||||
"blabla",
|
||||
"blabla.post",
|
||||
])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
"blabla.random_field",
|
||||
"blabla.post.id",
|
||||
"blabla.post.title",
|
||||
"blabla.post.date",
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and many relations limited to the relations given", async function () {
|
||||
const fields = gqlGetFieldsAndRelations(types, "User", ["posts", "blabla"])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
"blabla.random_field",
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
import { mergeTypeDefs } from "@graphql-tools/merge"
|
||||
import { makeExecutableSchema } from "@graphql-tools/schema"
|
||||
import { graphqlSchemaToFields } from "../../graphql-to-fields"
|
||||
|
||||
const userModule = `
|
||||
type User {
|
||||
id: ID!
|
||||
name: String!
|
||||
blabla: WHATEVER
|
||||
}
|
||||
|
||||
type Post {
|
||||
author: User!
|
||||
}
|
||||
`
|
||||
|
||||
const postModule = `
|
||||
type Post {
|
||||
id: ID!
|
||||
title: String!
|
||||
date: String
|
||||
}
|
||||
|
||||
type User {
|
||||
posts: [Post!]!
|
||||
}
|
||||
|
||||
type WHATEVER {
|
||||
random_field: String
|
||||
post: Post
|
||||
}
|
||||
`
|
||||
|
||||
const mergedSchema = mergeTypeDefs([userModule, postModule])
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs: mergedSchema,
|
||||
})
|
||||
|
||||
const types = schema.getTypeMap()
|
||||
|
||||
describe("graphqlSchemaToFields", function () {
|
||||
it("Should get all fields of a given entity", async function () {
|
||||
const fields = graphqlSchemaToFields(types, "User")
|
||||
expect(fields).toEqual(expect.arrayContaining(["id", "name"]))
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and a relation", async function () {
|
||||
const fields = graphqlSchemaToFields(types, "User", ["posts"])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and many relations", async function () {
|
||||
const fields = graphqlSchemaToFields(types, "User", [
|
||||
"posts",
|
||||
"blabla",
|
||||
"blabla.post",
|
||||
])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
"blabla.random_field",
|
||||
"blabla.post.id",
|
||||
"blabla.post.title",
|
||||
"blabla.post.date",
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and many relations limited to the relations given", async function () {
|
||||
const fields = graphqlSchemaToFields(types, "User", ["posts", "blabla"])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
"blabla.random_field",
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and many relations limited to the relations given if they exists", async function () {
|
||||
const fields = graphqlSchemaToFields(types, "User", [
|
||||
"posts",
|
||||
"doNotExists",
|
||||
])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Kind, parse, print, visit } from "graphql"
|
||||
|
||||
export function cleanGraphQLSchema(schema: string): {
|
||||
schema: string
|
||||
notFound: Record<string, Record<string, string>>
|
||||
} {
|
||||
const extractTypeNameAndKind = (type) => {
|
||||
if (type.kind === Kind.NAMED_TYPE) {
|
||||
return [type.name.value, type.kind]
|
||||
}
|
||||
if (type.kind === Kind.NON_NULL_TYPE || type.kind === Kind.LIST_TYPE) {
|
||||
return extractTypeNameAndKind(type.type)
|
||||
}
|
||||
return [null, null]
|
||||
}
|
||||
|
||||
const ast = parse(schema)
|
||||
|
||||
const typeNames = new Set(["String", "Int", "Float", "Boolean", "ID"])
|
||||
const extendedTypes = new Set()
|
||||
|
||||
const kinds = [
|
||||
Kind.OBJECT_TYPE_DEFINITION,
|
||||
Kind.INTERFACE_TYPE_DEFINITION,
|
||||
Kind.ENUM_TYPE_DEFINITION,
|
||||
Kind.SCALAR_TYPE_DEFINITION,
|
||||
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
||||
Kind.UNION_TYPE_DEFINITION,
|
||||
]
|
||||
ast.definitions.forEach((def: any) => {
|
||||
if (kinds.includes(def.kind)) {
|
||||
typeNames.add(def.name.value)
|
||||
} else if (def.kind === Kind.OBJECT_TYPE_EXTENSION) {
|
||||
extendedTypes.add(def.name.value)
|
||||
}
|
||||
})
|
||||
|
||||
const nonExistingMap: Record<string, Record<string, string>> = {}
|
||||
const parentStack: string[] = []
|
||||
|
||||
/*
|
||||
Traverse the graph mapping all the entities + fields and removing the ones that don't exist.
|
||||
Extensions are not removed, but marked with a "__extended" key if the main entity doesn't exist. (example: Link modules injecting fields into another module)
|
||||
*/
|
||||
const cleanedAst = visit(ast, {
|
||||
ObjectTypeExtension: {
|
||||
enter(node) {
|
||||
const typeName = node.name.value
|
||||
|
||||
parentStack.push(typeName)
|
||||
if (!typeNames.has(typeName)) {
|
||||
nonExistingMap[typeName] ??= {}
|
||||
nonExistingMap[typeName]["__extended"] = ""
|
||||
return null
|
||||
}
|
||||
return
|
||||
},
|
||||
leave() {
|
||||
parentStack.pop()
|
||||
},
|
||||
},
|
||||
ObjectTypeDefinition: {
|
||||
enter(node) {
|
||||
parentStack.push(node.name.value)
|
||||
},
|
||||
leave() {
|
||||
parentStack.pop()
|
||||
},
|
||||
},
|
||||
FieldDefinition: {
|
||||
leave(node) {
|
||||
const [typeName, kind] = extractTypeNameAndKind(node.type)
|
||||
|
||||
if (!typeNames.has(typeName) && kind === Kind.NAMED_TYPE) {
|
||||
const currentParent = parentStack[parentStack.length - 1]
|
||||
|
||||
nonExistingMap[currentParent] ??= {}
|
||||
nonExistingMap[currentParent][node.name.value] = typeName
|
||||
return null
|
||||
}
|
||||
return
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Return the schema and the map of non existing entities and fields
|
||||
return {
|
||||
schema: print(cleanedAst),
|
||||
notFound: nonExistingMap,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { isListType, isNonNullType, isObjectType } from "graphql"
|
||||
|
||||
/**
|
||||
* Extracts only the relation fields from the GraphQL type map.
|
||||
* @param {Map<string, any>} typeMap - The GraphQL schema TypeMap.
|
||||
* @returns {Map<string, Map<string, string>>} A map where each key is an entity name, and the values are a map of relation fields and their corresponding entity type.
|
||||
*/
|
||||
export function extractRelationsFromGQL(
|
||||
typeMap: Map<string, any>
|
||||
): Map<string, Map<string, string>> {
|
||||
const relationMap = new Map()
|
||||
|
||||
// Extract the actual type
|
||||
const getBaseType = (type) => {
|
||||
if (isNonNullType(type) || isListType(type)) {
|
||||
return getBaseType(type.ofType)
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
for (const [typeName, graphqlType] of Object.entries(typeMap)) {
|
||||
if (!isObjectType(graphqlType)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const fields = graphqlType.getFields()
|
||||
const entityRelations = new Map()
|
||||
|
||||
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
||||
const fieldType = getBaseType((fieldConfig as any).type)
|
||||
|
||||
// only add relation fields
|
||||
if (isObjectType(fieldType)) {
|
||||
entityRelations.set(fieldName, fieldType.name)
|
||||
}
|
||||
}
|
||||
relationMap.set(typeName, entityRelations)
|
||||
}
|
||||
|
||||
return relationMap
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { GraphQLNamedType, GraphQLObjectType, isObjectType } from "graphql"
|
||||
|
||||
/**
|
||||
* Generate a list of fields and fields relations for a given type with the requested relations
|
||||
* @param schemaTypeMap
|
||||
* @param typeName
|
||||
* @param relations
|
||||
*/
|
||||
export function gqlGetFieldsAndRelations(
|
||||
schemaTypeMap: { [key: string]: GraphQLNamedType },
|
||||
typeName: string,
|
||||
relations: string[] = []
|
||||
) {
|
||||
const result: string[] = []
|
||||
|
||||
function traverseFields(typeName, prefix = "") {
|
||||
const type = schemaTypeMap[typeName]
|
||||
|
||||
if (!(type instanceof GraphQLObjectType)) {
|
||||
return
|
||||
}
|
||||
|
||||
const fields = type.getFields()
|
||||
|
||||
for (const fieldName in fields) {
|
||||
const field = fields[fieldName]
|
||||
let fieldType = field.type as any
|
||||
|
||||
while (fieldType.ofType) {
|
||||
fieldType = fieldType.ofType
|
||||
}
|
||||
|
||||
if (!isObjectType(fieldType)) {
|
||||
result.push(`${prefix}${fieldName}`)
|
||||
} else if (relations.includes(prefix + fieldName)) {
|
||||
traverseFields(fieldType.name, `${prefix}${fieldName}.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields(typeName)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { RemoteJoinerQuery } from "@medusajs/types"
|
||||
import {
|
||||
ArgumentNode,
|
||||
DirectiveNode,
|
||||
DocumentNode,
|
||||
FieldNode,
|
||||
Kind,
|
||||
OperationDefinitionNode,
|
||||
SelectionSetNode,
|
||||
ValueNode,
|
||||
parse,
|
||||
} from "graphql"
|
||||
|
||||
interface Argument {
|
||||
name: string
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
interface Directive {
|
||||
name: string
|
||||
args?: Argument[]
|
||||
}
|
||||
|
||||
interface Entity {
|
||||
property: string
|
||||
fields: string[]
|
||||
args?: Argument[]
|
||||
directives?: { [field: string]: Directive[] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote joiner graphql parser
|
||||
*/
|
||||
export class GraphQLParser {
|
||||
private ast: DocumentNode
|
||||
|
||||
constructor(input: string, private variables: Record<string, unknown> = {}) {
|
||||
this.ast = parse(input)
|
||||
this.variables = variables
|
||||
}
|
||||
|
||||
private parseValueNode(valueNode: ValueNode): unknown {
|
||||
const obj = {}
|
||||
|
||||
switch (valueNode.kind) {
|
||||
case Kind.VARIABLE:
|
||||
return this.variables ? this.variables[valueNode.name.value] : undefined
|
||||
case Kind.INT:
|
||||
return parseInt(valueNode.value, 10)
|
||||
case Kind.FLOAT:
|
||||
return parseFloat(valueNode.value)
|
||||
case Kind.BOOLEAN:
|
||||
return Boolean(valueNode.value)
|
||||
case Kind.STRING:
|
||||
case Kind.ENUM:
|
||||
return valueNode.value
|
||||
case Kind.NULL:
|
||||
return null
|
||||
case Kind.LIST:
|
||||
return valueNode.values.map((v) => this.parseValueNode(v))
|
||||
case Kind.OBJECT:
|
||||
for (const field of valueNode.fields) {
|
||||
obj[field.name.value] = this.parseValueNode(field.value)
|
||||
}
|
||||
return obj
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
private parseArguments(
|
||||
args: readonly ArgumentNode[]
|
||||
): Argument[] | undefined {
|
||||
if (!args.length) {
|
||||
return
|
||||
}
|
||||
|
||||
return args.map((arg) => {
|
||||
const value = this.parseValueNode(arg.value)
|
||||
|
||||
return {
|
||||
name: arg.name.value,
|
||||
value: value,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private parseDirectives(directives: readonly DirectiveNode[]): Directive[] {
|
||||
return directives.map((directive) => ({
|
||||
name: directive.name.value,
|
||||
args: this.parseArguments(directive.arguments || []),
|
||||
}))
|
||||
}
|
||||
|
||||
private createDirectivesMap(selectionSet: SelectionSetNode):
|
||||
| {
|
||||
[field: string]: Directive[]
|
||||
}
|
||||
| undefined {
|
||||
const directivesMap: { [field: string]: Directive[] } = {}
|
||||
let hasDirectives = false
|
||||
selectionSet.selections.forEach((field) => {
|
||||
const fieldName = (field as FieldNode).name.value
|
||||
const fieldDirectives = this.parseDirectives(
|
||||
(field as FieldNode).directives || []
|
||||
)
|
||||
if (fieldDirectives.length > 0) {
|
||||
hasDirectives = true
|
||||
directivesMap[fieldName] = fieldDirectives
|
||||
}
|
||||
})
|
||||
return hasDirectives ? directivesMap : undefined
|
||||
}
|
||||
|
||||
private extractEntities(
|
||||
node: SelectionSetNode,
|
||||
parentName = "",
|
||||
mainService = ""
|
||||
): Entity[] {
|
||||
const entities: Entity[] = []
|
||||
|
||||
node.selections.forEach((selection) => {
|
||||
if (selection.kind === "Field") {
|
||||
const fieldNode = selection as FieldNode
|
||||
|
||||
if (!fieldNode.selectionSet) {
|
||||
return
|
||||
}
|
||||
|
||||
const propName = fieldNode.name.value
|
||||
const entityName = parentName ? `${parentName}.${propName}` : propName
|
||||
|
||||
const nestedEntity: Entity = {
|
||||
property: entityName.replace(`${mainService}.`, ""),
|
||||
fields: fieldNode.selectionSet.selections.map(
|
||||
(field) => (field as FieldNode).name.value
|
||||
),
|
||||
args: this.parseArguments(fieldNode.arguments || []),
|
||||
directives: this.createDirectivesMap(fieldNode.selectionSet),
|
||||
}
|
||||
|
||||
entities.push(nestedEntity)
|
||||
entities.push(
|
||||
...this.extractEntities(
|
||||
fieldNode.selectionSet,
|
||||
entityName,
|
||||
mainService
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
public parseQuery(): RemoteJoinerQuery {
|
||||
const queryDefinition = this.ast.definitions.find(
|
||||
(definition) => definition.kind === "OperationDefinition"
|
||||
) as OperationDefinitionNode
|
||||
|
||||
if (!queryDefinition) {
|
||||
throw new Error("No query found")
|
||||
}
|
||||
|
||||
const rootFieldNode = queryDefinition.selectionSet
|
||||
.selections[0] as FieldNode
|
||||
const propName = rootFieldNode.name.value
|
||||
|
||||
const remoteJoinConfig: RemoteJoinerQuery = {
|
||||
alias: propName,
|
||||
fields: [],
|
||||
expands: [],
|
||||
}
|
||||
|
||||
if (rootFieldNode.arguments) {
|
||||
remoteJoinConfig.args = this.parseArguments(rootFieldNode.arguments)
|
||||
}
|
||||
|
||||
if (rootFieldNode.selectionSet) {
|
||||
remoteJoinConfig.fields = rootFieldNode.selectionSet.selections.map(
|
||||
(field) => (field as FieldNode).name.value
|
||||
)
|
||||
remoteJoinConfig.directives = this.createDirectivesMap(
|
||||
rootFieldNode.selectionSet
|
||||
)
|
||||
remoteJoinConfig.expands = this.extractEntities(
|
||||
rootFieldNode.selectionSet,
|
||||
propName,
|
||||
propName
|
||||
)
|
||||
}
|
||||
|
||||
return remoteJoinConfig
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { GraphQLNamedType, GraphQLObjectType, isObjectType } from "graphql"
|
||||
|
||||
/**
|
||||
* From graphql schema get all the fields for the requested type and relations
|
||||
*
|
||||
* @param schemaTypeMap
|
||||
* @param typeName
|
||||
* @param relations
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const userModule = `
|
||||
* type User {
|
||||
* id: ID!
|
||||
* name: String!
|
||||
* blabla: WHATEVER
|
||||
* }
|
||||
*
|
||||
* type Post {
|
||||
* author: User!
|
||||
* }
|
||||
* `
|
||||
*
|
||||
* const postModule = `
|
||||
* type Post {
|
||||
* id: ID!
|
||||
* title: String!
|
||||
* date: String
|
||||
* }
|
||||
*
|
||||
* type User {
|
||||
* posts: [Post!]!
|
||||
* }
|
||||
*
|
||||
* type WHATEVER {
|
||||
* random_field: String
|
||||
* post: Post
|
||||
* }
|
||||
* `
|
||||
*
|
||||
* const mergedSchema = mergeTypeDefs([userModule, postModule])
|
||||
* const schema = makeExecutableSchema({
|
||||
* typeDefs: mergedSchema,
|
||||
* })
|
||||
*
|
||||
* const fields = graphqlSchemaToFields(types, "User", ["posts"])
|
||||
*
|
||||
* console.log(fields)
|
||||
*
|
||||
* // [
|
||||
* // "id",
|
||||
* // "name",
|
||||
* // "posts.id",
|
||||
* // "posts.title",
|
||||
* // "posts.date",
|
||||
* // ]
|
||||
*/
|
||||
export function graphqlSchemaToFields(
|
||||
schemaTypeMap: { [key: string]: GraphQLNamedType },
|
||||
typeName: string,
|
||||
relations: string[] = []
|
||||
) {
|
||||
const result: string[] = []
|
||||
|
||||
function traverseFields(typeName, parent = "") {
|
||||
const type = schemaTypeMap[typeName]
|
||||
|
||||
if (!(type instanceof GraphQLObjectType)) {
|
||||
return
|
||||
}
|
||||
|
||||
const fields = type.getFields()
|
||||
|
||||
for (const fieldName in fields) {
|
||||
const field = fields[fieldName]
|
||||
let fieldType = field.type as any
|
||||
|
||||
while (fieldType.ofType) {
|
||||
fieldType = fieldType.ofType
|
||||
}
|
||||
|
||||
const composedField = parent ? `${parent}.${fieldName}` : fieldName
|
||||
if (!isObjectType(fieldType)) {
|
||||
result.push(composedField)
|
||||
} else if (relations.includes(composedField)) {
|
||||
traverseFields(fieldType.name, composedField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields(typeName)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { FileSystem, toCamelCase } from "@medusajs/utils"
|
||||
import { GraphQLSchema } from "graphql/type"
|
||||
import { parse, printSchema } from "graphql"
|
||||
import { codegen } from "@graphql-codegen/core"
|
||||
import * as typescriptPlugin from "@graphql-codegen/typescript"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
|
||||
function buildEntryPointsTypeMap({
|
||||
schema,
|
||||
joinerConfigs,
|
||||
}: {
|
||||
schema: string
|
||||
joinerConfigs: ModuleJoinerConfig[]
|
||||
}): { entryPoint: string; entityType: any }[] {
|
||||
// build map entry point to there type to be merged and used by the remote query
|
||||
|
||||
return joinerConfigs
|
||||
.flatMap((config) => {
|
||||
const aliases = Array.isArray(config.alias)
|
||||
? config.alias
|
||||
: config.alias
|
||||
? [config.alias]
|
||||
: []
|
||||
|
||||
return aliases.flatMap((alias) => {
|
||||
const names = Array.isArray(alias.name) ? alias.name : [alias.name]
|
||||
const entity = alias?.["entity"]
|
||||
return names.map((aliasItem) => {
|
||||
return {
|
||||
entryPoint: aliasItem,
|
||||
entityType: entity
|
||||
? schema.includes(`export type ${entity} `)
|
||||
? alias?.["entity"]
|
||||
: "any"
|
||||
: "any",
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
async function generateTypes({
|
||||
outputDir,
|
||||
filename,
|
||||
config,
|
||||
joinerConfigs,
|
||||
}: {
|
||||
outputDir: string
|
||||
filename: string
|
||||
config: Parameters<typeof codegen>[0]
|
||||
joinerConfigs: ModuleJoinerConfig[]
|
||||
}) {
|
||||
const fileSystem = new FileSystem(outputDir)
|
||||
|
||||
let output = await codegen(config)
|
||||
const entryPoints = buildEntryPointsTypeMap({ schema: output, joinerConfigs })
|
||||
|
||||
const interfaceName = toCamelCase(filename)
|
||||
|
||||
const remoteQueryEntryPoints = `
|
||||
declare module '@medusajs/types' {
|
||||
interface ${interfaceName} {
|
||||
${entryPoints
|
||||
.map((entry) => ` ${entry.entryPoint}: ${entry.entityType}`)
|
||||
.join("\n")}
|
||||
}
|
||||
}`
|
||||
|
||||
output += remoteQueryEntryPoints
|
||||
|
||||
await fileSystem.create(filename + ".d.ts", output)
|
||||
|
||||
const doesBarrelExists = await fileSystem.exists("index.d.ts")
|
||||
if (!doesBarrelExists) {
|
||||
await fileSystem.create(
|
||||
"index.d.ts",
|
||||
`export * as ${interfaceName}Types from './${filename}'`
|
||||
)
|
||||
} else {
|
||||
const content = await fileSystem.contents("index.d.ts")
|
||||
if (!content.includes(`${interfaceName}Types`)) {
|
||||
const newContent = `export * as ${interfaceName}Types from './${filename}'\n${content}`
|
||||
await fileSystem.create("index.d.ts", newContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: rename from gqlSchemaToTypes to grapthqlToTsTypes
|
||||
export async function gqlSchemaToTypes({
|
||||
schema,
|
||||
outputDir,
|
||||
filename,
|
||||
joinerConfigs,
|
||||
}: {
|
||||
schema: GraphQLSchema
|
||||
outputDir: string
|
||||
filename: string
|
||||
joinerConfigs: ModuleJoinerConfig[]
|
||||
}) {
|
||||
const config = {
|
||||
documents: [],
|
||||
config: {
|
||||
scalars: {
|
||||
DateTime: { input: "Date | string", output: "Date | string" },
|
||||
JSON: {
|
||||
input: "Record<string, unknown>",
|
||||
output: "Record<string, unknown>",
|
||||
},
|
||||
},
|
||||
},
|
||||
filename: "",
|
||||
schema: parse(printSchema(schema as any)),
|
||||
plugins: [
|
||||
// Each plugin should be an object
|
||||
{
|
||||
typescript: {}, // Here you can pass configuration to the plugin
|
||||
},
|
||||
],
|
||||
pluginMap: {
|
||||
typescript: typescriptPlugin,
|
||||
},
|
||||
}
|
||||
|
||||
await generateTypes({ outputDir, filename, config, joinerConfigs })
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from "./graphql-parser"
|
||||
export * from "./graphql-to-fields"
|
||||
export * from "./extract-relations-from-graphql"
|
||||
export * from "./clean-graphql"
|
||||
export * from "./graphql-to-ts-types"
|
||||
export * from "./get-fields-and-relations"
|
||||
|
||||
export * from "graphql"
|
||||
export * from "graphql/type"
|
||||
Reference in New Issue
Block a user