diff --git a/.azure-devops/graphitation-release.yml b/.azure-devops/graphitation-release.yml index 720a446b0..289d753bc 100644 --- a/.azure-devops/graphitation-release.yml +++ b/.azure-devops/graphitation-release.yml @@ -1,6 +1,7 @@ pr: none trigger: - main + - jvejr/ts-codegen-subtype-alpha-release - alloy/relay-apollo-duct-tape variables: diff --git a/package.json b/package.json index 6d0c7990b..b5aef0858 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,10 @@ "lint": "lage lint --continue", "lage": "lage", "ci": "yarn lage build types test lint && yarn checkchange", - "beachball": "beachball -b origin/main", + "beachball": "beachball -b origin/jvejr/ts-codegen-subtype-alpha-release", "change": "yarn beachball change", "checkchange": "yarn beachball check", - "release": "yarn beachball publish -t latest", + "release": "yarn beachball publish -t alpha", "postinstall": "patch-package" }, "devDependencies": { diff --git a/packages/cli/CHANGELOG.json b/packages/cli/CHANGELOG.json index 071d7fd0b..96ad29a6c 100644 --- a/packages/cli/CHANGELOG.json +++ b/packages/cli/CHANGELOG.json @@ -1,6 +1,72 @@ { "name": "@graphitation/cli", "entries": [ + { + "date": "Tue, 15 Apr 2025 13:35:07 GMT", + "version": "2.1.0-alpha.2", + "tag": "@graphitation/cli_v2.1.0-alpha.2", + "comments": { + "none": [ + { + "author": "beachball", + "package": "@graphitation/cli", + "comment": "Bump @graphitation/ts-codegen to v3.1.0-alpha.3", + "commit": "not available" + } + ] + } + }, + { + "date": "Mon, 17 Mar 2025 14:45:57 GMT", + "version": "2.1.0-alpha.2", + "tag": "@graphitation/cli_v2.1.0-alpha.2", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/cli", + "commit": "c9e29a0d662b1e728faf43ec341453dd54060a97", + "comment": "Return namespaced type" + } + ] + } + }, + { + "date": "Mon, 17 Mar 2025 13:09:38 GMT", + "version": "2.1.0-alpha.1", + "tag": "@graphitation/cli_v2.1.0-alpha.1", + "comments": { + "none": [ + { + "author": "beachball", + "package": "@graphitation/cli", + "comment": "Bump @graphitation/ts-codegen to v3.1.0-alpha.2", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 04 Mar 2025 14:17:11 GMT", + "version": "2.1.0-alpha.1", + "tag": "@graphitation/cli_v2.1.0-alpha.1", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/cli", + "commit": "fb824e93c1b429874ca72516590d566daa4a13f1", + "comment": "ts-codegen context subtype v2" + }, + { + "author": "beachball", + "package": "@graphitation/cli", + "comment": "Bump @graphitation/ts-codegen to v3.1.0-alpha.1", + "commit": "not available" + } + ] + } + }, { "date": "Wed, 19 Feb 2025 14:18:14 GMT", "version": "2.0.0", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 9d82c9a25..80abdd323 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,9 +1,26 @@ # Change Log - @graphitation/cli - + +## 2.1.0-alpha.2 + +Mon, 17 Mar 2025 14:45:57 GMT + +### Changes + +- Return namespaced type (77059398+vejrj@users.noreply.github.com) + +## 2.1.0-alpha.1 + +Tue, 04 Mar 2025 14:17:11 GMT + +### Changes + +- ts-codegen context subtype v2 (77059398+vejrj@users.noreply.github.com) +- Bump @graphitation/ts-codegen to v3.1.0-alpha.1 + ## 2.0.0 Wed, 19 Feb 2025 14:18:14 GMT diff --git a/packages/cli/package.json b/packages/cli/package.json index 5ba0df816..5ca478680 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@graphitation/cli", "license": "MIT", - "version": "2.0.0", + "version": "2.1.0-alpha.2", "bin": { "supermassive": "./bin/supermassive.js" }, @@ -24,7 +24,7 @@ }, "dependencies": { "@graphitation/supermassive-extractors": "^2.2.5", - "@graphitation/ts-codegen": "^3.0.0", + "@graphitation/ts-codegen": "^3.1.0-alpha.3", "commander": "^8.3.0", "fast-glob": "^3.2.12", "graphql": "^15.6.1" diff --git a/packages/cli/src/supermassive.ts b/packages/cli/src/supermassive.ts index fa04d845b..99dfadf4d 100644 --- a/packages/cli/src/supermassive.ts +++ b/packages/cli/src/supermassive.ts @@ -16,13 +16,10 @@ type GenerateInterfacesOptions = { legacy?: boolean; legacyModels?: boolean; useStringUnionsInsteadOfEnums?: boolean; - enumMigrationJsonFile?: string; - enumMigrationExceptionsJsonFile?: string; + contextSubTypeMetadataFile?: string; generateOnlyEnums?: boolean; generateResolverMap?: boolean; mandatoryResolverTypes?: boolean; - contextSubTypeNameTemplate?: string; - contextSubTypePathTemplate?: string; defaultContextSubTypePath?: string; defaultContextSubTypeName?: string; scope?: string; @@ -63,14 +60,6 @@ export function supermassive(): Command { "-dcn, --default-context-sub-type-name [defaultContextSubTypeName]", "Default context type which will extend context sub type", ) - .option( - "-cnt, --context-sub-type-name-template [contextSubTypeNameTemplate]", - "context resource name template. You need to specify ${resourceName} in the parameter eg. `${resourceName}Context`", - ) - .option( - "-cpt, --context-sub-type-path-template [contextSubTypePathTemplate]", - "context resource path template. You need to specify ${resourceName} in the parameter eg. `@package/preffix-${resourceName}-suffix`", - ) .option("-ei, --enums-import [enumsImport]", "from where to import enums") .option("-l, --legacy", "generate legacy types") .option("--legacy-models", "do not use models for object types") @@ -81,12 +70,8 @@ export function supermassive(): Command { .option("--generate-only-enums", "Generate only enum file") .option("--scope [scope]", "generate models only for scope") .option( - "--enum-migration-json-file [enumMigrationJsonFile]", - "File containing array of enum names, which should be migrated to string unions", - ) - .option( - "--enum-migration-exceptions-json-file [enumMigrationExceptionsJsonFile]", - "File containing array of enum names, which should remain typescript enums", + "--context-sub-type-metadata-file [contextSubTypeMetadataFile]", + "Subtype metadata file", ) .option( "--generate-resolver-map", @@ -163,42 +148,20 @@ async function generateInterfaces( path.dirname(fullPath), options.outputDir ? options.outputDir : "__generated__", ); - let enumNamesToMigrate; - let enumNamesToKeep; - if (options.enumMigrationJsonFile) { - const content = JSON.parse( - await fs.readFile( - path.join(process.cwd(), options.enumMigrationJsonFile), - { - encoding: "utf-8", - }, - ), - ); - if (!Array.isArray(content)) { - throw new Error("enumMigrationJsonFile doesn't contain an array"); - } + let contextSubTypeMetadata: Record | undefined; - enumNamesToMigrate = content as string[]; - } - - if (options.enumMigrationExceptionsJsonFile) { + if (options.contextSubTypeMetadataFile) { const content = JSON.parse( await fs.readFile( - path.join(process.cwd(), options.enumMigrationExceptionsJsonFile), + path.join(process.cwd(), options.contextSubTypeMetadataFile), { encoding: "utf-8", }, ), ); - if (!Array.isArray(content)) { - throw new Error( - "enumMigrationExceptionsJsonFile doesn't contain an array", - ); - } - - enumNamesToKeep = content as string[]; + contextSubTypeMetadata = content; } const result = generateTS(document, { @@ -207,8 +170,6 @@ async function generateInterfaces( contextTypePath: getContextPath(outputPath, options.contextTypePath) || null, contextTypeName: options.contextTypeName, - contextSubTypeNameTemplate: options.contextSubTypeNameTemplate, - contextSubTypePathTemplate: options.contextSubTypePathTemplate, defaultContextSubTypePath: getContextPath( outputPath, options.defaultContextSubTypePath, @@ -221,8 +182,7 @@ async function generateInterfaces( generateOnlyEnums: !!options.generateOnlyEnums, generateResolverMap: !!options.generateResolverMap, mandatoryResolverTypes: !!options.mandatoryResolverTypes, - enumNamesToMigrate, - enumNamesToKeep, + contextSubTypeMetadata, modelScope: options.scope || null, }); diff --git a/packages/ts-codegen/CHANGELOG.json b/packages/ts-codegen/CHANGELOG.json index eb65f1151..c55720295 100644 --- a/packages/ts-codegen/CHANGELOG.json +++ b/packages/ts-codegen/CHANGELOG.json @@ -1,6 +1,51 @@ { "name": "@graphitation/ts-codegen", "entries": [ + { + "date": "Tue, 15 Apr 2025 13:35:07 GMT", + "version": "3.1.0-alpha.3", + "tag": "@graphitation/ts-codegen_v3.1.0-alpha.3", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/ts-codegen", + "commit": "7c44b873c4fe585465bcef72b08170c2940b5432", + "comment": "Metadata dump added" + } + ] + } + }, + { + "date": "Mon, 17 Mar 2025 13:09:38 GMT", + "version": "3.1.0-alpha.2", + "tag": "@graphitation/ts-codegen_v3.1.0-alpha.2", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/ts-codegen", + "commit": "f3a78cb45a5bc94c0309dd43a2c770e062344db4", + "comment": "Return namespaced type" + } + ] + } + }, + { + "date": "Tue, 04 Mar 2025 14:17:11 GMT", + "version": "3.1.0-alpha.1", + "tag": "@graphitation/ts-codegen_v3.1.0-alpha.1", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/ts-codegen", + "commit": "fb824e93c1b429874ca72516590d566daa4a13f1", + "comment": "ts-codegen context subtype v2" + } + ] + } + }, { "date": "Wed, 19 Feb 2025 11:31:28 GMT", "version": "3.0.0", diff --git a/packages/ts-codegen/CHANGELOG.md b/packages/ts-codegen/CHANGELOG.md index 945223c46..c3cd5a8bd 100644 --- a/packages/ts-codegen/CHANGELOG.md +++ b/packages/ts-codegen/CHANGELOG.md @@ -1,9 +1,33 @@ # Change Log - @graphitation/ts-codegen - + +## 3.1.0-alpha.3 + +Tue, 15 Apr 2025 13:35:07 GMT + +### Changes + +- Metadata dump added (77059398+vejrj@users.noreply.github.com) + +## 3.1.0-alpha.2 + +Mon, 17 Mar 2025 13:09:38 GMT + +### Changes + +- Return namespaced type (77059398+vejrj@users.noreply.github.com) + +## 3.1.0-alpha.1 + +Tue, 04 Mar 2025 14:17:11 GMT + +### Changes + +- ts-codegen context subtype v2 (77059398+vejrj@users.noreply.github.com) + ## 3.0.0 Wed, 19 Feb 2025 11:31:28 GMT diff --git a/packages/ts-codegen/package.json b/packages/ts-codegen/package.json index 71c0e1cce..a78de2e57 100644 --- a/packages/ts-codegen/package.json +++ b/packages/ts-codegen/package.json @@ -1,7 +1,7 @@ { "name": "@graphitation/ts-codegen", "license": "MIT", - "version": "3.0.0", + "version": "3.1.0-alpha.3", "main": "./src/index.ts", "repository": { "type": "git", diff --git a/packages/ts-codegen/src/__tests__/context.test.ts b/packages/ts-codegen/src/__tests__/context.test.ts index a118051b5..2d535b6bf 100644 --- a/packages/ts-codegen/src/__tests__/context.test.ts +++ b/packages/ts-codegen/src/__tests__/context.test.ts @@ -3,9 +3,80 @@ import { parse } from "graphql"; import { blankGraphQLTag as graphql } from "../utilities"; import { generateTS } from ".."; import { ContextMap } from "../context"; +import { SubTypeNamespace } from "../codegen"; describe(generateTS, () => { describe("Tests basic syntax GraphQL syntax", () => { + const contextSubTypeMetadata = { + managers: { + user: { + name: "user", + importTypeName: 'UserStateMachineType["user"]', + importPath: "@package/user-state-machine", + }, + whatever: { + name: "whatever", + importTypeName: 'WhateverStateMachineType["whatever"]', + importPath: "@package/whatever-state-machine", + }, + "different-whatever": { + name: "different-whatever", + importTypeName: + 'DifferentWhateverStateMachineType["different-whatever"]', + importPath: "@package/different-whatever-state-machine", + }, + post: { + name: "post", + importTypeName: 'PostStateMachineType["post"]', + importPath: "@package/post-state-machine", + }, + node: { + name: "node", + importTypeName: 'NodeStateMachineType["node"]', + importPath: "@package/node-state-machine", + }, + persona: { + name: "persona", + importTypeName: 'PersonaStateMachineType["persona"]', + importPath: "@package/persona-state-machine", + }, + admin: { + name: "admin", + importTypeName: 'AdminStateMachineType["admin"]', + importPath: "@package/admin-state-machine", + }, + message: { + name: "message", + importTypeName: 'MessageStateMachineType["message"]', + importPath: "@package/message-state-machine", + }, + customer: { + name: "customer", + importTypeName: 'CustomerStateMachineType["customer"]', + importPath: "@package/customer-state-machine", + }, + "shouldnt-apply": { + name: "shouldnt-apply", + importTypeName: 'UserStateMachineType["shouldnt-apply"]', + importPath: "@package/shouldnt-apply-state-machine", + }, + "user-or-customer": { + name: "user-or-customer", + importTypeName: 'UserStateMachineType["user-or-customer"]', + importPath: "@package/user-or-customer-state-machine", + }, + "company-or-customer": { + name: "company-or-customer", + importTypeName: 'UserStateMachineType["company-or-customer"]', + importPath: "@package/company-or-customer-state-machine", + }, + "id-user": { + name: "id-user", + importTypeName: 'UserStateMachineType["id-user"]', + importPath: "@package/id-user-state-machine", + }, + }, + }; test("all possible nullable and non-nullable combinations", () => { const { resolvers, models, enums, inputs, contextMappingOutput } = runGenerateTest( @@ -18,11 +89,11 @@ describe(generateTS, () => { } type Message { - id: ID! @context(uses: ["message"]) + id: ID! @context(uses: { managers: ["message"] }) } - type User @context(uses: ["user"]) { - id: ID! @context(uses: ["id-user"]) + type User @context(uses: { managers: ["user"] }) { + id: ID! @context(uses: { managers: ["id-user", "user"] }) name: String messagesWithAnswersNonRequired: [[Message]] messagesWithAnswersRequired: [[Message]]! @@ -31,7 +102,7 @@ describe(generateTS, () => { messagesWithArrayRequired: [Message]! messagesRequired: [Message!]! messagesOnlyMessageRequired: [Message!] - post: Post @context(uses: ["post"]) + post: Post @context(uses: { managers: ["post"] }) postRequired: Post! avatar: Avatar avatarRequired: Avatar! @@ -47,29 +118,88 @@ describe(generateTS, () => { } `, { - contextSubTypeNameTemplate: "I${resourceName}StateMachineContext", - contextSubTypePathTemplate: - "@msteams/core-cdl-sync-${resourceName}", + contextSubTypeMetadata: contextSubTypeMetadata, + defaultContextSubTypePath: "@package/default-context", + defaultContextSubTypeName: "DefaultContextType", }, ); expect(enums).toMatchInlineSnapshot(`undefined`); expect(contextMappingOutput).toMatchInlineSnapshot(` { "Message": { - "id": [ - "message", - ], + "id": { + "managers": [ + "message", + ], + }, }, "User": { - "__context": [ - "user", - ], - "id": [ - "id-user", - ], - "post": [ - "post", - ], + "avatar": { + "managers": [ + "user", + ], + }, + "avatarRequired": { + "managers": [ + "user", + ], + }, + "id": { + "managers": [ + "id-user", + "user", + ], + }, + "messagesNonRequired": { + "managers": [ + "user", + ], + }, + "messagesOnlyMessageRequired": { + "managers": [ + "user", + ], + }, + "messagesRequired": { + "managers": [ + "user", + ], + }, + "messagesWithAnswersAllRequired": { + "managers": [ + "user", + ], + }, + "messagesWithAnswersNonRequired": { + "managers": [ + "user", + ], + }, + "messagesWithAnswersRequired": { + "managers": [ + "user", + ], + }, + "messagesWithArrayRequired": { + "managers": [ + "user", + ], + }, + "name": { + "managers": [ + "user", + ], + }, + "post": { + "managers": [ + "post", + ], + }, + "postRequired": { + "managers": [ + "user", + ], + }, }, } `); @@ -111,10 +241,10 @@ describe(generateTS, () => { import type { PromiseOrValue } from "@graphitation/supermassive"; import type { ResolveInfo } from "@graphitation/supermassive"; import * as Models from "./models.interface"; - import type { IMessageStateMachineContext } from "@msteams/core-cdl-sync-message"; - import type { IUserStateMachineContext } from "@msteams/core-cdl-sync-user"; - import type { IIdUserStateMachineContext } from "@msteams/core-cdl-sync-id-user"; - import type { IPostStateMachineContext } from "@msteams/core-cdl-sync-post"; + import type { DefaultContextType } from "@package/default-context"; + import type { MessageStateMachineType } from "@package/message-state-machine"; + import type { UserStateMachineType } from "@package/user-state-machine"; + import type { PostStateMachineType } from "@package/post-state-machine"; export declare namespace Post { export interface Resolvers { readonly id?: id; @@ -125,7 +255,11 @@ describe(generateTS, () => { export interface Resolvers { readonly id?: id; } - export type id = (model: Models.Message, args: {}, context: IMessageStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type id = (model: Models.Message, args: {}, context: DefaultContextType & { + managers: { + "message": MessageStateMachineType["message"]; + }; + }, info: ResolveInfo) => PromiseOrValue; } export declare namespace User { export interface Resolvers { @@ -143,19 +277,72 @@ describe(generateTS, () => { readonly avatar?: avatar; readonly avatarRequired?: avatarRequired; } - export type id = (model: Models.User, args: {}, context: IIdUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; - export type name = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; - export type messagesWithAnswersNonRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue | null | undefined> | null | undefined>; - export type messagesWithAnswersRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue | null | undefined>>; - export type messagesWithAnswersAllRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue>>; - export type messagesNonRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue | null | undefined>; - export type messagesWithArrayRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue>; - export type messagesRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue>; - export type messagesOnlyMessageRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue | null | undefined>; - export type post = (model: Models.User, args: {}, context: IPostStateMachineContext, info: ResolveInfo) => PromiseOrValue; - export type postRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; - export type avatar = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; - export type avatarRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type id = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "id-user": UserStateMachineType["id-user"]; + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue; + export type name = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue; + export type messagesWithAnswersNonRequired = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue | null | undefined> | null | undefined>; + export type messagesWithAnswersRequired = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue | null | undefined>>; + export type messagesWithAnswersAllRequired = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue>>; + export type messagesNonRequired = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue | null | undefined>; + export type messagesWithArrayRequired = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue>; + export type messagesRequired = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue>; + export type messagesOnlyMessageRequired = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue | null | undefined>; + export type post = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "post": PostStateMachineType["post"]; + }; + }, info: ResolveInfo) => PromiseOrValue; + export type postRequired = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue; + export type avatar = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue; + export type avatarRequired = (model: Models.User, args: {}, context: DefaultContextType & { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue; } export declare namespace Query { export interface Resolvers { @@ -311,11 +498,11 @@ describe(generateTS, () => { const { resolvers, models, enums, inputs, contextMappingOutput } = runGenerateTest( graphql` - interface Node @context(uses: ["node"]) { + interface Node @context(uses: { managers: ["node"] }) { id: ID! } - interface Persona @context(uses: ["persona"]) { + interface Persona @context(uses: { managers: ["persona"] }) { phone: String! } @@ -324,7 +511,8 @@ describe(generateTS, () => { name: String! } - type Admin implements Node & Persona @context(uses: ["admin"]) { + type Admin implements Node & Persona + @context(uses: { managers: ["admin"] }) { id: ID! rank: Int! } @@ -335,26 +523,31 @@ describe(generateTS, () => { } `, { - contextSubTypeNameTemplate: "I${resourceName}StateMachineContext", - contextSubTypePathTemplate: - "@msteams/core-cdl-sync-${resourceName}", + contextSubTypeMetadata, }, ); expect(enums).toMatchInlineSnapshot(`undefined`); expect(contextMappingOutput).toMatchInlineSnapshot(` { "Admin": { - "__context": [ - "admin", - ], + "id": { + "managers": [ + "admin", + ], + }, + "rank": { + "managers": [ + "admin", + ], + }, }, "Node": { - "__context": [ + "managers": [ "node", ], }, "Persona": { - "__context": [ + "managers": [ "persona", ], }, @@ -386,20 +579,28 @@ describe(generateTS, () => { "import type { PromiseOrValue } from "@graphitation/supermassive"; import type { ResolveInfo } from "@graphitation/supermassive"; import * as Models from "./models.interface"; - import type { INodeStateMachineContext } from "@msteams/core-cdl-sync-node"; - import type { IPersonaStateMachineContext } from "@msteams/core-cdl-sync-persona"; - import type { IAdminStateMachineContext } from "@msteams/core-cdl-sync-admin"; + import type { NodeStateMachineType } from "@package/node-state-machine"; + import type { PersonaStateMachineType } from "@package/persona-state-machine"; + import type { AdminStateMachineType } from "@package/admin-state-machine"; export declare namespace Node { export interface Resolvers { readonly __resolveType?: __resolveType; } - export type __resolveType = (parent: unknown, context: INodeStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type __resolveType = (parent: unknown, context: { + managers: { + "node": NodeStateMachineType["node"]; + }; + }, info: ResolveInfo) => PromiseOrValue; } export declare namespace Persona { export interface Resolvers { readonly __resolveType?: __resolveType; } - export type __resolveType = (parent: unknown, context: IPersonaStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type __resolveType = (parent: unknown, context: { + managers: { + "persona": PersonaStateMachineType["persona"]; + }; + }, info: ResolveInfo) => PromiseOrValue; } export declare namespace User { export interface Resolvers { @@ -412,8 +613,16 @@ describe(generateTS, () => { readonly id?: id; readonly rank?: rank; } - export type id = (model: Models.Admin, args: {}, context: IAdminStateMachineContext, info: ResolveInfo) => PromiseOrValue; - export type rank = (model: Models.Admin, args: {}, context: IAdminStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type id = (model: Models.Admin, args: {}, context: { + managers: { + "admin": AdminStateMachineType["admin"]; + }; + }, info: ResolveInfo) => PromiseOrValue; + export type rank = (model: Models.Admin, args: {}, context: { + managers: { + "admin": AdminStateMachineType["admin"]; + }; + }, info: ResolveInfo) => PromiseOrValue; } export declare namespace Query { export interface Resolvers { @@ -459,11 +668,12 @@ describe(generateTS, () => { const { resolvers, models, enums, inputs, contextMappingOutput } = runGenerateTest( graphql` - interface Node @context(uses: ["node"]) { + interface Node @context(uses: { managers: ["node"] }) { id: ID! } - interface Customer implements Node @context(uses: ["customer"]) { + interface Customer implements Node + @context(uses: { managers: ["customer"] }) { id: ID! name: String! } @@ -478,21 +688,19 @@ describe(generateTS, () => { } `, { - contextSubTypeNameTemplate: "I${resourceName}StateMachineContext", - contextSubTypePathTemplate: - "@msteams/core-cdl-sync-${resourceName}", + contextSubTypeMetadata, }, ); expect(enums).toMatchInlineSnapshot(`undefined`); expect(contextMappingOutput).toMatchInlineSnapshot(` { "Customer": { - "__context": [ + "managers": [ "customer", ], }, "Node": { - "__context": [ + "managers": [ "node", ], }, @@ -521,19 +729,27 @@ describe(generateTS, () => { "import type { PromiseOrValue } from "@graphitation/supermassive"; import type { ResolveInfo } from "@graphitation/supermassive"; import * as Models from "./models.interface"; - import type { INodeStateMachineContext } from "@msteams/core-cdl-sync-node"; - import type { ICustomerStateMachineContext } from "@msteams/core-cdl-sync-customer"; + import type { NodeStateMachineType } from "@package/node-state-machine"; + import type { CustomerStateMachineType } from "@package/customer-state-machine"; export declare namespace Node { export interface Resolvers { readonly __resolveType?: __resolveType; } - export type __resolveType = (parent: unknown, context: INodeStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type __resolveType = (parent: unknown, context: { + managers: { + "node": NodeStateMachineType["node"]; + }; + }, info: ResolveInfo) => PromiseOrValue; } export declare namespace Customer { export interface Resolvers { readonly __resolveType?: __resolveType; } - export type __resolveType = (parent: unknown, context: ICustomerStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type __resolveType = (parent: unknown, context: { + managers: { + "customer": CustomerStateMachineType["customer"]; + }; + }, info: ResolveInfo) => PromiseOrValue; } export declare namespace User { export interface Resolvers { @@ -555,22 +771,26 @@ describe(generateTS, () => { test("applying @context to enum shouldn't affect anything", () => { const { resolvers, models, enums, inputs, contextMappingOutput } = - runGenerateTest(graphql` - enum PresenceAvailability @context(uses: ["shouldnt-apply"]) { - Available - Away - Offline - } + runGenerateTest( + graphql` + enum PresenceAvailability + @context(uses: { managers: ["shouldnt-apply"] }) { + Available + Away + Offline + } - type User { - id: ID! - availability: PresenceAvailability! - } + type User { + id: ID! + availability: PresenceAvailability! + } - extend type Query { - userById(id: ID!): User - } - `); + extend type Query { + userById(id: ID!): User + } + `, + { contextSubTypeMetadata }, + ); expect(enums).toMatchInlineSnapshot(` "export enum PresenceAvailability { Available = "Available", @@ -631,11 +851,11 @@ describe(generateTS, () => { id: ID! } - type Admin @context(uses: ["admin"]) { + type Admin @context(uses: { managers: ["admin"] }) { id: ID! } - type User @context(uses: ["user"]) { + type User @context(uses: { managers: ["user"] }) { id: ID! } @@ -644,54 +864,63 @@ describe(generateTS, () => { } union UserOrAdmin = User | Admin - union UserOrCustomer @context(uses: ["user-or-customer"]) = + union UserOrCustomer + @context(uses: { managers: ["user-or-customer"] }) = User | Customer - union CompanyOrCustomer @context(uses: ["company-or-customer"]) = + union CompanyOrCustomer + @context(uses: { managers: ["company-or-customer"] }) = Company | Customer extend type Query { - userById(id: ID!): whatever @context(uses: ["whatever"]) + userById(id: ID!): whatever + @context(uses: { managers: ["whatever"] }) userByMail(mail: String): whatever - @context(uses: ["different-whatever"]) + @context(uses: { managers: ["different-whatever"] }) node(id: ID!): Node } `, { - contextSubTypeNameTemplate: "I${resourceName}StateMachineContext", - contextSubTypePathTemplate: - "@msteams/core-cdl-sync-${resourceName}", + contextSubTypeMetadata, }, ); expect(enums).toMatchInlineSnapshot(`undefined`); expect(contextMappingOutput).toMatchInlineSnapshot(` { "Admin": { - "__context": [ - "admin", - ], + "id": { + "managers": [ + "admin", + ], + }, }, "CompanyOrCustomer": { - "__context": [ + "managers": [ "company-or-customer", ], }, "Query": { - "userById": [ - "whatever", - ], - "userByMail": [ - "different-whatever", - ], + "userById": { + "managers": [ + "whatever", + ], + }, + "userByMail": { + "managers": [ + "different-whatever", + ], + }, }, "User": { - "__context": [ - "user", - ], + "id": { + "managers": [ + "user", + ], + }, }, "UserOrCustomer": { - "__context": [ + "managers": [ "user-or-customer", ], }, @@ -731,12 +960,10 @@ describe(generateTS, () => { "import type { PromiseOrValue } from "@graphitation/supermassive"; import type { ResolveInfo } from "@graphitation/supermassive"; import * as Models from "./models.interface"; - import type { IAdminStateMachineContext } from "@msteams/core-cdl-sync-admin"; - import type { IUserStateMachineContext } from "@msteams/core-cdl-sync-user"; - import type { IUserOrCustomerStateMachineContext } from "@msteams/core-cdl-sync-user-or-customer"; - import type { ICompanyOrCustomerStateMachineContext } from "@msteams/core-cdl-sync-company-or-customer"; - import type { IWhateverStateMachineContext } from "@msteams/core-cdl-sync-whatever"; - import type { IDifferentWhateverStateMachineContext } from "@msteams/core-cdl-sync-different-whatever"; + import type { AdminStateMachineType } from "@package/admin-state-machine"; + import type { UserStateMachineType } from "@package/user-state-machine"; + import type { WhateverStateMachineType } from "@package/whatever-state-machine"; + import type { DifferentWhateverStateMachineType } from "@package/different-whatever-state-machine"; export declare namespace Customer { export interface Resolvers { readonly id?: id; @@ -753,13 +980,21 @@ describe(generateTS, () => { export interface Resolvers { readonly id?: id; } - export type id = (model: Models.Admin, args: {}, context: IAdminStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type id = (model: Models.Admin, args: {}, context: { + managers: { + "admin": AdminStateMachineType["admin"]; + }; + }, info: ResolveInfo) => PromiseOrValue; } export declare namespace User { export interface Resolvers { readonly id?: id; } - export type id = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type id = (model: Models.User, args: {}, context: { + managers: { + "user": UserStateMachineType["user"]; + }; + }, info: ResolveInfo) => PromiseOrValue; } export declare namespace Node { export interface Resolvers { @@ -777,13 +1012,21 @@ describe(generateTS, () => { export interface Resolvers { readonly __resolveType?: __resolveType; } - export type __resolveType = (parent: Models.User | Models.Customer, context: IUserOrCustomerStateMachineContext, info: ResolveInfo) => PromiseOrValue<"User" | "Customer" | null>; + export type __resolveType = (parent: Models.User | Models.Customer, context: { + managers: { + "user-or-customer": UserStateMachineType["user-or-customer"]; + }; + }, info: ResolveInfo) => PromiseOrValue<"User" | "Customer" | null>; } export declare namespace CompanyOrCustomer { export interface Resolvers { readonly __resolveType?: __resolveType; } - export type __resolveType = (parent: Models.Company | Models.Customer, context: ICompanyOrCustomerStateMachineContext, info: ResolveInfo) => PromiseOrValue<"Company" | "Customer" | null>; + export type __resolveType = (parent: Models.Company | Models.Customer, context: { + managers: { + "company-or-customer": UserStateMachineType["company-or-customer"]; + }; + }, info: ResolveInfo) => PromiseOrValue<"Company" | "Customer" | null>; } export declare namespace Query { export interface Resolvers { @@ -793,10 +1036,18 @@ describe(generateTS, () => { } export type userById = (model: unknown, args: { readonly id: string; - }, context: IWhateverStateMachineContext, info: ResolveInfo) => PromiseOrValue; + }, context: { + managers: { + "whatever": WhateverStateMachineType["whatever"]; + }; + }, info: ResolveInfo) => PromiseOrValue; export type userByMail = (model: unknown, args: { readonly mail?: string | null; - }, context: IDifferentWhateverStateMachineContext, info: ResolveInfo) => PromiseOrValue; + }, context: { + managers: { + "different-whatever": DifferentWhateverStateMachineType["different-whatever"]; + }; + }, info: ResolveInfo) => PromiseOrValue; export type node = (model: unknown, args: { readonly id: string; }, context: unknown, info: ResolveInfo) => PromiseOrValue; @@ -812,17 +1063,15 @@ function runGenerateTest( options: { outputPath?: string; documentPath?: string; - defaultContextTypePath?: string; + defaultContextSubTypeName?: string; + defaultContextSubTypePath?: string; contextName?: string; legacyCompat?: boolean; enumsImport?: string; legacyNoModelsForObjects?: boolean; useStringUnionsInsteadOfEnums?: boolean; - enumNamesToMigrate?: string[]; - enumNamesToKeep?: string[]; modelScope?: string; - contextSubTypeNameTemplate?: string; - contextSubTypePathTemplate?: string; + contextSubTypeMetadata?: SubTypeNamespace; } = {}, ): { enums?: string; @@ -833,23 +1082,18 @@ function runGenerateTest( legacyResolvers?: string; legacyNoModelsForObjects?: boolean; useStringUnionsInsteadOfEnums?: boolean; - enumNamesToMigrate?: string[]; - enumNamesToKeep?: string[]; modelScope?: string; contextMappingOutput: ContextMap | null; } { const fullOptions: { outputPath: string; documentPath: string; - defaultContextTypePath?: string | null; + defaultContextSubTypeName?: string; + defaultContextSubTypePath?: string; contextName?: string; legacyCompat?: boolean; legacyNoModelsForObjects?: boolean; useStringUnionsInsteadOfEnums?: boolean; - enumNamesToMigrate?: string[]; - enumNamesToKeep?: string[]; - contextSubTypeNameTemplate?: string; - contextSubTypePathTemplate?: string; } = { outputPath: "__generated__", documentPath: "./typedef.graphql", diff --git a/packages/ts-codegen/src/__tests__/index.test.ts b/packages/ts-codegen/src/__tests__/index.test.ts index 9f56376e5..e93dced12 100644 --- a/packages/ts-codegen/src/__tests__/index.test.ts +++ b/packages/ts-codegen/src/__tests__/index.test.ts @@ -1735,182 +1735,6 @@ describe(generateTS, () => { `); }); - it("generateTS with string unions instead of enums, but only Enums specified in enumNamesToMigrate will be migrated", () => { - const { models, resolvers, legacyTypes, enums, inputs } = runGenerateTest( - graphql` - interface Node { - id: ID! - } - - enum Type { - type1 - type2 - } - - enum EnumToMigrate { - type1 - type2 - } - - type User implements Node { - id: ID! - userType: Type! - } - - extend type Query { - user(id: ID!): User! - } - `, - { - useStringUnionsInsteadOfEnums: true, - enumNamesToMigrate: ["EnumToMigrate"], - }, - ); - expect(enums).toMatchInlineSnapshot(` - "export enum Type { - type1 = "type1", - type2 = "type2" - } - export type EnumToMigrate = "type1" | "type2"; - " - `); - expect(inputs).toMatchInlineSnapshot(`undefined`); - expect(models).toMatchInlineSnapshot(` - "import * as Enums from "./enums.interface"; - export * from "./enums.interface"; - // Base type for all models. Enables automatic resolution of abstract GraphQL types (interfaces, unions) - export interface BaseModel { - readonly __typename?: string; - } - export interface Node extends BaseModel { - readonly __typename?: string; - } - export interface User extends BaseModel, Node { - readonly __typename?: "User"; - readonly id: string; - readonly userType: Enums.Type; - } - " - `); - expect(resolvers).toMatchInlineSnapshot(` - "import type { PromiseOrValue } from "@graphitation/supermassive"; - import type { ResolveInfo } from "@graphitation/supermassive"; - import * as Models from "./models.interface"; - export declare namespace Node { - export interface Resolvers { - readonly __resolveType?: __resolveType; - } - export type __resolveType = (parent: unknown, context: unknown, info: ResolveInfo) => PromiseOrValue; - } - export declare namespace User { - export interface Resolvers { - readonly id?: id; - readonly userType?: userType; - } - export type id = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; - export type userType = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; - } - export declare namespace Query { - export interface Resolvers { - readonly user?: user; - } - export type user = (model: unknown, args: { - readonly id: string; - }, context: unknown, info: ResolveInfo) => PromiseOrValue; - } - " - `); - expect(legacyTypes).toMatchInlineSnapshot(`undefined`); - }); - - it("generateTS with string unions instead of enums, but enums specified in `enumNamesToKeep` will remain the typescript enums", () => { - const { models, resolvers, legacyTypes, enums, inputs } = runGenerateTest( - graphql` - interface Node { - id: ID! - } - - enum Type { - type1 - type2 - } - - enum EnumToKeep { - type1 - type2 - } - - type User implements Node { - id: ID! - userType: Type! - } - - extend type Query { - user(id: ID!): User! - } - `, - { - useStringUnionsInsteadOfEnums: true, - enumNamesToKeep: ["EnumToKeep"], - }, - ); - expect(enums).toMatchInlineSnapshot(` - "export type Type = "type1" | "type2"; - export enum EnumToKeep { - type1 = "type1", - type2 = "type2" - } - " - `); - expect(inputs).toMatchInlineSnapshot(`undefined`); - expect(models).toMatchInlineSnapshot(` - "import * as Enums from "./enums.interface"; - export * from "./enums.interface"; - // Base type for all models. Enables automatic resolution of abstract GraphQL types (interfaces, unions) - export interface BaseModel { - readonly __typename?: string; - } - export interface Node extends BaseModel { - readonly __typename?: string; - } - export interface User extends BaseModel, Node { - readonly __typename?: "User"; - readonly id: string; - readonly userType: Enums.Type; - } - " - `); - expect(resolvers).toMatchInlineSnapshot(` - "import type { PromiseOrValue } from "@graphitation/supermassive"; - import type { ResolveInfo } from "@graphitation/supermassive"; - import * as Models from "./models.interface"; - export declare namespace Node { - export interface Resolvers { - readonly __resolveType?: __resolveType; - } - export type __resolveType = (parent: unknown, context: unknown, info: ResolveInfo) => PromiseOrValue; - } - export declare namespace User { - export interface Resolvers { - readonly id?: id; - readonly userType?: userType; - } - export type id = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; - export type userType = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; - } - export declare namespace Query { - export interface Resolvers { - readonly user?: user; - } - export type user = (model: unknown, args: { - readonly id: string; - }, context: unknown, info: ResolveInfo) => PromiseOrValue; - } - " - `); - expect(legacyTypes).toMatchInlineSnapshot(`undefined`); - }); - it("legacy interfaces", () => { const { models, resolvers, enums, inputs } = runGenerateTest( graphql` @@ -2247,8 +2071,6 @@ function runGenerateTest( legacyResolvers?: string; legacyNoModelsForObjects?: boolean; useStringUnionsInsteadOfEnums?: boolean; - enumNamesToMigrate?: string[]; - enumNamesToKeep?: string[]; modelScope?: string; } { const fullOptions = { diff --git a/packages/ts-codegen/src/codegen.ts b/packages/ts-codegen/src/codegen.ts index 49a5fcf83..7875af4c7 100644 --- a/packages/ts-codegen/src/codegen.ts +++ b/packages/ts-codegen/src/codegen.ts @@ -1,6 +1,6 @@ import ts from "typescript"; import { DocumentNode } from "graphql"; -import { ContextMap, extractContext } from "./context/index"; +import { extractContext } from "./context/index"; import { generateResolvers } from "./resolvers"; import { generateModels } from "./models"; import { generateLegacyTypes } from "./legacyTypes"; @@ -8,6 +8,18 @@ import { generateLegacyResolvers } from "./legacyResolvers"; import { generateEnums } from "./enums"; import { generateInputs } from "./inputs"; +export type SubTypeItem = { + [name: string]: { + name: string; + importTypeName: string; + importPath: string; + }; +}; + +export type SubTypeNamespace = { + [namespace: string]: SubTypeItem; +}; + export interface GenerateTSOptions { outputPath: string; documentPath: string; @@ -19,10 +31,7 @@ export interface GenerateTSOptions { legacyNoModelsForObjects?: boolean; modelScope?: string | null; generateOnlyEnums?: boolean; - enumNamesToMigrate?: string[]; - enumNamesToKeep?: string[]; - contextSubTypeNameTemplate?: string; - contextSubTypePathTemplate?: string; + contextSubTypeMetadata?: SubTypeNamespace; defaultContextSubTypePath?: string; defaultContextSubTypeName?: string; /** @@ -77,10 +86,7 @@ export function generateTS( legacyNoModelsForObjects, modelScope, generateOnlyEnums, - enumNamesToMigrate, - enumNamesToKeep, - contextSubTypeNameTemplate, - contextSubTypePathTemplate, + contextSubTypeMetadata, defaultContextSubTypePath, defaultContextSubTypeName, generateResolverMap = false, @@ -88,7 +94,7 @@ export function generateTS( }: GenerateTSOptions, ): { files: ts.SourceFile[]; - contextMappingOutput: ContextMap | null; + contextMappingOutput: any | null; } { try { const context = extractContext( @@ -102,10 +108,7 @@ export function generateTS( enumsImport, legacyNoModelsForObjects, modelScope, - enumNamesToMigrate, - enumNamesToKeep, - contextSubTypeNameTemplate, - contextSubTypePathTemplate, + contextSubTypeMetadata, defaultContextSubTypePath, defaultContextSubTypeName, }, @@ -137,9 +140,10 @@ export function generateTS( result.push(generateLegacyResolvers(context)); } } + return { files: result, - contextMappingOutput: context.getContextMap(), + contextMappingOutput: context.getMetadataObject(), }; } catch (e) { console.error(e); diff --git a/packages/ts-codegen/src/context/index.ts b/packages/ts-codegen/src/context/index.ts index 4825b869b..00c8b0764 100644 --- a/packages/ts-codegen/src/context/index.ts +++ b/packages/ts-codegen/src/context/index.ts @@ -23,15 +23,18 @@ import ts, { SyntaxKind, } from "typescript"; import { DefinitionImport, DefinitionModel } from "../types"; -import { createImportDeclaration } from "./utilities"; import { - camelCase, + createImportDeclaration, + buildContextMetadataOutput, +} from "./utilities"; +import { createListType, createNonNullableType, createNullableType, } from "../utilities"; import { IMPORT_DIRECTIVE_NAME, processImportDirective } from "./import"; import { MODEL_DIRECTIVE_NAME, processModelDirective } from "./model"; +import { SubTypeNamespace } from "../codegen"; export type TsCodegenContextOptions = { moduleRoot: string; @@ -52,13 +55,10 @@ export type TsCodegenContextOptions = { }; legacyCompat: boolean; legacyNoModelsForObjects: boolean; - contextSubTypePathTemplate?: string; - contextSubTypeNameTemplate?: string; defaultContextSubTypePath?: string; defaultContextSubTypeName?: string; useStringUnionsInsteadOfEnums: boolean; - enumNamesToMigrate: string[] | null; - enumNamesToKeep: string[] | null; + contextSubTypeMetadata?: SubTypeNamespace; modelScope: string | null; }; @@ -90,8 +90,6 @@ const TsCodegenContextDefault: TsCodegenContextOptions = { from: "@graphitation/supermassive", }, legacyCompat: false, - enumNamesToMigrate: null, - enumNamesToKeep: null, legacyNoModelsForObjects: false, useStringUnionsInsteadOfEnums: false, @@ -109,6 +107,7 @@ export type ContextMapTypeItem = { __context?: string[] } & { }; export class TsCodegenContext { private allTypes: Array; + private resolverTypeMap: any; private typeContextMap: ContextMap; private typeNameToType: Map; private usedEntitiesInModels: Set; @@ -122,10 +121,6 @@ export class TsCodegenContext { private typeNameToModels: Map; private legacyInterfaces: Set; context?: { name: string; from: string }; - contextDefaultSubTypeTemplate?: { - nameTemplate: string; - pathTemplate: string; - }; contextDefaultSubTypeContext?: { name: string; from: string }; hasUsedModelInInputs: boolean; hasUsedEnumsInModels: boolean; @@ -136,6 +131,7 @@ export class TsCodegenContext { constructor(private options: TsCodegenContextOptions) { this.allTypes = []; this.typeContextMap = {}; + this.resolverTypeMap = {}; this.typeNameToType = new Map(); this.usedEntitiesInModels = new Set(); this.usedEntitiesInResolvers = new Set(); @@ -151,16 +147,6 @@ export class TsCodegenContext { this.hasEnums = Boolean(options.enumsImport); this.hasUsedEnumsInModels = false; - if ( - options.contextSubTypeNameTemplate && - options.contextSubTypePathTemplate - ) { - this.contextDefaultSubTypeTemplate = { - nameTemplate: options.contextSubTypeNameTemplate, - pathTemplate: options.contextSubTypePathTemplate, - }; - } - if ( options.defaultContextSubTypeName && options.defaultContextSubTypePath @@ -192,56 +178,75 @@ export class TsCodegenContext { return null; } - public replaceTemplateWithContextName( - template: string, - contextName: string, - camelCased = true, - ) { - return template.replace( - "${resourceName}", - camelCased ? camelCase(contextName, { pascalCase: true }) : contextName, - ); + public getSubTypesMetadata() { + return this.options.contextSubTypeMetadata; } - public getContextTemplate() { - return this.contextDefaultSubTypeTemplate || null; + private buildContextSubTypeNamespaceObject(typeNames: string[]) { + const subTypesMetadata = this.getSubTypesMetadata(); + + return typeNames.reduce< + Record + >((acc, typeName) => { + const [namespace, subTypeName] = typeName.split(":"); + if (!acc[namespace]) { + acc[namespace] = []; + } + + if (!subTypesMetadata?.[namespace]?.[subTypeName]) { + throw new Error("something went really wrong"); + } + acc[namespace].push({ + subType: subTypesMetadata[namespace][subTypeName].importTypeName, + name: subTypeName, + }); + + return acc; + }, {}); } public getContextTypeNode(typeNames?: string[] | null) { - const contextDefaultSubTypeTemplate = this.contextDefaultSubTypeTemplate; + const subTypesMetadata = this.getSubTypesMetadata(); - if (!typeNames || !typeNames.length || !contextDefaultSubTypeTemplate) { + if (!typeNames || !typeNames.length || !subTypesMetadata) { return this.getContextType().toTypeReference(); - } else if ( - (typeNames.length === 1 && this.contextDefaultSubTypeContext) || - typeNames.length > 1 - ) { - const typeNameWithNamespace = typeNames.map((typeName) => { - return this.replaceTemplateWithContextName( - contextDefaultSubTypeTemplate.nameTemplate, - typeName, - ); - }); + } else { + const typeNameWithNamespace = + this.buildContextSubTypeNamespaceObject(typeNames); return factory.createIntersectionTypeNode( - (this.contextDefaultSubTypeContext - ? [this.contextDefaultSubTypeContext.name, ...typeNameWithNamespace] - : typeNameWithNamespace - ).map((type: string) => { - return factory.createTypeReferenceNode( - factory.createIdentifier(type), - undefined, - ); - }), + [ + this.contextDefaultSubTypeContext?.name && + factory.createTypeReferenceNode( + factory.createIdentifier(this.contextDefaultSubTypeContext.name), + undefined, + ), + factory.createTypeLiteralNode( + Object.entries(typeNameWithNamespace).map( + ([namespace, subTypes]) => { + return factory.createPropertySignature( + undefined, + factory.createIdentifier(namespace), + undefined, + factory.createTypeLiteralNode( + subTypes.map(({ subType, name }) => { + return factory.createPropertySignature( + undefined, + factory.createIdentifier(`"${name}"`), + undefined, + factory.createTypeReferenceNode( + factory.createIdentifier(subType), + undefined, + ), + ); + }), + ), + ); + }, + ), + ), + ].filter(Boolean) as ts.TypeNode[], ); - } else { - return new TypeLocation( - null, - this.replaceTemplateWithContextName( - contextDefaultSubTypeTemplate.nameTemplate, - typeNames[0], - ), - ).toTypeReference(); } } @@ -298,25 +303,58 @@ export class TsCodegenContext { } } - getSubTypeNamesFromTemplate( - subTypes: string[], - nameTemplate: string, - pathTemplate: string, - ) { + getMetadataObject() { + if (!this.typeContextMap || !this.resolverTypeMap) { + return null; + } + + return buildContextMetadataOutput( + this.typeContextMap, + this.resolverTypeMap, + ); + } + + setResolverTypeMapItem(typeName: string, fieldName: string | null) { + if (fieldName === null) { + this.resolverTypeMap[typeName] = null; + return; + } + + if (!this.resolverTypeMap[typeName]) { + this.resolverTypeMap[typeName] = []; + } + + this.resolverTypeMap[typeName].push(fieldName); + } + + getResolverTypeMap() { + return this.resolverTypeMap; + } + + cleanSubtypeImportName(subTypeImportIdentifier: string) { + return subTypeImportIdentifier.split(/\.|\[/)[0]; + } + + getSubTypeNamesImportMap(subTypes: string[]) { + const subTypeMetadata = this.getSubTypesMetadata(); return subTypes.reduce>( (acc: Record, importName: string) => { - const importPath = this.replaceTemplateWithContextName( - pathTemplate, - importName, - false, - ); + const [namespace, subTypeName] = importName.split(":"); + const subType = subTypeMetadata?.[namespace]?.[subTypeName]; + + if (!subType) { + throw new Error( + `Critical Error: Subtype ${importName} not found in metadata`, + ); + } + + const { importPath, importTypeName } = subType; + if (importPath) { if (!acc[importPath]) { acc[importPath] = []; } - acc[importPath].push( - this.replaceTemplateWithContextName(nameTemplate, importName), - ); + acc[importPath].push(this.cleanSubtypeImportName(importTypeName)); } return acc; }, @@ -349,22 +387,6 @@ export class TsCodegenContext { return this.allTypes; } - shouldMigrateEnum(enumName: string) { - if (!this.options.enumNamesToKeep && !this.options.enumNamesToMigrate) { - return true; - } - - if (this.options.enumNamesToKeep) { - return !this.options.enumNamesToKeep.includes(enumName); - } - - if (this.options.enumNamesToMigrate) { - return this.options.enumNamesToMigrate.includes(enumName); - } - - return true; - } - getTypeReferenceFromTypeNode( node: TypeNode, markUsage?: "MODELS" | "RESOLVERS", @@ -734,7 +756,6 @@ export function extractContext( }; const context = new TsCodegenContext(fullOptions); - const { contextSubTypeNameTemplate, contextSubTypePathTemplate } = options; visit(document, { Directive: { @@ -767,26 +788,51 @@ export function extractContext( context.addLegacyInterface(typeName); } else if ( node.name.value === "context" && - contextSubTypeNameTemplate && - contextSubTypePathTemplate + options.contextSubTypeMetadata ) { + const subTypeKeys: Set = new Set(); if ( node.arguments?.length !== 1 || node.arguments[0].name.value !== "uses" || - node.arguments[0].value.kind !== "ListValue" + node.arguments[0].value.kind !== "ObjectValue" ) { throw new Error("Invalid context use"); } - const directiveValues = node.arguments[0].value.values.map((item) => { - if (item.kind !== "StringValue") { + + node.arguments[0].value.fields.forEach(({ name, value, kind }) => { + if (kind !== "ObjectField") { throw new Error("Invalid context use"); } - return item.value; + const namespace = name.value; + if (value.kind !== "ListValue") { + throw new Error(`Namespace "${name}" must be list of strings`); + } + + const namespaceValues: string[] = value.values.map((v) => { + if (v.kind !== "StringValue") { + throw new Error(`Namespace "${name}" must be list of strings`); + } + return v.value; + }); + + if (!options.contextSubTypeMetadata?.[namespace]) { + throw new Error(`Namespace "${name}" is not supported`); + } + + namespaceValues.forEach((namespaceValue) => { + if ( + !options.contextSubTypeMetadata?.[namespace]?.[namespaceValue] + ) { + throw new Error( + `Value "${namespaceValue}" in namespace "${namespace}" is not supported`, + ); + } + + subTypeKeys.add(`${namespace}:${namespaceValue}`); + }); }); - if (directiveValues.length) { - context.initContextMap(ancestors, directiveValues); - } + context.initContextMap(ancestors, Array.from(subTypeKeys)); } }, }, diff --git a/packages/ts-codegen/src/context/utilities.ts b/packages/ts-codegen/src/context/utilities.ts index 4a9805bb9..028287bc1 100644 --- a/packages/ts-codegen/src/context/utilities.ts +++ b/packages/ts-codegen/src/context/utilities.ts @@ -1,36 +1,6 @@ -import ts, { factory } from "typescript"; +import { factory } from "typescript"; import path from "path"; -export function getImportIdentifierForTypenames( - importNames: string[], - importPath: string, - contextImportNames: Set, -) { - return factory.createImportDeclaration( - undefined, - factory.createImportClause( - true, - undefined, - factory.createNamedImports( - importNames - .map((importName: string) => { - if (contextImportNames.has(importName)) { - return; - } - contextImportNames.add(importName); - return factory.createImportSpecifier( - false, - undefined, - factory.createIdentifier(importName), - ); - }) - .filter(Boolean) as ts.ImportSpecifier[], - ), - ), - factory.createStringLiteral(importPath), - ); -} - export function createImportDeclaration( importNames: string[], from: string, @@ -83,3 +53,101 @@ export function getRelativePath( return cleanRelativePath(path.relative(outputPath, modelFullPath)); } + +type MetadataItem = { [namespace: string]: string[] }; + +type RootResolersMetadata = { + [resolver: string]: MetadataItem; +}; + +type TypeMetadata = { + [typeName: string]: { [field: string]: MetadataItem }; +}; + +type OutputMetadata = RootResolersMetadata | TypeMetadata; + +export function buildContextMetadataOutput( + contextMap: any, + resolverTypeMap: any, +) { + const metadata: OutputMetadata = {}; + + for (const [key, values] of Object.entries( + resolverTypeMap as Record, + )) { + if (!contextMap[key]) { + continue; + } + + if (values === null) { + if (contextMap[key]?.__context) { + for (const contextValue of contextMap[key].__context) { + const [namespace, subTypeName] = contextValue.split(":"); + + if (!metadata[key]) { + metadata[key] = {}; + } + + if (!metadata[key][namespace]) { + metadata[key][namespace] = []; + } + + if (!Array.isArray(metadata[key][namespace])) { + throw Error("Invalid context metadata"); + } + + if (!metadata[key][namespace].includes(subTypeName)) { + metadata[key][namespace].push(subTypeName); + } + } + continue; + } + continue; + } + + for (const value of values) { + if (contextMap[key][value]) { + for (const typeValue of contextMap[key][value]) { + buildContextMetadataOutputItem(metadata, typeValue, key, value); + } + continue; + } else if (contextMap[key].__context) { + for (const contextValue of contextMap[key].__context) { + buildContextMetadataOutputItem(metadata, contextValue, key, value); + } + continue; + } + } + } + + return metadata; +} + +function buildContextMetadataOutputItem( + metadata: OutputMetadata, + contextKey: string, + key: string, + value: string, +) { + const [namespace, subTypeName] = contextKey.split(":"); + + if (!metadata[key]) { + metadata[key] = {}; + } + + if (!metadata[key][value]) { + metadata[key][value] = {}; + } + + if (Array.isArray(metadata[key][value])) { + throw Error("Invalid context metadata"); + } + + if (!metadata[key][value][namespace]) { + metadata[key][value][namespace] = []; + } + + if (!metadata[key][value][namespace].includes(subTypeName)) { + metadata[key][value][namespace].push(subTypeName); + } +} diff --git a/packages/ts-codegen/src/enums.ts b/packages/ts-codegen/src/enums.ts index 46ee9d7e5..38fb7552f 100644 --- a/packages/ts-codegen/src/enums.ts +++ b/packages/ts-codegen/src/enums.ts @@ -11,8 +11,7 @@ export function generateEnums(context: TsCodegenContext): ts.SourceFile { .map((type) => { if (type.kind === "ENUM") { const isStringUnion = - context.isUseStringUnionsInsteadOfEnumsEnabled() && - context.shouldMigrateEnum(type.name); + context.isUseStringUnionsInsteadOfEnumsEnabled(); return factory.createExportDeclaration( undefined, @@ -54,10 +53,7 @@ function createEnumTypeModel( context: TsCodegenContext, type: EnumType, ): ts.EnumDeclaration | ts.TypeAliasDeclaration { - if ( - context.isUseStringUnionsInsteadOfEnumsEnabled() && - context.shouldMigrateEnum(type.name) - ) { + if (context.isUseStringUnionsInsteadOfEnumsEnabled()) { return factory.createTypeAliasDeclaration( [factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier(type.name), diff --git a/packages/ts-codegen/src/resolvers.ts b/packages/ts-codegen/src/resolvers.ts index 9f904d4e7..5c1b58f97 100644 --- a/packages/ts-codegen/src/resolvers.ts +++ b/packages/ts-codegen/src/resolvers.ts @@ -13,10 +13,48 @@ import { createUnionResolveType, createInterfaceResolveType, } from "./utilities"; -import { - createImportDeclaration, - getImportIdentifierForTypenames, -} from "./context/utilities"; +import { createImportDeclaration } from "./context/utilities"; + +export function getImportIdentifierForTypenames( + imports: Record, + contextImportNames: Set, +) { + const statements = []; + + for (const [importPath, importNames] of Object.entries(imports)) { + const importIndentifiers = importNames + .map((importName: string) => { + if (contextImportNames.has(importName)) { + return; + } + contextImportNames.add(importName); + return factory.createImportSpecifier( + false, + undefined, + factory.createIdentifier(importName), + ); + }) + .filter(Boolean) as ts.ImportSpecifier[]; + + if (!importIndentifiers.length) { + continue; + } + + statements.push( + factory.createImportDeclaration( + undefined, + factory.createImportClause( + true, + undefined, + factory.createNamedImports(importIndentifiers), + ), + factory.createStringLiteral(importPath), + ), + ); + } + + return statements; +} const getResolverTypes = (context: TsCodegenContext): ResolverType[] => { return context @@ -53,9 +91,8 @@ export function generateResolvers( ), ); - const contextTemplate = context.getContextTemplate(); - - if (Object.keys(context.getContextMap()).length && contextTemplate) { + const getSubTypesMetadata = context.getSubTypesMetadata(); + if (Object.keys(context.getContextMap()).length && getSubTypesMetadata) { if ( context.contextDefaultSubTypeContext?.from && context.contextDefaultSubTypeContext?.name @@ -79,21 +116,11 @@ export function generateResolvers( ) { continue; } - const imports = context.getSubTypeNamesFromTemplate( - rootValue, - contextTemplate.nameTemplate, - contextTemplate.pathTemplate, - ); - for (const [importPath, importNames] of Object.entries(imports)) { - statements.push( - getImportIdentifierForTypenames( - importNames, - importPath, - contextImportNames, - ), - ); - } + const imports = context.getSubTypeNamesImportMap(rootValue); + statements.push( + ...getImportIdentifierForTypenames(imports, contextImportNames), + ); } for (const [key, value] of Object.entries(root)) { if (key.startsWith("__")) { @@ -107,21 +134,11 @@ export function generateResolvers( continue; } - const imports = context.getSubTypeNamesFromTemplate( - value, - contextTemplate.nameTemplate, - contextTemplate.pathTemplate, - ); + const imports = context.getSubTypeNamesImportMap(value); - for (const [importPath, importNames] of Object.entries(imports)) { - statements.push( - getImportIdentifierForTypenames( - importNames, - importPath, - contextImportNames, - ), - ); - } + statements.push( + ...getImportIdentifierForTypenames(imports, contextImportNames), + ); } } } @@ -266,6 +283,8 @@ function createResolverField( } } + context.setResolverTypeMapItem(type.name, field.name); + const resolverParametersDefinitions = { parent: { name: "model", @@ -346,6 +365,8 @@ function createUnionTypeResolvers( const contextTypes = context.getContextTypes(contextRootType); + context.setResolverTypeMapItem(type.name, null); + return factory.createModuleDeclaration( [ factory.createModifier(ts.SyntaxKind.ExportKeyword), @@ -376,6 +397,8 @@ function createInterfaceTypeResolvers( const contextRootType = context.getContextMap()[type.name]; const contextTypes = context.getContextTypes(contextRootType); + context.setResolverTypeMapItem(type.name, null); + const resolversObject = factory.createInterfaceDeclaration( [factory.createModifier(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier("Resolvers"),