diff --git a/package-lock.json b/package-lock.json index 49a00bf1f55..b45c298c99d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18522,6 +18522,7 @@ }, "peerDependencies": { "@aws-amplify/client-config": "^0.2.0-alpha.5", + "@aws-amplify/model-generator": "0.2.0-alpha.1", "@aws-amplify/sandbox": "^0.1.1-alpha.5", "@aws-sdk/types": "^3.347.0" } @@ -18765,6 +18766,7 @@ "@aws-amplify/graphql-generator": "^0.1.0", "@aws-sdk/client-appsync": "^3.398.0", "@aws-sdk/client-s3": "^3.414.0", + "@aws-sdk/credential-providers": "^3.414.0", "@aws-sdk/types": "^3.413.0" } }, diff --git a/packages/model-generator/API.md b/packages/model-generator/API.md index 87d5dd9beb1..0aec92e4503 100644 --- a/packages/model-generator/API.md +++ b/packages/model-generator/API.md @@ -4,21 +4,67 @@ ```ts +import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; +import { BackendIdentifier } from '@aws-amplify/deployed-backend-client'; import { ModelsTarget } from '@aws-amplify/graphql-generator'; import { StatementsTarget } from '@aws-amplify/graphql-generator'; import { TypesTarget } from '@aws-amplify/graphql-generator'; // @public -export const createGraphqlDocumentGenerator: ({ apiId, }: GraphqlDocumentGeneratorFactoryParams) => GraphqlDocumentGenerator; +export const createGraphqlDocumentGenerator: ({ backendIdentifier, credentialProvider, }: GraphqlDocumentGeneratorFactoryParams) => GraphqlDocumentGenerator; // @public (undocumented) export type DocumentGenerationParameters = { language: TargetLanguage; }; +// @public +export const generateAPICode: (props: GenerateAPICodeProps) => Promise; + +// @public (undocumented) +export type GenerateAPICodeProps = GenerateOptions & BackendIdentifier; + +// @public (undocumented) +export type GeneratedOutput = { + [filename: string]: string; +}; + +// @public (undocumented) +export type GenerateGraphqlCodegenOptions = { + format: 'graphql-codegen'; + statementTarget: 'javascript' | 'graphql' | 'flow' | 'typescript' | 'angular'; + maxDepth?: number; + typenameIntrospection?: boolean; + typeTarget?: 'json' | 'swift' | 'typescript' | 'flow' | 'scala' | 'flow-modern' | 'angular'; + multipleSwiftFiles?: boolean; +}; + +// @public (undocumented) +export type GenerateIntrospectionOptions = { + format: 'introspection'; +}; + +// @public (undocumented) +export type GenerateModelsOptions = { + format: 'modelgen'; + modelTarget: 'java' | 'swift' | 'javascript' | 'typescript' | 'dart'; + generateIndexRules?: boolean; + emitAuthProvider?: boolean; + useExperimentalPipelinedTransformer?: boolean; + transformerVersion?: boolean; + respectPrimaryKeyAttributesOnConnectionField?: boolean; + generateModelsForLazyLoadAndCustomSelectionSet?: boolean; + addTimestampFields?: boolean; + handleListNullabilityTransparently?: boolean; +}; + +// @public (undocumented) +export type GenerateOptions = GenerateGraphqlCodegenOptions | GenerateModelsOptions | GenerateIntrospectionOptions; + // @public (undocumented) export type GenerationResult = { writeToDirectory: (directoryPath: string) => Promise; + operations: Record; }; // @public (undocumented) @@ -28,7 +74,8 @@ export type GraphqlDocumentGenerator = { // @public (undocumented) export type GraphqlDocumentGeneratorFactoryParams = { - apiId: string; + backendIdentifier: BackendIdentifier; + credentialProvider: AwsCredentialIdentityProvider; }; // @public (undocumented) diff --git a/packages/model-generator/package.json b/packages/model-generator/package.json index 45c1b0dd369..2db91491a49 100644 --- a/packages/model-generator/package.json +++ b/packages/model-generator/package.json @@ -22,6 +22,7 @@ "@aws-amplify/graphql-generator": "^0.1.0", "@aws-sdk/client-appsync": "^3.398.0", "@aws-sdk/client-s3": "^3.414.0", + "@aws-sdk/credential-providers": "^3.414.0", "@aws-sdk/types": "^3.413.0" } } diff --git a/packages/model-generator/src/appsync_graphql_generation_result.ts b/packages/model-generator/src/appsync_graphql_generation_result.ts index f9d6fe0f1d6..291969c7d02 100644 --- a/packages/model-generator/src/appsync_graphql_generation_result.ts +++ b/packages/model-generator/src/appsync_graphql_generation_result.ts @@ -12,7 +12,7 @@ export class AppsyncGraphqlGenerationResult implements GenerationResult { * @param operations A record of FileName to FileContent * in the format of Record */ - constructor(private operations: ClientOperations) {} + constructor(public operations: ClientOperations) {} private writeSchemaToFile = async ( basePath: string, filePath: string, diff --git a/packages/model-generator/src/create_graphql_document_generator.test.ts b/packages/model-generator/src/create_graphql_document_generator.test.ts index d58a013ad46..df1a87f4d69 100644 --- a/packages/model-generator/src/create_graphql_document_generator.test.ts +++ b/packages/model-generator/src/create_graphql_document_generator.test.ts @@ -1,11 +1,25 @@ import assert from 'node:assert'; import { describe, it } from 'node:test'; +import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; +import { BackendIdentifier } from '@aws-amplify/deployed-backend-client'; import { createGraphqlDocumentGenerator } from './create_graphql_document_generator.js'; describe('model generator factory', () => { - it('throws an error if a null apiId is passed in', async () => { + it('throws an error if a null backendIdentifier is passed in', async () => { assert.throws(() => - createGraphqlDocumentGenerator({ apiId: null as unknown as string }) + createGraphqlDocumentGenerator({ + backendIdentifier: null as unknown as BackendIdentifier, + credentialProvider: null as unknown as AwsCredentialIdentityProvider, + }) + ); + }); + + it('throws an error if a null backendIdentifier is passed in', async () => { + assert.throws(() => + createGraphqlDocumentGenerator({ + backendIdentifier: { stackName: 'foo' }, + credentialProvider: null as unknown as AwsCredentialIdentityProvider, + }) ); }); }); diff --git a/packages/model-generator/src/create_graphql_document_generator.ts b/packages/model-generator/src/create_graphql_document_generator.ts index 5b7d9817c3e..5604f01dd70 100644 --- a/packages/model-generator/src/create_graphql_document_generator.ts +++ b/packages/model-generator/src/create_graphql_document_generator.ts @@ -1,25 +1,51 @@ import { AppSyncClient } from '@aws-sdk/client-appsync'; +import { + BackendIdentifier, + BackendOutputClient, +} from '@aws-amplify/deployed-backend-client'; +import { graphqlOutputKey } from '@aws-amplify/backend-output-schemas'; +import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; import { AppsyncGraphqlGenerationResult } from './appsync_graphql_generation_result.js'; import { AppSyncIntrospectionSchemaFetcher } from './appsync_schema_fetcher.js'; import { AppSyncGraphqlDocumentGenerator } from './graphql_document_generator.js'; import { GraphqlDocumentGenerator } from './model_generator.js'; export type GraphqlDocumentGeneratorFactoryParams = { - apiId: string; + backendIdentifier: BackendIdentifier; + credentialProvider: AwsCredentialIdentityProvider; }; /** * Factory function to compose a model generator */ export const createGraphqlDocumentGenerator = ({ - apiId, + backendIdentifier, + credentialProvider, }: GraphqlDocumentGeneratorFactoryParams): GraphqlDocumentGenerator => { - if (!apiId) { - throw new Error('`apiId` must be defined'); + if (!backendIdentifier) { + throw new Error('`backendIdentifier` must be defined'); } + if (!credentialProvider) { + throw new Error('`credentialProvider` must be defined'); + } + + const fetchSchema = async () => { + const backendOutputClient = new BackendOutputClient( + credentialProvider, + backendIdentifier + ); + const output = await backendOutputClient.getOutput(); + const apiId = output[graphqlOutputKey]?.payload.awsAppsyncApiId; + if (!apiId) { + throw new Error(`Unable to determine AppSync API ID.`); + } + + return new AppSyncIntrospectionSchemaFetcher(new AppSyncClient()).fetch( + apiId + ); + }; return new AppSyncGraphqlDocumentGenerator( - () => - new AppSyncIntrospectionSchemaFetcher(new AppSyncClient()).fetch(apiId), + fetchSchema, (fileMap) => new AppsyncGraphqlGenerationResult(fileMap) ); }; diff --git a/packages/model-generator/src/create_graphql_types_generator.test.ts b/packages/model-generator/src/create_graphql_types_generator.test.ts index 871bfdab5e2..0ff022fd782 100644 --- a/packages/model-generator/src/create_graphql_types_generator.test.ts +++ b/packages/model-generator/src/create_graphql_types_generator.test.ts @@ -1,11 +1,25 @@ import assert from 'node:assert'; import { describe, it } from 'node:test'; +import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; +import { BackendIdentifier } from '@aws-amplify/deployed-backend-client'; import { createGraphqlTypesGenerator } from './create_graphql_types_generator.js'; describe('types generator factory', () => { - it('throws an error if a null apiId is passed in', async () => { + it('throws an error if a null backendIdentifier is passed in', async () => { assert.throws(() => - createGraphqlTypesGenerator({ apiId: null as unknown as string }) + createGraphqlTypesGenerator({ + backendIdentifier: null as unknown as BackendIdentifier, + credentialProvider: null as unknown as AwsCredentialIdentityProvider, + }) + ); + }); + + it('throws an error if a null backendIdentifier is passed in', async () => { + assert.throws(() => + createGraphqlTypesGenerator({ + backendIdentifier: { stackName: 'foo' }, + credentialProvider: null as unknown as AwsCredentialIdentityProvider, + }) ); }); }); diff --git a/packages/model-generator/src/create_graphql_types_generator.ts b/packages/model-generator/src/create_graphql_types_generator.ts index c62d44e90e2..e4f7d292fae 100644 --- a/packages/model-generator/src/create_graphql_types_generator.ts +++ b/packages/model-generator/src/create_graphql_types_generator.ts @@ -1,25 +1,51 @@ import { AppSyncClient } from '@aws-sdk/client-appsync'; +import { + BackendIdentifier, + BackendOutputClient, +} from '@aws-amplify/deployed-backend-client'; +import { graphqlOutputKey } from '@aws-amplify/backend-output-schemas'; +import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; import { AppsyncGraphqlGenerationResult } from './appsync_graphql_generation_result.js'; import { AppSyncIntrospectionSchemaFetcher } from './appsync_schema_fetcher.js'; import { AppSyncGraphqlTypesGenerator } from './graphql_types_generator.js'; import { GraphqlTypesGenerator } from './model_generator.js'; export type GraphqlTypesGeneratorFactoryParams = { - apiId: string; + backendIdentifier: BackendIdentifier; + credentialProvider: AwsCredentialIdentityProvider; }; /** * Factory function to compose a model generator */ export const createGraphqlTypesGenerator = ({ - apiId, + backendIdentifier, + credentialProvider, }: GraphqlTypesGeneratorFactoryParams): GraphqlTypesGenerator => { - if (!apiId) { - throw new Error('`apiId` must be defined'); + if (!backendIdentifier) { + throw new Error('`backendIdentifier` must be defined'); } + if (!credentialProvider) { + throw new Error('`credentialProvider` must be defined'); + } + + const fetchSchema = async () => { + const backendOutputClient = new BackendOutputClient( + credentialProvider, + backendIdentifier + ); + const output = await backendOutputClient.getOutput(); + const apiId = output[graphqlOutputKey]?.payload.awsAppsyncApiId; + if (!apiId) { + throw new Error(`Unable to determine AppSync API ID.`); + } + + return new AppSyncIntrospectionSchemaFetcher(new AppSyncClient()).fetch( + apiId + ); + }; return new AppSyncGraphqlTypesGenerator( - () => - new AppSyncIntrospectionSchemaFetcher(new AppSyncClient()).fetch(apiId), + fetchSchema, (fileMap) => new AppsyncGraphqlGenerationResult(fileMap) ); }; diff --git a/packages/model-generator/src/generate_api_code.test.ts b/packages/model-generator/src/generate_api_code.test.ts new file mode 100644 index 00000000000..df0084673a5 --- /dev/null +++ b/packages/model-generator/src/generate_api_code.test.ts @@ -0,0 +1,17 @@ +import assert from 'assert'; +import { describe, it, mock } from 'node:test'; +import { GenerateAPICodeProps, generateAPICode } from './generate_api_code.js'; + +describe('generateAPICode', () => { + describe('graphql-codegen', () => {}); + describe('modelgen', () => {}); + describe('introspection', () => {}); + + it('throws error on unknown format', async () => { + const props = { + format: 'unsupported', + stackName: 'stack_name', + } as unknown as GenerateAPICodeProps; + await assert.rejects(() => generateAPICode(props)); + }); +}); diff --git a/packages/model-generator/src/generate_api_code.ts b/packages/model-generator/src/generate_api_code.ts new file mode 100644 index 00000000000..b670f0710f3 --- /dev/null +++ b/packages/model-generator/src/generate_api_code.ts @@ -0,0 +1,124 @@ +import { BackendIdentifier } from '@aws-amplify/deployed-backend-client'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; +import path from 'path'; + +import { createGraphqlModelsGenerator } from './create_graphql_models_generator.js'; +import { createGraphqlTypesGenerator } from './create_graphql_types_generator.js'; +import { createGraphqlDocumentGenerator } from './create_graphql_document_generator.js'; + +export type GeneratedOutput = { [filename: string]: string }; + +export type GenerateModelsOptions = { + format: 'modelgen'; + modelTarget: 'java' | 'swift' | 'javascript' | 'typescript' | 'dart'; + generateIndexRules?: boolean; + emitAuthProvider?: boolean; + useExperimentalPipelinedTransformer?: boolean; + transformerVersion?: boolean; + respectPrimaryKeyAttributesOnConnectionField?: boolean; + generateModelsForLazyLoadAndCustomSelectionSet?: boolean; + addTimestampFields?: boolean; + handleListNullabilityTransparently?: boolean; +}; + +export type GenerateGraphqlCodegenOptions = { + format: 'graphql-codegen'; + statementTarget: 'javascript' | 'graphql' | 'flow' | 'typescript' | 'angular'; + maxDepth?: number; + typenameIntrospection?: boolean; + typeTarget?: + | 'json' + | 'swift' + | 'typescript' + | 'flow' + | 'scala' + | 'flow-modern' + | 'angular'; + multipleSwiftFiles?: boolean; +}; + +export type GenerateIntrospectionOptions = { + format: 'introspection'; +}; + +export type GenerateOptions = + | GenerateGraphqlCodegenOptions + | GenerateModelsOptions + | GenerateIntrospectionOptions; + +export type GenerateAPICodeProps = GenerateOptions & BackendIdentifier; + +/** + * Mock generateApiCode command. + */ +export const generateAPICode = async ( + props: GenerateAPICodeProps +): Promise => { + const credentialProvider = fromNodeProviderChain(); + + switch (props.format) { + case 'graphql-codegen': { + const documents = ( + await createGraphqlDocumentGenerator({ + backendIdentifier: props as BackendIdentifier, + credentialProvider, + }).generateModels({ + language: props.statementTarget, + maxDepth: props.maxDepth, + typenameIntrospection: props.typenameIntrospection, + }) + ).operations; + + if (props.typeTarget) { + const types = ( + await createGraphqlTypesGenerator({ + backendIdentifier: props as BackendIdentifier, + credentialProvider, + }).generateTypes({ + target: props.typeTarget, + multipleSwiftFiles: props.multipleSwiftFiles, + }) + ).operations; + return { ...documents, ...types }; + } + + return documents; + } + case 'modelgen': { + return ( + await createGraphqlModelsGenerator({ + backendIdentifier: props as BackendIdentifier, + credentialProvider, + }).generateModels({ + target: props.modelTarget, + generateIndexRules: props.generateIndexRules, + emitAuthProvider: props.emitAuthProvider, + useExperimentalPipelinedTransformer: + props.useExperimentalPipelinedTransformer, + transformerVersion: props.transformerVersion, + respectPrimaryKeyAttributesOnConnectionField: + props.respectPrimaryKeyAttributesOnConnectionField, + generateModelsForLazyLoadAndCustomSelectionSet: + props.generateModelsForLazyLoadAndCustomSelectionSet, + addTimestampFields: props.addTimestampFields, + handleListNullabilityTransparently: + props.handleListNullabilityTransparently, + }) + ).operations; + } + case 'introspection': { + return ( + await createGraphqlModelsGenerator({ + backendIdentifier: props as BackendIdentifier, + credentialProvider, + }).generateModels({ + target: 'introspection', + }) + ).operations; + } + default: + throw new Error( + `${(props as GenerateAPICodeProps).format} is not a supported format.` + ); + } +}; diff --git a/packages/model-generator/src/graphql_document_generator.test.ts b/packages/model-generator/src/graphql_document_generator.test.ts index 7f74351f371..1a78740bc7f 100644 --- a/packages/model-generator/src/graphql_document_generator.test.ts +++ b/packages/model-generator/src/graphql_document_generator.test.ts @@ -6,7 +6,7 @@ describe('client generator', () => { it('if `fetchSchema` returns null, it should throw an error', async () => { const generator = new AppSyncGraphqlDocumentGenerator( async () => null as unknown as string, - () => ({ writeToDirectory: () => Promise.resolve() }) + () => ({ writeToDirectory: () => Promise.resolve(), operations: {} }) ); await assert.rejects(() => generator.generateModels({ language: 'typescript' }) diff --git a/packages/model-generator/src/graphql_document_generator.ts b/packages/model-generator/src/graphql_document_generator.ts index badd05e8015..098b207313f 100644 --- a/packages/model-generator/src/graphql_document_generator.ts +++ b/packages/model-generator/src/graphql_document_generator.ts @@ -18,7 +18,11 @@ export class AppSyncGraphqlDocumentGenerator private fetchSchema: () => Promise, private resultBuilder: (fileMap: Record) => GenerationResult ) {} - generateModels = async ({ language }: DocumentGenerationParameters) => { + generateModels = async ({ + language, + maxDepth, + typenameIntrospection, + }: DocumentGenerationParameters) => { const schema = await this.fetchSchema(); if (!schema) { @@ -28,8 +32,8 @@ export class AppSyncGraphqlDocumentGenerator const generatedStatements = generateStatements({ schema, target: language, - maxDepth: 3, - typenameIntrospection: true, + maxDepth, + typenameIntrospection, }); return this.resultBuilder(generatedStatements); diff --git a/packages/model-generator/src/graphql_models_generator.test.ts b/packages/model-generator/src/graphql_models_generator.test.ts index cfd9fcc7ed1..d1c26e754cb 100644 --- a/packages/model-generator/src/graphql_models_generator.test.ts +++ b/packages/model-generator/src/graphql_models_generator.test.ts @@ -6,7 +6,7 @@ describe('models generator', () => { it('if `fetchSchema` returns null, it should throw an error', async () => { const generator = new StackMetadataGraphqlModelsGenerator( async () => null as unknown as string, - () => ({ writeToDirectory: () => Promise.resolve() }) + () => ({ writeToDirectory: () => Promise.resolve(), operations: {} }) ); await assert.rejects(() => generator.generateModels({ target: 'typescript' }) diff --git a/packages/model-generator/src/graphql_models_generator.ts b/packages/model-generator/src/graphql_models_generator.ts index 0cf8f54ff95..807e505def6 100644 --- a/packages/model-generator/src/graphql_models_generator.ts +++ b/packages/model-generator/src/graphql_models_generator.ts @@ -20,7 +20,17 @@ export class StackMetadataGraphqlModelsGenerator private resultBuilder: (fileMap: Record) => GenerationResult ) {} - generateModels = async ({ target }: ModelsGenerationParameters) => { + generateModels = async ({ + target, + generateIndexRules, + emitAuthProvider, + useExperimentalPipelinedTransformer, + transformerVersion, + respectPrimaryKeyAttributesOnConnectionField, + generateModelsForLazyLoadAndCustomSelectionSet, + addTimestampFields, + handleListNullabilityTransparently, + }: ModelsGenerationParameters) => { const schema = await this.fetchSchema(); if (!schema) { @@ -31,6 +41,15 @@ export class StackMetadataGraphqlModelsGenerator schema, target, directives: defaultDirectiveDefinitions, + generateIndexRules, + emitAuthProvider, + // typo in @aws-amplify/graphql-generator. Will be fixed in next release + useExperimentalPipelinedTranformer: useExperimentalPipelinedTransformer, + transformerVersion, + respectPrimaryKeyAttributesOnConnectionField, + generateModelsForLazyLoadAndCustomSelectionSet, + addTimestampFields, + handleListNullabilityTransparently, }); return this.resultBuilder(generatedModels); diff --git a/packages/model-generator/src/graphql_types_generator.test.ts b/packages/model-generator/src/graphql_types_generator.test.ts index 8153f794733..c6f13a799b4 100644 --- a/packages/model-generator/src/graphql_types_generator.test.ts +++ b/packages/model-generator/src/graphql_types_generator.test.ts @@ -6,7 +6,7 @@ describe('types generator', () => { it('if `fetchSchema` returns null, it should throw an error', async () => { const generator = new AppSyncGraphqlTypesGenerator( async () => null as unknown as string, - () => ({ writeToDirectory: () => Promise.resolve() }) + () => ({ writeToDirectory: () => Promise.resolve(), operations: {} }) ); await assert.rejects(() => generator.generateTypes({ target: 'typescript' }) diff --git a/packages/model-generator/src/graphql_types_generator.ts b/packages/model-generator/src/graphql_types_generator.ts index 1789896876a..6905d13d0bb 100644 --- a/packages/model-generator/src/graphql_types_generator.ts +++ b/packages/model-generator/src/graphql_types_generator.ts @@ -20,7 +20,10 @@ export class AppSyncGraphqlTypesGenerator implements GraphqlTypesGenerator { private resultBuilder: (fileMap: Record) => GenerationResult ) {} - generateTypes = async ({ target }: TypesGenerationParameters) => { + generateTypes = async ({ + target, + multipleSwiftFiles, + }: TypesGenerationParameters) => { const schema = await this.fetchSchema(); if (!schema) { @@ -38,6 +41,7 @@ export class AppSyncGraphqlTypesGenerator implements GraphqlTypesGenerator { schema, target, queries, + multipleSwiftFiles, }); return this.resultBuilder(generatedTypes); diff --git a/packages/model-generator/src/index.ts b/packages/model-generator/src/index.ts index 5497ba2ffb9..332bb3c475c 100644 --- a/packages/model-generator/src/index.ts +++ b/packages/model-generator/src/index.ts @@ -1,2 +1,3 @@ export * from './model_generator.js'; export * from './create_graphql_document_generator.js'; +export * from './generate_api_code.js'; diff --git a/packages/model-generator/src/model_generator.ts b/packages/model-generator/src/model_generator.ts index d3ed9fc54e0..d5dfa84bc13 100644 --- a/packages/model-generator/src/model_generator.ts +++ b/packages/model-generator/src/model_generator.ts @@ -7,9 +7,12 @@ export type TargetLanguage = StatementsTarget; export type DocumentGenerationParameters = { language: TargetLanguage; + maxDepth?: number; + typenameIntrospection?: boolean; }; export type GenerationResult = { writeToDirectory: (directoryPath: string) => Promise; + operations: Record; }; export type GraphqlDocumentGenerator = { generateModels: ( @@ -19,6 +22,7 @@ export type GraphqlDocumentGenerator = { export type TypesGenerationParameters = { target: TypesTarget; + multipleSwiftFiles?: boolean; }; export type GraphqlTypesGenerator = { generateTypes: ( @@ -28,6 +32,14 @@ export type GraphqlTypesGenerator = { export type ModelsGenerationParameters = { target: ModelsTarget; + generateIndexRules?: boolean; + emitAuthProvider?: boolean; + useExperimentalPipelinedTransformer?: boolean; + transformerVersion?: boolean; + respectPrimaryKeyAttributesOnConnectionField?: boolean; + generateModelsForLazyLoadAndCustomSelectionSet?: boolean; + addTimestampFields?: boolean; + handleListNullabilityTransparently?: boolean; }; export type GraphqlModelsGenerator = { generateModels: (