From 5a089b7a359016b35e938455cd9ead26b1288645 Mon Sep 17 00:00:00 2001 From: MorFix Date: Thu, 6 Feb 2025 17:35:35 +0200 Subject: [PATCH] feat: Add support for JS and TS interfaces/unions --- .../languages/typescript-declaration-block.ts | 16 ++- .../appsync-modelgen-plugin/src/preset.ts | 1 + .../visitors/appsync-json-metadata-visitor.ts | 97 ++++++++++++++----- .../visitors/appsync-typescript-visitor.ts | 46 +++++++-- .../src/visitors/appsync-visitor.ts | 39 ++++++++ 5 files changed, 166 insertions(+), 33 deletions(-) diff --git a/packages/appsync-modelgen-plugin/src/languages/typescript-declaration-block.ts b/packages/appsync-modelgen-plugin/src/languages/typescript-declaration-block.ts index be421c836..09573bc92 100644 --- a/packages/appsync-modelgen-plugin/src/languages/typescript-declaration-block.ts +++ b/packages/appsync-modelgen-plugin/src/languages/typescript-declaration-block.ts @@ -255,7 +255,21 @@ export class TypeScriptDeclarationBlock { } protected generateInterface(): string { - throw new Error('Not implemented yet'); + const header = [ + this._flags.shouldExport ? 'export' : '', + this._flags.isDeclaration ? 'declare' : '', + 'interface', + this._name, + '{', + ]; + + if (this._extends.length) { + header.push(['extends', this._extends.join(', ')].join(' ')); + } + + const body = [this.generateProperties()]; + + return [`${header.filter(h => h).join(' ')}`, indentMultiline(body.join('\n')), '}'].join('\n'); } protected generateType(): string { diff --git a/packages/appsync-modelgen-plugin/src/preset.ts b/packages/appsync-modelgen-plugin/src/preset.ts index 549ee1da8..afcf67bdc 100644 --- a/packages/appsync-modelgen-plugin/src/preset.ts +++ b/packages/appsync-modelgen-plugin/src/preset.ts @@ -216,6 +216,7 @@ const generateJavasScriptPreset = ( filename: join(modelFolder, 'index.d.ts'), config: { ...options.config, + target: 'typescript', scalars: { ...TYPESCRIPT_SCALAR_MAP, ...options.config.scalars }, metadata: false, isDeclaration: true, diff --git a/packages/appsync-modelgen-plugin/src/visitors/appsync-json-metadata-visitor.ts b/packages/appsync-modelgen-plugin/src/visitors/appsync-json-metadata-visitor.ts index 55704f9d9..b49c1dfca 100644 --- a/packages/appsync-modelgen-plugin/src/visitors/appsync-json-metadata-visitor.ts +++ b/packages/appsync-modelgen-plugin/src/visitors/appsync-json-metadata-visitor.ts @@ -8,21 +8,35 @@ import { ParsedAppSyncModelConfig, RawAppSyncModelConfig, CodeGenEnum, + CodeGenUnion, + CodeGenInterface, } from './appsync-visitor'; import { METADATA_SCALAR_MAP } from '../scalars'; export type JSONSchema = { models: JSONSchemaModels; enums: JSONSchemaEnums; nonModels: JSONSchemaTypes; + interfaces: JSONSchemaInterfaces; + unions: JSONSchemaUnions; version: string; codegenVersion: string; }; export type JSONSchemaModels = Record; export type JSONSchemaTypes = Record; +export type JSONSchemaInterfaces = Record; +export type JSONSchemaUnions = Record; export type JSONSchemaNonModel = { name: string; fields: JSONModelFields; }; +export type JSONSchemaInterface = { + name: string; + fields: JSONModelFields; +}; +export type JSONSchemaUnion = { + name: string; + types: JSONModelFieldType[]; +}; type JSONSchemaModel = { name: string; attributes?: JSONModelAttributes; @@ -58,7 +72,7 @@ type AssociationBelongsTo = AssociationBaseType & { type AssociationType = AssociationHasMany | AssociationHasOne | AssociationBelongsTo; -type JSONModelFieldType = keyof typeof METADATA_SCALAR_MAP | { model: string } | { enum: string } | { nonModel: string }; +type JSONModelFieldType = keyof typeof METADATA_SCALAR_MAP | { model: string } | { enum: string } | { nonModel: string } | { interface: string } | { union: string }; type JSONModelField = { name: string; type: JSONModelFieldType; @@ -160,6 +174,8 @@ export class AppSyncJSONVisitor< models: {}, enums: {}, nonModels: {}, + interfaces: {}, + unions: {}, // This is hard-coded for the schema version purpose instead of codegen version // To avoid the failure of validation method checkCodegenSchema in JS Datastore // The hard code is starting from amplify codegen major version 4 @@ -175,11 +191,19 @@ export class AppSyncJSONVisitor< return { ...acc, [nonModel.name]: this.generateNonModelMetadata(nonModel) }; }, {}); + const interfaces = Object.values(this.getSelectedInterfaces()).reduce((acc, codegenInterface: CodeGenInterface) => { + return { ...acc, [codegenInterface.name]: this.generateInterfaceMetadata(codegenInterface) }; + }, {}); + + const unions = Object.values(this.getSelectedUnions()).reduce((acc, union: CodeGenUnion) => { + return { ...acc, [union.name]: this.generateUnionMetadata(union) }; + }, {}); + const enums = Object.values(this.enumMap).reduce((acc, enumObj) => { const enumV = this.generateEnumMetadata(enumObj); return { ...acc, [this.getEnumName(enumObj)]: enumV }; }, {}); - return { ...result, models, nonModels: nonModels, enums }; + return { ...result, models, nonModels: nonModels, enums, interfaces, unions }; } private getFieldAssociation(field: CodeGenField): AssociationType | void { @@ -229,32 +253,24 @@ export class AppSyncJSONVisitor< private generateNonModelMetadata(nonModel: CodeGenModel): JSONSchemaNonModel { return { name: this.getModelName(nonModel), - fields: nonModel.fields.reduce((acc: JSONModelFields, field: CodeGenField) => { - const fieldMeta: JSONModelField = { - name: this.getFieldName(field), - isArray: field.isList, - type: this.getType(field.type), - isRequired: !field.isNullable, - attributes: [], - }; - - if (field.isListNullable !== undefined) { - fieldMeta.isArrayNullable = field.isListNullable; - } + fields: this.generateFieldsMetadata(nonModel.fields) + }; + } - if (field.isReadOnly !== undefined) { - fieldMeta.isReadOnly = field.isReadOnly; - } + private generateInterfaceMetadata(codeGenInterface: CodeGenInterface): JSONSchemaInterface { + return { + name: codeGenInterface.name, + fields: this.generateFieldsMetadata(codeGenInterface.fields), + }; + } - const association: AssociationType | void = this.getFieldAssociation(field); - if (association) { - fieldMeta.association = association; - } - acc[fieldMeta.name] = fieldMeta; - return acc; - }, {}), + private generateUnionMetadata(codeGenUnion: CodeGenUnion): JSONSchemaUnion { + return { + name: codeGenUnion.name, + types: codeGenUnion.typeNames.map(t => this.getType(t)) }; } + private generateEnumMetadata(enumObj: CodeGenEnum): JSONSchemaEnum { return { name: enumObj.name, @@ -262,6 +278,33 @@ export class AppSyncJSONVisitor< }; } + private generateFieldsMetadata(fields: CodeGenField[]): JSONModelFields { + return fields.reduce((acc: JSONModelFields, field: CodeGenField) => { + const fieldMeta: JSONModelField = { + name: this.getFieldName(field), + isArray: field.isList, + type: this.getType(field.type), + isRequired: !field.isNullable, + attributes: [], + }; + + if (field.isListNullable !== undefined) { + fieldMeta.isArrayNullable = field.isListNullable; + } + + if (field.isReadOnly !== undefined) { + fieldMeta.isReadOnly = field.isReadOnly; + } + + const association: AssociationType | void = this.getFieldAssociation(field); + if (association) { + fieldMeta.association = association; + } + acc[fieldMeta.name] = fieldMeta; + return acc; + }, {}) + } + private getType(gqlType: string): JSONModelFieldType { // Todo: Handle unlisted scalars if (gqlType in METADATA_SCALAR_MAP) { @@ -273,6 +316,12 @@ export class AppSyncJSONVisitor< if (gqlType in this.nonModelMap) { return { nonModel: gqlType }; } + if (gqlType in this.interfaceMap) { + return { interface: this.interfaceMap[gqlType].name }; + } + if (gqlType in this.unionMap) { + return { union: this.unionMap[gqlType].name }; + } if (gqlType in this.modelMap) { return { model: gqlType }; } diff --git a/packages/appsync-modelgen-plugin/src/visitors/appsync-typescript-visitor.ts b/packages/appsync-modelgen-plugin/src/visitors/appsync-typescript-visitor.ts index 25f642f6c..931f1d4e0 100644 --- a/packages/appsync-modelgen-plugin/src/visitors/appsync-typescript-visitor.ts +++ b/packages/appsync-modelgen-plugin/src/visitors/appsync-typescript-visitor.ts @@ -4,8 +4,10 @@ import { AppSyncModelVisitor, CodeGenEnum, CodeGenField, + CodeGenInterface, CodeGenModel, CodeGenPrimaryKeyType, + CodeGenUnion, ParsedAppSyncModelConfig, RawAppSyncModelConfig, } from './appsync-visitor'; @@ -63,11 +65,19 @@ export class AppSyncModelTypeScriptVisitor< .map(typeObj => this.generateModelDeclaration(typeObj, true, false)) .join('\n\n'); + const unionDeclarations = Object.values(this.unionMap) + .map(unionObj => this.generateUnionDeclaration(unionObj)) + .join('\n\n'); + + const interfaceDeclarations = Object.values(this.interfaceMap) + .map(interfaceObj => this.generateInterfaceDeclaration(interfaceObj)) + .join('\n\n'); + const modelInitialization = this.generateModelInitialization([...Object.values(this.modelMap), ...Object.values(this.nonModelMap)]); const modelExports = this.generateExports(Object.values(this.modelMap)); - return [imports, enumDeclarations, modelDeclarations, nonModelDeclarations, modelInitialization, modelExports].join('\n\n'); + return [imports, enumDeclarations, unionDeclarations, interfaceDeclarations, modelDeclarations, nonModelDeclarations, modelInitialization, modelExports].join('\n\n'); } protected generateImports(): string { @@ -215,6 +225,26 @@ export class AppSyncModelTypeScriptVisitor< return [eagerModelDeclaration.string, lazyModelDeclaration.string, conditionalType, modelVariable].join('\n\n'); } + protected generateInterfaceDeclaration(interfaceObj: CodeGenInterface): string { + const declaration = new TypeScriptDeclarationBlock() + .asKind('interface') + .withName(interfaceObj.name) + .export(true); + + interfaceObj.fields.forEach(field => { + declaration.addProperty(field.name, this.getNativeType(field), undefined, 'DEFAULT', { + readonly: true, + optional: field.isList ? field.isListNullable : field.isNullable, + }); + }); + + return declaration.string; + } + + protected generateUnionDeclaration(unionObj: CodeGenUnion): string { + return `export declare type ${unionObj.name} = ${unionObj.typeNames.join(' | ')};`; + } + /** * Generate model Declaration using classCreator * @param model @@ -242,15 +272,15 @@ export class AppSyncModelTypeScriptVisitor< return `${initializationResult.join(' ')};`; } - protected generateExports(modelsOrEnum: (CodeGenModel | CodeGenEnum)[]): string { - const exportStr = modelsOrEnum - .map(model => { - if (model.type === 'model') { - const modelClassName = this.generateModelImportAlias(model); - const exportClassName = this.getModelName(model); + protected generateExports(types: (CodeGenModel | CodeGenEnum | CodeGenInterface | CodeGenUnion)[]): string { + const exportStr = types + .map(type => { + if (type.type === 'model') { + const modelClassName = this.generateModelImportAlias(type); + const exportClassName = this.getModelName(type); return modelClassName !== exportClassName ? `${modelClassName} as ${exportClassName}` : modelClassName; } - return model.name; + return type.name; }) .join(',\n'); return ['export {', indentMultiline(exportStr), '};'].join('\n'); diff --git a/packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts b/packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts index 7e978ac22..7c8d29302 100644 --- a/packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts +++ b/packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts @@ -558,6 +558,23 @@ export class AppSyncModelVisitor< } return this.enumMap; } + + protected getSelectedUnions(): CodeGenUnionMap { + if (this._parsedConfig.selectedType) { + const selectedModel = this.unionMap[this._parsedConfig.selectedType]; + return selectedModel ? { [this._parsedConfig.selectedType]: selectedModel } : {}; + } + return this.unionMap; + } + + protected getSelectedInterfaces(): CodeGenInterfaceMap { + if (this._parsedConfig.selectedType) { + const selectedModel = this.interfaceMap[this._parsedConfig.selectedType]; + return selectedModel ? { [this._parsedConfig.selectedType]: selectedModel } : {}; + } + return this.interfaceMap; + } + protected selectedTypeIsEnum() { if (this._parsedConfig && this._parsedConfig.selectedType) { if (this._parsedConfig.selectedType in this.enumMap) { @@ -591,6 +608,10 @@ export class AppSyncModelVisitor< typeNameStr = this.getEnumName(this.enumMap[typeName]); } else if (this.isNonModelType(field)) { typeNameStr = this.getNonModelName(this.nonModelMap[typeName]); + } else if (this.isUnionType(field)) { + typeNameStr = this.getUnionName(this.unionMap[typeName]); + } else if (this.isInterfaceType(field)) { + typeNameStr = this.getInterfaceName(this.interfaceMap[typeName]); } else { throw new Error(`Unknown type ${typeName} for field ${field.name}. Did you forget to add the @model directive`); } @@ -617,6 +638,14 @@ export class AppSyncModelVisitor< return model.name; } + protected getUnionName(union: CodeGenUnion) { + return union.name; + } + + protected getInterfaceName(codeGenInterface: CodeGenInterface) { + return codeGenInterface.name; + } + protected getNonModelName(model: CodeGenModel) { return model.name; } @@ -644,6 +673,16 @@ export class AppSyncModelVisitor< return typeName in this.nonModelMap; } + protected isUnionType(field: CodeGenField): boolean { + const typeName = field.type; + return typeName in this.unionMap; + } + + protected isInterfaceType(field: CodeGenField): boolean { + const typeName = field.type; + return typeName in this.interfaceMap; + } + protected computeVersion(): string { // Sort types const typeArr: any[] = [];