Skip to content
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

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

Open
wants to merge 1 commit 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
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',
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering why this was needed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably indeed not needed (js generator inherits ts). Removing.

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
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