Skip to content

feat: Add support for JS and TS interfaces/unions #930

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"verdaccio-stop": "kill -9 $(lsof -n -t -iTCP:4873 -sTCP:LISTEN)",
"setup-dev": "(yarn && lerna run build) && yarn add-cli-no-save && (yarn hoist-cli && yarn rm-dev-link && yarn link-dev)",
"add-cli-no-save": "yarn add @aws-amplify/amplify-category-api @aws-amplify/cli-internal -W && git checkout -- package.json yarn.lock",
"hoist-cli": "rm -rf node_modules/amplify-cli-internal && mkdir node_modules/amplify-cli-internal && cp -r node_modules/@aws-amplify/cli-internal/ node_modules/amplify-cli-internal",
"hoist-cli": "rm -rf node_modules/amplify-cli-internal && mkdir node_modules/amplify-cli-internal && cp -r node_modules/@aws-amplify/cli-internal/. node_modules/amplify-cli-internal",
"link-dev": "cd node_modules/amplify-cli-internal && ln -s \"$(pwd)/bin/amplify\" \"$(yarn global bin)/amplify-dev\" && cd -",
"rm-dev-link": "rm -f \"$(yarn global bin)/amplify-dev\"",
"commit": "git-cz",
Expand Down
11 changes: 10 additions & 1 deletion packages/amplify-codegen/src/commands/models.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const path = require('path');
const fs = require('fs-extra');
const globby = require('globby');
const { FeatureFlags, pathManager } = require('@aws-amplify/amplify-cli-core');
const { FeatureFlags, pathManager, stateManager, CLIContextEnvironmentProvider } = require('@aws-amplify/amplify-cli-core');
const { generateModels: generateModelsHelper } = require('@aws-amplify/graphql-generator');
const { DefaultDirectives } = require('@aws-amplify/graphql-directives');
const { validateAmplifyFlutterMinSupportedVersion } = require('../utils/validateAmplifyFlutterMinSupportedVersion');
Expand Down Expand Up @@ -259,6 +259,15 @@ async function generateModels(context, generateOptions = null) {
return;
}

if (!FeatureFlags.isInitialized()) {
const contextEnvironmentProvider = new CLIContextEnvironmentProvider({
getEnvInfo: context.amplify.getEnvInfo,
});

const useNewDefaults = !stateManager.projectConfigExists(projectRoot);
await FeatureFlags.initialize(contextEnvironmentProvider, useNewDefaults);
}

const generatedCode = await generateModelsHelper({
schema: loadSchema(apiResourcePath),
directives: await getDirectives(context, apiResourcePath),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,24 @@ export class AppSyncModelJavascriptVisitor<
.map(typeObj => this.generateModelDeclaration(typeObj, true, false))
.join('\n\n');

const interfaceDeclarations = Object.values(this.interfaceMap)
.map(typeObj => this.generateInterfaceDeclaration(typeObj))
.join('\n\n');

const unionDeclarations = Object.values(this.unionMap)
.map(typeObj => this.generateUnionDeclaration(typeObj))
.join('\n\n');

const imports = this.generateImports();

if (!this.isCustomPKEnabled()) {
const modelMetaData = Object.values(this.modelMap)
.map(typeObj => this.generateModelMetaData(typeObj))
.join('\n\n');
return [imports, enumDeclarations, nonModelDeclarations, modelMetaData, modelDeclarations].filter(b => b).join('\n\n');
return [imports, enumDeclarations, interfaceDeclarations, unionDeclarations, nonModelDeclarations, modelMetaData, modelDeclarations].filter(b => b).join('\n\n');
}

return [imports, enumDeclarations, nonModelDeclarations, modelDeclarations].join('\n\n');
return [imports, enumDeclarations, interfaceDeclarations, unionDeclarations, nonModelDeclarations, modelDeclarations].filter(b => b).join('\n\n');
} else {
const imports = this.generateImportsJavaScriptImplementation();
const enumDeclarations = Object.values(this.enumMap)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, JSONSchemaModel>;
export type JSONSchemaTypes = Record<string, JSONSchemaNonModel>;
export type JSONSchemaInterfaces = Record<string, JSONSchemaInterface>;
export type JSONSchemaUnions = Record<string, JSONSchemaUnion>;
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -147,7 +161,27 @@ export class AppSyncJSONVisitor<
}

protected generateTypeDeclaration() {
return ["import { Schema } from '@aws-amplify/datastore';", '', 'export declare const schema: Schema;'].join('\n');
return `import type { Schema, SchemaNonModel, ModelField, ModelFieldType } from '@aws-amplify/datastore';

type Replace<T, R> = Omit<T, keyof R> & R;
type WithFields = { fields: Record<string, ModelField> };
type SchemaTypes = Record<string, WithFields>;

export type ExtendModelFieldType = ModelField['type'] | { interface: string } | { union: string };
export type ExtendModelField = Replace<ModelField, { type: ExtendModelFieldType }>;
export type ExtendType<T extends WithFields> = Replace<T, { fields: Record<string, ExtendModelField> }>
export type ExtendFields<Types extends SchemaTypes | undefined> = {
[TypeName in keyof Types]: ExtendType<Types[TypeName]>
}

type ExtendFieldsAll<T> = {
[K in keyof T]: T[K] extends SchemaTypes | undefined ? ExtendFields<T[K]> : T[K];
};

export declare const schema: ExtendFieldsAll<Schema & {
interfaces: Schema['nonModels'];
unions?: Record<string, {name: string, types: ExtendModelFieldType[]}>;
}>;`;
}

protected generateJSONMetadata(): string {
Expand All @@ -160,6 +194,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
Expand All @@ -175,11 +211,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 {
Expand Down Expand Up @@ -229,39 +273,58 @@ 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,
values: Object.values(enumObj.values),
};
}

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) {
Expand All @@ -273,6 +336,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 };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import {
AppSyncModelVisitor,
CodeGenEnum,
CodeGenField,
CodeGenInterface,
CodeGenModel,
CodeGenPrimaryKeyType,
CodeGenUnion,
ParsedAppSyncModelConfig,
RawAppSyncModelConfig,
} from './appsync-visitor';
Expand Down Expand Up @@ -64,11 +66,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 {
Expand Down Expand Up @@ -216,6 +226,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.length > 0 ? unionObj.typeNames.join(' | ') : 'never'};`;
}

/**
* Generate model Declaration using classCreator
* @param model
Expand Down Expand Up @@ -243,15 +273,17 @@ 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;
} else if (type.type === 'enum') {
return pascalCase(type.name);
}
return model.name;
return type.name;
})
.join(',\n');
return ['export {', indentMultiline(exportStr), '};'].join('\n');
Expand Down
Loading