Skip to content

Commit

Permalink
feat: Add support for JS and TS interfaces/unions
Browse files Browse the repository at this point in the history
  • Loading branch information
MorFix committed Feb 6, 2025
1 parent e6ac948 commit 5a089b7
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 33 deletions.
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
1 change: 1 addition & 0 deletions packages/appsync-modelgen-plugin/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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 @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -229,39 +253,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 +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 };
}
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 @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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');
Expand Down
39 changes: 39 additions & 0 deletions packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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`);
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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[] = [];
Expand Down

0 comments on commit 5a089b7

Please sign in to comment.