Skip to content

Commit 008df44

Browse files
committed
feat: Add support for JS and TS interfaces/unions
1 parent 049f007 commit 008df44

File tree

7 files changed

+209
-39
lines changed

7 files changed

+209
-39
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"verdaccio-stop": "kill -9 $(lsof -n -t -iTCP:4873 -sTCP:LISTEN)",
2727
"setup-dev": "(yarn && lerna run build) && yarn add-cli-no-save && (yarn hoist-cli && yarn rm-dev-link && yarn link-dev)",
2828
"add-cli-no-save": "yarn add @aws-amplify/amplify-category-api @aws-amplify/cli-internal -W && git checkout -- package.json yarn.lock",
29-
"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",
29+
"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",
3030
"link-dev": "cd node_modules/amplify-cli-internal && ln -s \"$(pwd)/bin/amplify\" \"$(yarn global bin)/amplify-dev\" && cd -",
3131
"rm-dev-link": "rm -f \"$(yarn global bin)/amplify-dev\"",
3232
"commit": "git-cz",

packages/amplify-codegen/src/commands/models.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const path = require('path');
22
const fs = require('fs-extra');
33
const globby = require('globby');
4-
const { FeatureFlags, pathManager } = require('@aws-amplify/amplify-cli-core');
4+
const { FeatureFlags, pathManager, stateManager, CLIContextEnvironmentProvider } = require('@aws-amplify/amplify-cli-core');
55
const { generateModels: generateModelsHelper } = require('@aws-amplify/graphql-generator');
66
const { DefaultDirectives } = require('@aws-amplify/graphql-directives');
77
const { validateAmplifyFlutterMinSupportedVersion } = require('../utils/validateAmplifyFlutterMinSupportedVersion');
@@ -259,6 +259,15 @@ async function generateModels(context, generateOptions = null) {
259259
return;
260260
}
261261

262+
if (!FeatureFlags.isInitialized()) {
263+
const contextEnvironmentProvider = new CLIContextEnvironmentProvider({
264+
getEnvInfo: context.amplify.getEnvInfo,
265+
});
266+
267+
const useNewDefaults = !stateManager.projectConfigExists(projectRoot);
268+
await FeatureFlags.initialize(contextEnvironmentProvider, useNewDefaults);
269+
}
270+
262271
const generatedCode = await generateModelsHelper({
263272
schema: loadSchema(apiResourcePath),
264273
directives: await getDirectives(context, apiResourcePath),

packages/appsync-modelgen-plugin/src/languages/typescript-declaration-block.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,21 @@ export class TypeScriptDeclarationBlock {
255255
}
256256

257257
protected generateInterface(): string {
258-
throw new Error('Not implemented yet');
258+
const header = [
259+
this._flags.shouldExport ? 'export' : '',
260+
this._flags.isDeclaration ? 'declare' : '',
261+
'interface',
262+
this._name,
263+
'{',
264+
];
265+
266+
if (this._extends.length) {
267+
header.push(['extends', this._extends.join(', ')].join(' '));
268+
}
269+
270+
const body = [this.generateProperties()];
271+
272+
return [`${header.filter(h => h).join(' ')}`, indentMultiline(body.join('\n')), '}'].join('\n');
259273
}
260274

261275
protected generateType(): string {

packages/appsync-modelgen-plugin/src/visitors/appsync-javascript-visitor.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,24 @@ export class AppSyncModelJavascriptVisitor<
6868
.map(typeObj => this.generateModelDeclaration(typeObj, true, false))
6969
.join('\n\n');
7070

71+
const interfaceDeclarations = Object.values(this.interfaceMap)
72+
.map(typeObj => this.generateInterfaceDeclaration(typeObj))
73+
.join('\n\n');
74+
75+
const unionDeclarations = Object.values(this.unionMap)
76+
.map(typeObj => this.generateUnionDeclaration(typeObj))
77+
.join('\n\n');
78+
7179
const imports = this.generateImports();
7280

7381
if (!this.isCustomPKEnabled()) {
7482
const modelMetaData = Object.values(this.modelMap)
7583
.map(typeObj => this.generateModelMetaData(typeObj))
7684
.join('\n\n');
77-
return [imports, enumDeclarations, nonModelDeclarations, modelMetaData, modelDeclarations].filter(b => b).join('\n\n');
85+
return [imports, enumDeclarations, interfaceDeclarations, unionDeclarations, nonModelDeclarations, modelMetaData, modelDeclarations].filter(b => b).join('\n\n');
7886
}
7987

80-
return [imports, enumDeclarations, nonModelDeclarations, modelDeclarations].join('\n\n');
88+
return [imports, enumDeclarations, interfaceDeclarations, unionDeclarations, nonModelDeclarations, modelDeclarations].filter(b => b).join('\n\n');
8189
} else {
8290
const imports = this.generateImportsJavaScriptImplementation();
8391
const enumDeclarations = Object.values(this.enumMap)

packages/appsync-modelgen-plugin/src/visitors/appsync-json-metadata-visitor.ts

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,35 @@ import {
88
ParsedAppSyncModelConfig,
99
RawAppSyncModelConfig,
1010
CodeGenEnum,
11+
CodeGenUnion,
12+
CodeGenInterface,
1113
} from './appsync-visitor';
1214
import { METADATA_SCALAR_MAP } from '../scalars';
1315
export type JSONSchema = {
1416
models: JSONSchemaModels;
1517
enums: JSONSchemaEnums;
1618
nonModels: JSONSchemaTypes;
19+
interfaces: JSONSchemaInterfaces;
20+
unions: JSONSchemaUnions;
1721
version: string;
1822
codegenVersion: string;
1923
};
2024
export type JSONSchemaModels = Record<string, JSONSchemaModel>;
2125
export type JSONSchemaTypes = Record<string, JSONSchemaNonModel>;
26+
export type JSONSchemaInterfaces = Record<string, JSONSchemaInterface>;
27+
export type JSONSchemaUnions = Record<string, JSONSchemaUnion>;
2228
export type JSONSchemaNonModel = {
2329
name: string;
2430
fields: JSONModelFields;
2531
};
32+
export type JSONSchemaInterface = {
33+
name: string;
34+
fields: JSONModelFields;
35+
};
36+
export type JSONSchemaUnion = {
37+
name: string;
38+
types: JSONModelFieldType[];
39+
};
2640
type JSONSchemaModel = {
2741
name: string;
2842
attributes?: JSONModelAttributes;
@@ -58,7 +72,7 @@ type AssociationBelongsTo = AssociationBaseType & {
5872

5973
type AssociationType = AssociationHasMany | AssociationHasOne | AssociationBelongsTo;
6074

61-
type JSONModelFieldType = keyof typeof METADATA_SCALAR_MAP | { model: string } | { enum: string } | { nonModel: string };
75+
type JSONModelFieldType = keyof typeof METADATA_SCALAR_MAP | { model: string } | { enum: string } | { nonModel: string } | { interface: string } | { union: string };
6276
type JSONModelField = {
6377
name: string;
6478
type: JSONModelFieldType;
@@ -147,7 +161,27 @@ export class AppSyncJSONVisitor<
147161
}
148162

149163
protected generateTypeDeclaration() {
150-
return ["import { Schema } from '@aws-amplify/datastore';", '', 'export declare const schema: Schema;'].join('\n');
164+
return `import type { Schema, SchemaNonModel, ModelField, ModelFieldType } from '@aws-amplify/datastore';
165+
166+
type Replace<T, R> = Omit<T, keyof R> & R;
167+
type WithFields = { fields: Record<string, ModelField> };
168+
type SchemaTypes = Record<string, WithFields>;
169+
170+
export type ExtendModelFieldType = ModelField['type'] | { interface: string } | { union: string };
171+
export type ExtendModelField = Replace<ModelField, { type: ExtendModelFieldType }>;
172+
export type ExtendType<T extends WithFields> = Replace<T, { fields: Record<string, ExtendModelField> }>
173+
export type ExtendFields<Types extends SchemaTypes | undefined> = {
174+
[TypeName in keyof Types]: ExtendType<Types[TypeName]>
175+
}
176+
177+
type ExtendFieldsAll<T> = {
178+
[K in keyof T]: T[K] extends SchemaTypes | undefined ? ExtendFields<T[K]> : T[K];
179+
};
180+
181+
export declare const schema: ExtendFieldsAll<Schema & {
182+
interfaces: Schema['nonModels'];
183+
unions?: Record<string, {name: string, types: ExtendModelFieldType[]}>;
184+
}>;`;
151185
}
152186

153187
protected generateJSONMetadata(): string {
@@ -160,6 +194,8 @@ export class AppSyncJSONVisitor<
160194
models: {},
161195
enums: {},
162196
nonModels: {},
197+
interfaces: {},
198+
unions: {},
163199
// This is hard-coded for the schema version purpose instead of codegen version
164200
// To avoid the failure of validation method checkCodegenSchema in JS Datastore
165201
// The hard code is starting from amplify codegen major version 4
@@ -175,11 +211,19 @@ export class AppSyncJSONVisitor<
175211
return { ...acc, [nonModel.name]: this.generateNonModelMetadata(nonModel) };
176212
}, {});
177213

214+
const interfaces = Object.values(this.getSelectedInterfaces()).reduce((acc, codegenInterface: CodeGenInterface) => {
215+
return { ...acc, [codegenInterface.name]: this.generateInterfaceMetadata(codegenInterface) };
216+
}, {});
217+
218+
const unions = Object.values(this.getSelectedUnions()).reduce((acc, union: CodeGenUnion) => {
219+
return { ...acc, [union.name]: this.generateUnionMetadata(union) };
220+
}, {});
221+
178222
const enums = Object.values(this.enumMap).reduce((acc, enumObj) => {
179223
const enumV = this.generateEnumMetadata(enumObj);
180224
return { ...acc, [this.getEnumName(enumObj)]: enumV };
181225
}, {});
182-
return { ...result, models, nonModels: nonModels, enums };
226+
return { ...result, models, nonModels: nonModels, enums, interfaces, unions };
183227
}
184228

185229
private getFieldAssociation(field: CodeGenField): AssociationType | void {
@@ -229,39 +273,58 @@ export class AppSyncJSONVisitor<
229273
private generateNonModelMetadata(nonModel: CodeGenModel): JSONSchemaNonModel {
230274
return {
231275
name: this.getModelName(nonModel),
232-
fields: nonModel.fields.reduce((acc: JSONModelFields, field: CodeGenField) => {
233-
const fieldMeta: JSONModelField = {
234-
name: this.getFieldName(field),
235-
isArray: field.isList,
236-
type: this.getType(field.type),
237-
isRequired: !field.isNullable,
238-
attributes: [],
239-
};
240-
241-
if (field.isListNullable !== undefined) {
242-
fieldMeta.isArrayNullable = field.isListNullable;
243-
}
276+
fields: this.generateFieldsMetadata(nonModel.fields)
277+
};
278+
}
244279

245-
if (field.isReadOnly !== undefined) {
246-
fieldMeta.isReadOnly = field.isReadOnly;
247-
}
280+
private generateInterfaceMetadata(codeGenInterface: CodeGenInterface): JSONSchemaInterface {
281+
return {
282+
name: codeGenInterface.name,
283+
fields: this.generateFieldsMetadata(codeGenInterface.fields),
284+
};
285+
}
248286

249-
const association: AssociationType | void = this.getFieldAssociation(field);
250-
if (association) {
251-
fieldMeta.association = association;
252-
}
253-
acc[fieldMeta.name] = fieldMeta;
254-
return acc;
255-
}, {}),
287+
private generateUnionMetadata(codeGenUnion: CodeGenUnion): JSONSchemaUnion {
288+
return {
289+
name: codeGenUnion.name,
290+
types: codeGenUnion.typeNames.map(t => this.getType(t))
256291
};
257292
}
293+
258294
private generateEnumMetadata(enumObj: CodeGenEnum): JSONSchemaEnum {
259295
return {
260296
name: enumObj.name,
261297
values: Object.values(enumObj.values),
262298
};
263299
}
264300

301+
private generateFieldsMetadata(fields: CodeGenField[]): JSONModelFields {
302+
return fields.reduce((acc: JSONModelFields, field: CodeGenField) => {
303+
const fieldMeta: JSONModelField = {
304+
name: this.getFieldName(field),
305+
isArray: field.isList,
306+
type: this.getType(field.type),
307+
isRequired: !field.isNullable,
308+
attributes: [],
309+
};
310+
311+
if (field.isListNullable !== undefined) {
312+
fieldMeta.isArrayNullable = field.isListNullable;
313+
}
314+
315+
if (field.isReadOnly !== undefined) {
316+
fieldMeta.isReadOnly = field.isReadOnly;
317+
}
318+
319+
const association: AssociationType | void = this.getFieldAssociation(field);
320+
if (association) {
321+
fieldMeta.association = association;
322+
}
323+
acc[fieldMeta.name] = fieldMeta;
324+
return acc;
325+
}, {})
326+
}
327+
265328
private getType(gqlType: string): JSONModelFieldType {
266329
// Todo: Handle unlisted scalars
267330
if (gqlType in METADATA_SCALAR_MAP) {
@@ -273,6 +336,12 @@ export class AppSyncJSONVisitor<
273336
if (gqlType in this.nonModelMap) {
274337
return { nonModel: gqlType };
275338
}
339+
if (gqlType in this.interfaceMap) {
340+
return { interface: this.interfaceMap[gqlType].name };
341+
}
342+
if (gqlType in this.unionMap) {
343+
return { union: this.unionMap[gqlType].name };
344+
}
276345
if (gqlType in this.modelMap) {
277346
return { model: gqlType };
278347
}

packages/appsync-modelgen-plugin/src/visitors/appsync-typescript-visitor.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import {
44
AppSyncModelVisitor,
55
CodeGenEnum,
66
CodeGenField,
7+
CodeGenInterface,
78
CodeGenModel,
89
CodeGenPrimaryKeyType,
10+
CodeGenUnion,
911
ParsedAppSyncModelConfig,
1012
RawAppSyncModelConfig,
1113
} from './appsync-visitor';
@@ -64,11 +66,19 @@ export class AppSyncModelTypeScriptVisitor<
6466
.map(typeObj => this.generateModelDeclaration(typeObj, true, false))
6567
.join('\n\n');
6668

69+
const unionDeclarations = Object.values(this.unionMap)
70+
.map(unionObj => this.generateUnionDeclaration(unionObj))
71+
.join('\n\n');
72+
73+
const interfaceDeclarations = Object.values(this.interfaceMap)
74+
.map(interfaceObj => this.generateInterfaceDeclaration(interfaceObj))
75+
.join('\n\n');
76+
6777
const modelInitialization = this.generateModelInitialization([...Object.values(this.modelMap), ...Object.values(this.nonModelMap)]);
6878

6979
const modelExports = this.generateExports(Object.values(this.modelMap));
7080

71-
return [imports, enumDeclarations, modelDeclarations, nonModelDeclarations, modelInitialization, modelExports].join('\n\n');
81+
return [imports, enumDeclarations, unionDeclarations, interfaceDeclarations, modelDeclarations, nonModelDeclarations, modelInitialization, modelExports].join('\n\n');
7282
}
7383

7484
protected generateImports(): string {
@@ -216,6 +226,26 @@ export class AppSyncModelTypeScriptVisitor<
216226
return [eagerModelDeclaration.string, lazyModelDeclaration.string, conditionalType, modelVariable].join('\n\n');
217227
}
218228

229+
protected generateInterfaceDeclaration(interfaceObj: CodeGenInterface): string {
230+
const declaration = new TypeScriptDeclarationBlock()
231+
.asKind('interface')
232+
.withName(interfaceObj.name)
233+
.export(true);
234+
235+
interfaceObj.fields.forEach(field => {
236+
declaration.addProperty(field.name, this.getNativeType(field), undefined, 'DEFAULT', {
237+
readonly: true,
238+
optional: field.isList ? field.isListNullable : field.isNullable,
239+
});
240+
});
241+
242+
return declaration.string;
243+
}
244+
245+
protected generateUnionDeclaration(unionObj: CodeGenUnion): string {
246+
return `export declare type ${unionObj.name} = ${unionObj.typeNames.length > 0 ? unionObj.typeNames.join(' | ') : 'never'};`;
247+
}
248+
219249
/**
220250
* Generate model Declaration using classCreator
221251
* @param model
@@ -243,16 +273,17 @@ export class AppSyncModelTypeScriptVisitor<
243273
return `${initializationResult.join(' ')};`;
244274
}
245275

246-
protected generateExports(modelsOrEnum: (CodeGenModel | CodeGenEnum)[]): string {
247-
const exportStr = modelsOrEnum
248-
.map(model => {
249-
if (model.type === 'model') {
250-
const modelClassName = this.generateModelImportAlias(model);
251-
const exportClassName = this.getModelName(model);
276+
protected generateExports(types: (CodeGenModel | CodeGenEnum | CodeGenInterface | CodeGenUnion)[]): string {
277+
const exportStr = types
278+
.map(type => {
279+
if (type.type === 'model') {
280+
const modelClassName = this.generateModelImportAlias(type);
281+
const exportClassName = this.getModelName(type);
252282
return modelClassName !== exportClassName ? `${modelClassName} as ${exportClassName}` : modelClassName;
253-
} else if (model.type === 'enum') {
254-
return pascalCase(model.name);
283+
} else if (type.type === 'enum') {
284+
return pascalCase(type.name);
255285
}
286+
return type.name;
256287
})
257288
.join(',\n');
258289
return ['export {', indentMultiline(exportStr), '};'].join('\n');

0 commit comments

Comments
 (0)