chore: Move graphl to a single place (#9303)

* chore: Move graphl to a single place

* add new line
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-09-25 12:04:25 +02:00
1 parent 1d3a60023a
commit 358435d715
30 files changed
+78 -62

No files matched your search

-1
View File
@@ -44,7 +44,6 @@
"@graphql-tools/schema": "^10.0.0",
"@medusajs/orchestration": "^0.5.7",
"@medusajs/utils": "^1.11.9",
"graphql": "^16.6.0",
"resolve-cwd": "^3.0.0"
},
"peerDependencies": {
@@ -1,111 +0,0 @@
import { cleanGraphQLSchema } from "../utils/clean-graphql-schema"
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" },
})
})
})
@@ -1,94 +0,0 @@
import { mergeTypeDefs } from "@graphql-tools/merge"
import { makeExecutableSchema } from "@graphql-tools/schema"
import { gqlGetFieldsAndRelations } from "../../utils"
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",
])
)
})
})
@@ -1,110 +0,0 @@
import { mergeTypeDefs } from "@graphql-tools/merge"
import { makeExecutableSchema } from "@graphql-tools/schema"
import { graphqlSchemaToFields } from "../../utils"
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",
])
)
})
})
+3 -4
View File
@@ -20,6 +20,7 @@ import {
ContainerRegistrationKeys,
createMedusaContainer,
dynamicImport,
GraphQLUtils,
isObject,
isString,
MedusaError,
@@ -29,7 +30,6 @@ import {
} from "@medusajs/utils"
import type { Knex } from "@mikro-orm/knex"
import { asValue } from "awilix"
import { GraphQLSchema } from "graphql/type"
import { MODULE_PACKAGE_NAMES } from "./definitions"
import {
MedusaModule,
@@ -39,7 +39,6 @@ import {
import { RemoteLink } from "./remote-link"
import { createQuery, RemoteQuery } from "./remote-query"
import { MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types"
import { cleanGraphQLSchema } from "./utils"
const LinkModulePackage = MODULE_PACKAGE_NAMES[Modules.LINK]
@@ -204,7 +203,7 @@ function cleanAndMergeSchema(loadedSchema) {
scalar DateTime
scalar JSON
`
const { schema: cleanedSchema, notFound } = cleanGraphQLSchema(
const { schema: cleanedSchema, notFound } = GraphQLUtils.cleanGraphQLSchema(
defaultMedusaSchema + loadedSchema
)
const mergedSchema = mergeTypeDefs(cleanedSchema)
@@ -232,7 +231,7 @@ export type MedusaAppOutput = {
link: RemoteLink | undefined
query: RemoteQueryFunction
entitiesMap?: Record<string, any>
gqlSchema?: GraphQLSchema
gqlSchema?: GraphQLUtils.GraphQLSchema
notFound?: Record<string, Record<string, string>>
runMigrations: RunMigrationFn
revertMigrations: RevertMigrationFn
@@ -1,6 +1,6 @@
import { mergeTypeDefs } from "@graphql-tools/merge"
import { makeExecutableSchema } from "@graphql-tools/schema"
import { cleanGraphQLSchema } from "../../utils/clean-graphql-schema"
import { cleanGraphQLSchema } from "@medusajs/utils"
export function getEntitiesMap(loadedSchema): Map<string, any> {
const defaultMedusaSchema = `
@@ -7,5 +7,3 @@ export enum MODULE_RESOURCE_TYPE {
SHARED = "shared",
ISOLATED = "isolated",
}
export { GraphQLSchema } from "graphql"
@@ -1,91 +0,0 @@
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,
}
}
@@ -1,43 +0,0 @@
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
}
@@ -1,118 +0,0 @@
import { MedusaModule } from "../medusa-module"
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"
function buildEntryPointsTypeMap(
schema: string
): { entryPoint: string; entityType: any }[] {
// build map entry point to there type to be merged and used by the remote query
const joinerConfigs = MedusaModule.getAllJoinerConfigs()
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,
}: {
outputDir: string
filename: string
config: Parameters<typeof codegen>[0]
}) {
const fileSystem = new FileSystem(outputDir)
let output = await codegen(config)
const entryPoints = buildEntryPointsTypeMap(output)
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)
}
}
}
export async function gqlSchemaToTypes({
schema,
outputDir,
filename,
}: {
schema: GraphQLSchema
outputDir: string
filename: string
}) {
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 })
}
@@ -1,93 +0,0 @@
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
}
+2 -4
View File
@@ -1,4 +1,2 @@
export * from "./clean-graphql-schema"
export * from "./graphql-schema-to-fields"
export * from "./gql-schema-to-types"
export * from "./gql-get-fields-and-relations"
export * from "./linking-error"
export * from "./convert-data-to-link-definition"