Merge pull request #12684 from medusajs/pedro/keep-enum-values-in-types-generation

fix: keep enum values in types generation
fix: generate union types instead of enums
This commit is contained in:
Pedro Guzman
2025-06-13 13:15:56 +02:00
committed by GitHub
9 changed files with 150 additions and 15 deletions
@@ -234,6 +234,7 @@ function cleanAndMergeSchema(loadedSchema) {
const defaultMedusaSchema = `
scalar DateTime
scalar JSON
directive @enumValue(value: String) on ENUM_VALUE
`
const { schema: cleanedSchema, notFound } = GraphQLUtils.cleanGraphQLSchema(
defaultMedusaSchema + loadedSchema
@@ -13,6 +13,11 @@ describe("GraphQL builder", () => {
isVerified: model.boolean(),
})
enum DepartmentEnum {
FinanceDept = "finance",
MarketingDept = "marketing",
}
const user = model.define("user", {
id: model.id(),
username: model.text(),
@@ -21,8 +26,9 @@ describe("GraphQL builder", () => {
phones: model.array(),
group: model.belongsTo(() => group, { mappedBy: "users" }),
role: model
.enum(["moderator", "admin", "guest", "new_user"])
.enum(["moderator", "admin", "guest", "new-user"])
.default("guest"),
department: model.enum(DepartmentEnum),
tags: model.manyToMany(() => tag, {
pivotTable: "custom_user_tags",
}),
@@ -38,6 +44,9 @@ describe("GraphQL builder", () => {
const toGql = toGraphQLSchema([tag, email, user, group])
const expected = `
scalar DateTime
scalar JSON
directive @enumValue(value: String) on ENUM_VALUE
type Tag {
id: ID!
value: String!
@@ -59,10 +68,15 @@ describe("GraphQL builder", () => {
}
enum UserRoleEnum {
MODERATOR
ADMIN
GUEST
NEW_USER
MODERATOR @enumValue(value: "moderator")
ADMIN @enumValue(value: "admin")
GUEST @enumValue(value: "guest")
NEW_USER @enumValue(value: "new-user")
}
enum UserDepartmentEnum {
FINANCE @enumValue(value: "finance")
MARKETING @enumValue(value: "marketing")
}
type User {
@@ -74,6 +88,7 @@ describe("GraphQL builder", () => {
group_id:String!
group: Group!
role: UserRoleEnum!
department: UserDepartmentEnum!
tags: [Tag]!
raw_spend_limit: JSON!
created_at: DateTime!
@@ -1,9 +1,9 @@
import type { PropertyType } from "@medusajs/types"
import { DmlEntity } from "../entity"
import { parseEntityName } from "./entity-builder/parse-entity-name"
import { setGraphQLRelationship } from "./graphql-builder/set-relationship"
import { getGraphQLAttributeFromDMLPropety } from "./graphql-builder/get-attribute"
import { getForeignKey } from "./entity-builder"
import { parseEntityName } from "./entity-builder/parse-entity-name"
import { getGraphQLAttributeFromDMLPropety } from "./graphql-builder/get-attribute"
import { setGraphQLRelationship } from "./graphql-builder/set-relationship"
export function generateGraphQLFromEntity<T extends DmlEntity<any, any>>(
entity: T
@@ -82,5 +82,14 @@ export const toGraphQLSchema = <T extends any[]>(entities: T): string => {
return entity
})
return gqlSchemas.join("\n")
const defaultMedusaSchema =
gqlSchemas.length > 0
? `
scalar DateTime
scalar JSON
directive @enumValue(value: String) on ENUM_VALUE
`
: ""
return defaultMedusaSchema + gqlSchemas.join("\n")
}
@@ -44,7 +44,7 @@ export function getGraphQLAttributeFromDMLPropety(
const enumValues = field.dataType
.options!.choices.map((value) => {
const enumValue = value.replace(/[^a-z0-9_]/gi, "_").toUpperCase()
return ` ${enumValue}`
return ` ${enumValue} @enumValue(value: "${value}")`
})
.join("\n")
@@ -0,0 +1 @@
.test-output
@@ -0,0 +1,65 @@
import { makeExecutableSchema } from "@graphql-tools/schema"
import fs from "fs"
import path from "path"
import { gqlSchemaToTypes } from "../graphql-to-ts-types"
describe("gqlSchemaToTypes", () => {
it("should use enumValue directive for enum values", async () => {
const schema = `
directive @enumValue(value: String) on ENUM_VALUE
enum Test {
Test_A @enumValue(value: "test-a")
Test_B
}
`
const executableSchema = makeExecutableSchema({
typeDefs: schema,
})
await gqlSchemaToTypes({
schema: executableSchema,
outputDir: path.resolve(__dirname, ".test-output/enum-values"),
filename: "query-entry-points",
joinerConfigs: [],
interfaceName: "RemoteQueryEntryPoints",
})
const expectedTypes = `
import "@medusajs/framework/types"
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
};
export type Test = | 'test-a' | 'Test_B';
declare module '@medusajs/framework/types' {
interface RemoteQueryEntryPoints {
}
}`
const fileName = ".test-output/enum-values/query-entry-points.d.ts"
const generatedTypes = fs
.readFileSync(path.resolve(__dirname, fileName))
.toString()
expect(normalizeFile(generatedTypes)).toEqual(normalizeFile(expectedTypes))
})
})
const normalizeFile = (file: string) => {
return file.replace(/^\s+/g, "").replace(/\s+/g, " ").trim()
}
@@ -1,7 +1,14 @@
import { codegen } from "@graphql-codegen/core"
import * as typescriptPlugin from "@graphql-codegen/typescript"
import { ModuleJoinerConfig } from "@medusajs/types"
import { type GraphQLSchema, parse, printSchema } from "graphql"
import {
EnumTypeDefinitionNode,
EnumValueDefinitionNode,
type GraphQLSchema,
Kind,
parse,
printSchema,
} from "graphql"
import { FileSystem } from "../common"
function buildEntryPointsTypeMap({
@@ -87,6 +94,35 @@ ${entryPoints
}
}
const getEnumValues = (schema: GraphQLSchema) => {
const enumTypes = Object.values(schema.getTypeMap()).filter(
(type) => type.astNode?.kind === Kind.ENUM_TYPE_DEFINITION
)
const enumValues = {}
enumTypes.forEach((type) => {
const enumName = type.name
enumValues[enumName] = {}
const nodes = (type.astNode as EnumTypeDefinitionNode).values || []
nodes.forEach((node: EnumValueDefinitionNode) => {
const directive = node.directives?.find(
(d) => d.name.value === "enumValue"
)
if (directive) {
const valueArg = directive.arguments?.find(
(a) => a.name.value === "value"
)
if (valueArg && valueArg.value.kind === Kind.STRING) {
enumValues[enumName][node.name.value] = valueArg.value.value
}
}
})
})
return enumValues
}
// TODO: rename from gqlSchemaToTypes to grapthqlToTsTypes
export async function gqlSchemaToTypes({
schema,
@@ -118,9 +154,11 @@ export async function gqlSchemaToTypes({
filename: "",
schema: parse(printSchema(schema as any)),
plugins: [
// Each plugin should be an object
{
typescript: {}, // Here you can pass configuration to the plugin
typescript: {
enumValues: getEnumValues(schema),
enumsAsTypes: true,
},
},
],
pluginMap: {
@@ -405,7 +405,10 @@ describe("joiner-config-builder", () => {
],
})
const schemaExpected = `type FulfillmentSet {
const schemaExpected = `scalar DateTime
scalar JSON
directive @enumValue(value: String) on ENUM_VALUE
type FulfillmentSet {
id: ID!
created_at: DateTime!
updated_at: DateTime!
@@ -1253,7 +1253,10 @@ export function buildSchemaObjectRepresentation(schema: string): {
} as IndexTypes.SchemaObjectRepresentation
Object.entries(entitiesMap).forEach(([entityName, entityMapValue]) => {
if (!entityMapValue.astNode) {
if (
!entityMapValue.astNode ||
entityMapValue.astNode.kind === GraphQLUtils.Kind.SCALAR_TYPE_DEFINITION
) {
return
}