Skip to content

Commit f5fd8b9

Browse files
committed
feat: Add support for JS and TS interfaces/unions
1 parent e6ac948 commit f5fd8b9

File tree

4 files changed

+186
-34
lines changed

4 files changed

+186
-34
lines changed

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

+15-1
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-json-metadata-visitor.ts

+94-25
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

+38-8
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';
@@ -63,11 +65,19 @@ export class AppSyncModelTypeScriptVisitor<
6365
.map(typeObj => this.generateModelDeclaration(typeObj, true, false))
6466
.join('\n\n');
6567

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

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

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

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

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

245-
protected generateExports(modelsOrEnum: (CodeGenModel | CodeGenEnum)[]): string {
246-
const exportStr = modelsOrEnum
247-
.map(model => {
248-
if (model.type === 'model') {
249-
const modelClassName = this.generateModelImportAlias(model);
250-
const exportClassName = this.getModelName(model);
275+
protected generateExports(types: (CodeGenModel | CodeGenEnum | CodeGenInterface | CodeGenUnion)[]): string {
276+
const exportStr = types
277+
.map(type => {
278+
if (type.type === 'model') {
279+
const modelClassName = this.generateModelImportAlias(type);
280+
const exportClassName = this.getModelName(type);
251281
return modelClassName !== exportClassName ? `${modelClassName} as ${exportClassName}` : modelClassName;
252282
}
253-
return model.name;
283+
return type.name;
254284
})
255285
.join(',\n');
256286
return ['export {', indentMultiline(exportStr), '};'].join('\n');

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

+39
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,23 @@ export class AppSyncModelVisitor<
558558
}
559559
return this.enumMap;
560560
}
561+
562+
protected getSelectedUnions(): CodeGenUnionMap {
563+
if (this._parsedConfig.selectedType) {
564+
const selectedModel = this.unionMap[this._parsedConfig.selectedType];
565+
return selectedModel ? { [this._parsedConfig.selectedType]: selectedModel } : {};
566+
}
567+
return this.unionMap;
568+
}
569+
570+
protected getSelectedInterfaces(): CodeGenInterfaceMap {
571+
if (this._parsedConfig.selectedType) {
572+
const selectedModel = this.interfaceMap[this._parsedConfig.selectedType];
573+
return selectedModel ? { [this._parsedConfig.selectedType]: selectedModel } : {};
574+
}
575+
return this.interfaceMap;
576+
}
577+
561578
protected selectedTypeIsEnum() {
562579
if (this._parsedConfig && this._parsedConfig.selectedType) {
563580
if (this._parsedConfig.selectedType in this.enumMap) {
@@ -591,6 +608,10 @@ export class AppSyncModelVisitor<
591608
typeNameStr = this.getEnumName(this.enumMap[typeName]);
592609
} else if (this.isNonModelType(field)) {
593610
typeNameStr = this.getNonModelName(this.nonModelMap[typeName]);
611+
} else if (this.isUnionType(field)) {
612+
typeNameStr = this.getUnionName(this.unionMap[typeName]);
613+
} else if (this.isInterfaceType(field)) {
614+
typeNameStr = this.getInterfaceName(this.interfaceMap[typeName]);
594615
} else {
595616
throw new Error(`Unknown type ${typeName} for field ${field.name}. Did you forget to add the @model directive`);
596617
}
@@ -617,6 +638,14 @@ export class AppSyncModelVisitor<
617638
return model.name;
618639
}
619640

641+
protected getUnionName(union: CodeGenUnion) {
642+
return union.name;
643+
}
644+
645+
protected getInterfaceName(codeGenInterface: CodeGenInterface) {
646+
return codeGenInterface.name;
647+
}
648+
620649
protected getNonModelName(model: CodeGenModel) {
621650
return model.name;
622651
}
@@ -644,6 +673,16 @@ export class AppSyncModelVisitor<
644673
return typeName in this.nonModelMap;
645674
}
646675

676+
protected isUnionType(field: CodeGenField): boolean {
677+
const typeName = field.type;
678+
return typeName in this.unionMap;
679+
}
680+
681+
protected isInterfaceType(field: CodeGenField): boolean {
682+
const typeName = field.type;
683+
return typeName in this.interfaceMap;
684+
}
685+
647686
protected computeVersion(): string {
648687
// Sort types
649688
const typeArr: any[] = [];

0 commit comments

Comments
 (0)