From f44358aef41556a9dbc8e511ad69d254a1e7ee67 Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Mon, 18 Sep 2023 09:00:10 -0600 Subject: [PATCH 01/11] fix: avoid name collision on java model build step (#700) --- .../appsync-java-visitor.test.ts.snap | 200 ++++++++++++++++++ .../visitors/appsync-java-visitor.test.ts | 109 +++++++--- .../src/visitors/appsync-java-visitor.ts | 122 ++++++++--- 3 files changed, 367 insertions(+), 64 deletions(-) diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-java-visitor.test.ts.snap b/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-java-visitor.test.ts.snap index f4a9a0cf8..f27aab9b0 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-java-visitor.test.ts.snap +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-java-visitor.test.ts.snap @@ -7494,6 +7494,206 @@ public final class Todo implements Model { " `; +exports[`AppSyncModelVisitor should avoid name collision on builder step 1`] = ` +"package com.amplifyframework.datastore.generated.model; + +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.List; +import java.util.UUID; +import java.util.Objects; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.annotations.Index; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +/** This is an auto generated class representing the MyObject type in your schema. */ +@SuppressWarnings(\\"all\\") +@ModelConfig(pluralName = \\"MyObjects\\") +public final class MyObject implements Model { + public static final QueryField ID = field(\\"MyObject\\", \\"id\\"); + public static final QueryField TUTORIAL = field(\\"MyObject\\", \\"tutorial\\"); + public static final QueryField FORM_CUES = field(\\"MyObject\\", \\"formCues\\"); + private final @ModelField(targetType=\\"ID\\", isRequired = true) String id; + private final @ModelField(targetType=\\"TutorialStep\\", isRequired = true) List tutorial; + private final @ModelField(targetType=\\"FormCue\\", isRequired = true) List formCues; + private @ModelField(targetType=\\"AWSDateTime\\", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType=\\"AWSDateTime\\", isReadOnly = true) Temporal.DateTime updatedAt; + public String getId() { + return id; + } + + public List getTutorial() { + return tutorial; + } + + public List getFormCues() { + return formCues; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private MyObject(String id, List tutorial, List formCues) { + this.id = id; + this.tutorial = tutorial; + this.formCues = formCues; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + MyObject myObject = (MyObject) obj; + return ObjectsCompat.equals(getId(), myObject.getId()) && + ObjectsCompat.equals(getTutorial(), myObject.getTutorial()) && + ObjectsCompat.equals(getFormCues(), myObject.getFormCues()) && + ObjectsCompat.equals(getCreatedAt(), myObject.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), myObject.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getId()) + .append(getTutorial()) + .append(getFormCues()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append(\\"MyObject {\\") + .append(\\"id=\\" + String.valueOf(getId()) + \\", \\") + .append(\\"tutorial=\\" + String.valueOf(getTutorial()) + \\", \\") + .append(\\"formCues=\\" + String.valueOf(getFormCues()) + \\", \\") + .append(\\"createdAt=\\" + String.valueOf(getCreatedAt()) + \\", \\") + .append(\\"updatedAt=\\" + String.valueOf(getUpdatedAt())) + .append(\\"}\\") + .toString(); + } + + public static TutorialBuildStep builder() { + return new Builder(); + } + + /** + * WARNING: This method should not be used to build an instance of this object for a CREATE mutation. + * This is a convenience method to return an instance of the object with only its ID populated + * to be used in the context of a parameter in a delete mutation or referencing a foreign key + * in a relationship. + * @param id the id of the existing item this instance will represent + * @return an instance of this model with only ID populated + */ + public static MyObject justId(String id) { + return new MyObject( + id, + null, + null + ); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(id, + tutorial, + formCues); + } + public interface TutorialBuildStep { + FormCuesStep tutorial(List tutorial); + } + + + public interface FormCuesStep { + BuildStep formCues(List formCues); + } + + + public interface BuildStep { + MyObject build(); + BuildStep id(String id); + } + + + public static class Builder implements TutorialBuildStep, FormCuesStep, BuildStep { + private String id; + private List tutorial; + private List formCues; + @Override + public MyObject build() { + String id = this.id != null ? this.id : UUID.randomUUID().toString(); + + return new MyObject( + id, + tutorial, + formCues); + } + + @Override + public FormCuesStep tutorial(List tutorial) { + Objects.requireNonNull(tutorial); + this.tutorial = tutorial; + return this; + } + + @Override + public BuildStep formCues(List formCues) { + Objects.requireNonNull(formCues); + this.formCues = formCues; + return this; + } + + /** + * @param id id + * @return Current Builder instance, for fluent method chaining + */ + public BuildStep id(String id) { + this.id = id; + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String id, List tutorial, List formCues) { + super.id(id); + super.tutorial(tutorial) + .formCues(formCues); + } + + @Override + public CopyOfBuilder tutorial(List tutorial) { + return (CopyOfBuilder) super.tutorial(tutorial); + } + + @Override + public CopyOfBuilder formCues(List formCues) { + return (CopyOfBuilder) super.formCues(formCues); + } + } + +} +" +`; + exports[`AppSyncModelVisitor should generate Temporal type for AWSDate* scalars 1`] = ` "package com.amplifyframework.datastore.generated.model; diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-visitor.test.ts index c9959a159..cf748940f 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-visitor.test.ts @@ -11,17 +11,12 @@ const defaultJavaVisitorSettings = { transformerVersion: 1, generate: CodeGenGenerateEnum.code, respectPrimaryKeyAttributesOnConnectionField: false, -} +}; const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; - -const getVisitor = ( - schema: string, - selectedType?: string, - settings: any = {} -) => { +const getVisitor = (schema: string, selectedType?: string, settings: any = {}) => { const visitorConfig = { ...defaultJavaVisitorSettings, ...settings }; const ast = parse(schema); const builtSchema = buildSchemaWithDirectives(schema); @@ -31,7 +26,7 @@ const getVisitor = ( directives, target: 'java', scalars: JAVA_SCALAR_MAP, - ...visitorConfig + ...visitorConfig, }, { selectedType }, ); @@ -39,11 +34,7 @@ const getVisitor = ( return visitor; }; -const getVisitorPipelinedTransformer = ( - schema: string, - selectedType?: string, - settings: any = {} -) => { +const getVisitorPipelinedTransformer = (schema: string, selectedType?: string, settings: any = {}) => { return getVisitor(schema, selectedType, { ...settings, transformerVersion: 2 }); }; @@ -215,6 +206,30 @@ describe('AppSyncModelVisitor', () => { expect(generatedCode).toMatchSnapshot(); }); + it('should avoid name collision on builder step', () => { + const schema = /* GraphQL */ ` + type TutorialStep { + position: Int! + text: String! + } + + type FormCue { + position: Int! + text: String! + } + + type MyObject @model { + id: ID! + tutorial: [TutorialStep!]! + formCues: [FormCue!]! + } + `; + const visitor = getVisitor(schema, 'MyObject'); + const generatedCode = visitor.generate(); + expect(() => validateJava(generatedCode)).not.toThrow(); + expect(generatedCode).toMatchSnapshot(); + }); + describe('vNext transformer feature parity tests', () => { it('should produce the same result for @primaryKey as the primary key variant of @key', async () => { const schemaV1 = /* GraphQL */ ` @@ -596,7 +611,7 @@ describe('AppSyncModelVisitor', () => { content: String tags: [Tag] @manyToMany(relationName: "PostTags") } - + type Tag @model { id: ID! label: String! @@ -614,17 +629,17 @@ describe('AppSyncModelVisitor', () => { type Blog @model { id: ID! name: String! - blogOwner: BlogOwnerWithCustomPKS!@belongsTo + blogOwner: BlogOwnerWithCustomPKS! @belongsTo posts: [Post] @hasMany } - + type BlogOwnerWithCustomPKS @model { id: ID! - name: String!@primaryKey(sortKeyFields: ["wea"]) + name: String! @primaryKey(sortKeyFields: ["wea"]) wea: String! blogs: [Blog] @hasMany } - + type Post @model { postId: ID! @primaryKey title: String! @@ -637,32 +652,40 @@ describe('AppSyncModelVisitor', () => { type Comment @model { post: Post @belongsTo - title: String! @primaryKey(sortKeyFields: ["content","likes"]) + title: String! @primaryKey(sortKeyFields: ["content", "likes"]) content: String! likes: Int! description: String } `; it('Should generate correct model file for default id as primary key type', () => { - const generatedCode = getVisitorPipelinedTransformer(schema, `Blog`, { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); + const generatedCode = getVisitorPipelinedTransformer(schema, `Blog`, { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); expect(() => validateJava(generatedCode)).not.toThrow(); expect(generatedCode).toMatchSnapshot(); }); it('Should generate correct model file for custom primary key type', () => { - const generatedCode = getVisitorPipelinedTransformer(schema, `Post`, { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); + const generatedCode = getVisitorPipelinedTransformer(schema, `Post`, { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); expect(() => validateJava(generatedCode)).not.toThrow(); expect(generatedCode).toMatchSnapshot(); }); it('Should generate correct model file for composite key type without id field defined', () => { - const generatedCode = getVisitorPipelinedTransformer(schema, `Comment`, { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); + const generatedCode = getVisitorPipelinedTransformer(schema, `Comment`, { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); expect(generatedCode).toMatchSnapshot(); }); it('Should generate correct model file for composite key type with id field defined', () => { - const generatedCode = getVisitorPipelinedTransformer(schema, `BlogOwnerWithCustomPKS`, { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); + const generatedCode = getVisitorPipelinedTransformer(schema, `BlogOwnerWithCustomPKS`, { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); expect(generatedCode).toMatchSnapshot(); }); }); - + describe('Custom primary key for connected model tests', () => { it('Should generate correct model file for hasOne & belongsTo relation with composite primary key when CPK is enabled', () => { const schema = /* GraphQL */ ` @@ -678,8 +701,12 @@ describe('AppSyncModelVisitor', () => { project: Project @belongsTo } `; - const generatedCodeProject = getVisitorPipelinedTransformer(schema, `Project`, { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); - const generatedCodeTeam = getVisitorPipelinedTransformer(schema, `Team`, { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); + const generatedCodeProject = getVisitorPipelinedTransformer(schema, `Project`, { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); + const generatedCodeTeam = getVisitorPipelinedTransformer(schema, `Team`, { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); expect(generatedCodeProject).toMatchSnapshot(); expect(generatedCodeTeam).toMatchSnapshot(); }); @@ -690,14 +717,18 @@ describe('AppSyncModelVisitor', () => { title: String! comments: [Comment] @hasMany } - + type Comment @model { id: ID! @primaryKey(sortKeyFields: ["content"]) content: String! } `; - const generatedCodePost = getVisitorPipelinedTransformer(schema, 'Post', { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); - const generatedCodeComment = getVisitorPipelinedTransformer(schema, 'Comment', { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); + const generatedCodePost = getVisitorPipelinedTransformer(schema, 'Post', { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); + const generatedCodeComment = getVisitorPipelinedTransformer(schema, 'Comment', { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); expect(generatedCodePost).toMatchSnapshot(); expect(generatedCodeComment).toMatchSnapshot(); }); @@ -713,7 +744,9 @@ describe('AppSyncModelVisitor', () => { rating: Float! } `; - const generatedCodeMyPost = getVisitorPipelinedTransformer(schema, `MyPost`, { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); + const generatedCodeMyPost = getVisitorPipelinedTransformer(schema, `MyPost`, { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); expect(generatedCodeMyPost).toMatchSnapshot(); }); it('Should generate ModelIdentifier factory with resolveIdentifier returning Java types matching graphql scalar conversion', () => { @@ -725,15 +758,21 @@ describe('AppSyncModelVisitor', () => { type IdModel @model { customKey: ID! @primaryKey } - + type IntModel @model { customKey: Int! @primaryKey } `; - const generatedCodeStringModel= getVisitorPipelinedTransformer(schema, 'StringModel', { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); - const generatedCodeIdModel = getVisitorPipelinedTransformer(schema, 'IdModel', { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); - const generatedCodeIntModel = getVisitorPipelinedTransformer(schema, 'IntModel', { respectPrimaryKeyAttributesOnConnectionField: true }).generate(); - + const generatedCodeStringModel = getVisitorPipelinedTransformer(schema, 'StringModel', { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); + const generatedCodeIdModel = getVisitorPipelinedTransformer(schema, 'IdModel', { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); + const generatedCodeIntModel = getVisitorPipelinedTransformer(schema, 'IntModel', { + respectPrimaryKeyAttributesOnConnectionField: true, + }).generate(); + expect(generatedCodeStringModel).toMatchSnapshot(); expect(generatedCodeIdModel).toMatchSnapshot(); expect(generatedCodeIntModel).toMatchSnapshot(); diff --git a/packages/appsync-modelgen-plugin/src/visitors/appsync-java-visitor.ts b/packages/appsync-modelgen-plugin/src/visitors/appsync-java-visitor.ts index d9d5541ee..97f20ce69 100644 --- a/packages/appsync-modelgen-plugin/src/visitors/appsync-java-visitor.ts +++ b/packages/appsync-modelgen-plugin/src/visitors/appsync-java-visitor.ts @@ -13,7 +13,14 @@ import { } from '../configs/java-config'; import { JAVA_TYPE_IMPORT_MAP } from '../scalars'; import { JavaDeclarationBlock } from '../languages/java-declaration-block'; -import { AppSyncModelVisitor, CodeGenField, CodeGenModel, CodeGenPrimaryKeyType, ParsedAppSyncModelConfig, RawAppSyncModelConfig } from './appsync-visitor'; +import { + AppSyncModelVisitor, + CodeGenField, + CodeGenModel, + CodeGenPrimaryKeyType, + ParsedAppSyncModelConfig, + RawAppSyncModelConfig, +} from './appsync-visitor'; import { CodeGenConnectionType } from '../utils/process-connections'; import { AuthDirective, AuthStrategy } from '../utils/process-auth'; import { printWarning } from '../utils/warn'; @@ -31,10 +38,7 @@ export class AppSyncModelJavaVisitor< const shouldUseModelNameFieldInHasManyAndBelongsTo = true; // This flag is going to be used to tight-trigger on JS implementations only. const shouldImputeKeyForUniDirectionalHasMany = false; - this.processDirectives( - shouldUseModelNameFieldInHasManyAndBelongsTo, - shouldImputeKeyForUniDirectionalHasMany - ); + this.processDirectives(shouldUseModelNameFieldInHasManyAndBelongsTo, shouldImputeKeyForUniDirectionalHasMany); if (this._parsedConfig.generate === 'loader') { return this.generateClassLoader(); @@ -377,7 +381,13 @@ export class AppSyncModelJavaVisitor< * @param classDeclaration */ protected generateIdentifierClassField(model: CodeGenModel, classDeclaration: JavaDeclarationBlock): void { - classDeclaration.addClassMember(this.getModelIdentifierClassFieldName(model), this.getModelIdentifierClassName(model), '', undefined, 'private'); + classDeclaration.addClassMember( + this.getModelIdentifierClassFieldName(model), + this.getModelIdentifierClassName(model), + '', + undefined, + 'private', + ); } /** * Generate step builder interfaces for each non-null field in the model @@ -386,11 +396,15 @@ export class AppSyncModelJavaVisitor< protected generateStepBuilderInterfaces(model: CodeGenModel, isIdAsModelPrimaryKey: boolean = true): JavaDeclarationBlock[] { const nonNullableFields = this.getWritableFields(model).filter(field => this.isRequiredField(field)); const nullableFields = this.getWritableFields(model).filter(field => !this.isRequiredField(field)); - const requiredInterfaces = nonNullableFields.filter((field: CodeGenField) => !(isIdAsModelPrimaryKey && this.READ_ONLY_FIELDS.includes(field.name))); + const requiredInterfaces = nonNullableFields.filter( + (field: CodeGenField) => !(isIdAsModelPrimaryKey && this.READ_ONLY_FIELDS.includes(field.name)), + ); + const types = this.getTypesUsedByModel(model); + const interfaces = requiredInterfaces.map((field, idx) => { const isLastField = requiredInterfaces.length - 1 === idx ? true : false; const returnType = isLastField ? 'Build' : requiredInterfaces[idx + 1].name; - const interfaceName = this.getStepInterfaceName(field.name); + const interfaceName = this.getStepInterfaceName(field.name, types); const methodName = this.getStepFunctionName(field); const argumentType = this.getNativeType(field); const argumentName = this.getStepFunctionArgumentName(field); @@ -398,14 +412,16 @@ export class AppSyncModelJavaVisitor< .asKind('interface') .withName(interfaceName) .access('public'); - interfaceDeclaration.withBlock(indent(`${this.getStepInterfaceName(returnType)} ${methodName}(${argumentType} ${argumentName});`)); + interfaceDeclaration.withBlock( + indent(`${this.getStepInterfaceName(returnType, types)} ${methodName}(${argumentType} ${argumentName});`), + ); return interfaceDeclaration; }); // Builder const builder = new JavaDeclarationBlock() .asKind('interface') - .withName(this.getStepInterfaceName('Build')) + .withName(this.getStepInterfaceName('Build', types)) .access('public'); const builderBody = []; // build method @@ -413,13 +429,13 @@ export class AppSyncModelJavaVisitor< if (isIdAsModelPrimaryKey) { // id method. Special case as this can throw exception - builderBody.push(`${this.getStepInterfaceName('Build')} id(String id);`); + builderBody.push(`${this.getStepInterfaceName('Build', types)} id(String id);`); } nullableFields.forEach(field => { const fieldName = this.getStepFunctionArgumentName(field); const methodName = this.getStepFunctionName(field); - builderBody.push(`${this.getStepInterfaceName('Build')} ${methodName}(${this.getNativeType(field)} ${fieldName});`); + builderBody.push(`${this.getStepInterfaceName('Build', types)} ${methodName}(${this.getNativeType(field)} ${fieldName});`); }); builder.withBlock(indentMultiline(builderBody.join('\n'))); @@ -434,15 +450,18 @@ export class AppSyncModelJavaVisitor< protected generateBuilderClass(model: CodeGenModel, classDeclaration: JavaDeclarationBlock, isIdAsModelPrimaryKey: boolean = true): void { const nonNullableFields = this.getWritableFields(model).filter(field => this.isRequiredField(field)); const nullableFields = this.getWritableFields(model).filter(field => !this.isRequiredField(field)); - const stepFields = nonNullableFields.filter((field: CodeGenField) => !(isIdAsModelPrimaryKey && this.READ_ONLY_FIELDS.includes(field.name))); - const stepInterfaces = stepFields.map((field: CodeGenField) => this.getStepInterfaceName(field.name)); + const stepFields = nonNullableFields.filter( + (field: CodeGenField) => !(isIdAsModelPrimaryKey && this.READ_ONLY_FIELDS.includes(field.name)), + ); + const types = this.getTypesUsedByModel(model); + const stepInterfaces = stepFields.map((field: CodeGenField) => this.getStepInterfaceName(field.name, types)); const builderClassDeclaration = new JavaDeclarationBlock() .access('public') .static() .asKind('class') .withName('Builder') - .implements([...stepInterfaces, this.getStepInterfaceName('Build')]); + .implements([...stepInterfaces, this.getStepInterfaceName('Build', types)]); // Add private instance fields [...nonNullableFields, ...nullableFields].forEach((field: CodeGenField) => { @@ -452,7 +471,9 @@ export class AppSyncModelJavaVisitor< // methods // build(); - const buildImplementation = isIdAsModelPrimaryKey ? [`String id = this.id != null ? this.id : UUID.randomUUID().toString();`, ''] : ['']; + const buildImplementation = isIdAsModelPrimaryKey + ? [`String id = this.id != null ? this.id : UUID.randomUUID().toString();`, ''] + : ['']; const buildParams = this.getWritableFields(model) .map(field => this.getFieldName(field)) .join(',\n'); @@ -473,7 +494,7 @@ export class AppSyncModelJavaVisitor< const isLastStep = idx === fields.length - 1; const fieldName = this.getFieldName(field); const methodName = this.getStepFunctionName(field); - const returnType = isLastStep ? this.getStepInterfaceName('Build') : this.getStepInterfaceName(fields[idx + 1].name); + const returnType = isLastStep ? this.getStepInterfaceName('Build', types) : this.getStepInterfaceName(fields[idx + 1].name, types); const argumentType = this.getNativeType(field); const argumentName = this.getStepFunctionArgumentName(field); const body = [`Objects.requireNonNull(${argumentName});`, `this.${fieldName} = ${argumentName};`, `return this;`].join('\n'); @@ -493,7 +514,7 @@ export class AppSyncModelJavaVisitor< nullableFields.forEach((field: CodeGenField) => { const fieldName = this.getFieldName(field); const methodName = this.getStepFunctionName(field); - const returnType = this.getStepInterfaceName('Build'); + const returnType = this.getStepInterfaceName('Build', types); const argumentType = this.getNativeType(field); const argumentName = this.getStepFunctionArgumentName(field); const body = [`this.${fieldName} = ${argumentName};`, `return this;`].join('\n'); @@ -520,7 +541,7 @@ export class AppSyncModelJavaVisitor< builderClassDeclaration.addClassMethod( 'id', - this.getStepInterfaceName('Build'), + this.getStepInterfaceName('Build', types), indentMultiline(idBuildStepBody), [{ name: 'id', type: 'String' }], [], @@ -541,7 +562,11 @@ export class AppSyncModelJavaVisitor< * @param model * @param classDeclaration */ - protected generateCopyOfBuilderClass(model: CodeGenModel, classDeclaration: JavaDeclarationBlock, isIdAsModelPrimaryKey: boolean = true): void { + protected generateCopyOfBuilderClass( + model: CodeGenModel, + classDeclaration: JavaDeclarationBlock, + isIdAsModelPrimaryKey: boolean = true, + ): void { const builderName = 'CopyOfBuilder'; const copyOfBuilderClassDeclaration = new JavaDeclarationBlock() .access('public') @@ -616,11 +641,11 @@ export class AppSyncModelJavaVisitor< primaryKeyClassDeclaration.addClassMember('serialVersionUID', 'long', '1L', [], 'private', { static: true, final: true }); // constructor const primaryKeyComponentFields: CodeGenField[] = [primaryKeyField, ...sortKeyFields]; - const constructorParams = primaryKeyComponentFields.map(field => ({name: this.getFieldName(field), type: this.getNativeType(field)})); + const constructorParams = primaryKeyComponentFields.map(field => ({ name: this.getFieldName(field), type: this.getNativeType(field) })); const constructorImpl = `super(${primaryKeyComponentFields.map(field => this.getFieldName(field)).join(', ')});`; primaryKeyClassDeclaration.addClassMethod(modelPrimaryKeyClassName, null, constructorImpl, constructorParams, [], 'public'); classDeclaration.nestedClass(primaryKeyClassDeclaration); -} + } /** * adds a copyOfBuilder method to the Model class. This method is used to create a copy of the model to mutate it @@ -647,12 +672,27 @@ export class AppSyncModelJavaVisitor< const body = isCompositeKey ? [ `if (${modelIdentifierClassFieldName} == null) {`, - indent(`this.${modelIdentifierClassFieldName} = new ${this.getModelIdentifierClassName(model)}(${[primaryKeyField, ...sortKeyFields].map(f => this.getFieldName(f)).join(', ')});`), + indent( + `this.${modelIdentifierClassFieldName} = new ${this.getModelIdentifierClassName(model)}(${[primaryKeyField, ...sortKeyFields] + .map(f => this.getFieldName(f)) + .join(', ')});`, + ), '}', - `return ${modelIdentifierClassFieldName};` + `return ${modelIdentifierClassFieldName};`, ].join('\n') : `return ${this.getFieldName(primaryKeyField)};`; - declarationsBlock.addClassMethod('resolveIdentifier', returnType, body, [], [], 'public', {}, ["Deprecated"], [], "@deprecated This API is internal to Amplify and should not be used."); + declarationsBlock.addClassMethod( + 'resolveIdentifier', + returnType, + body, + [], + [], + 'public', + {}, + ['Deprecated'], + [], + '@deprecated This API is internal to Amplify and should not be used.', + ); } /** @@ -823,11 +863,18 @@ export class AppSyncModelJavaVisitor< * @param model * @param classDeclaration */ - protected generateBuilderMethod(model: CodeGenModel, classDeclaration: JavaDeclarationBlock, isIdAsModelPrimaryKey: boolean = true): void { + protected generateBuilderMethod( + model: CodeGenModel, + classDeclaration: JavaDeclarationBlock, + isIdAsModelPrimaryKey: boolean = true, + ): void { const requiredFields = this.getWritableFields(model).filter( field => !field.isNullable && !(isIdAsModelPrimaryKey && this.READ_ONLY_FIELDS.includes(field.name)), ); - const returnType = requiredFields.length ? this.getStepInterfaceName(requiredFields[0].name) : this.getStepInterfaceName('Build'); + const types = this.getTypesUsedByModel(model); + const returnType = requiredFields.length + ? this.getStepInterfaceName(requiredFields[0].name, types) + : this.getStepInterfaceName('Build', types); classDeclaration.addClassMethod( 'builder', returnType, @@ -843,10 +890,27 @@ export class AppSyncModelJavaVisitor< /** * Generate the name of the step builder interface * @param nextFieldName: string + * @param types: string - set of types for all fields on model * @returns string */ - private getStepInterfaceName(nextFieldName: string): string { - return `${pascalCase(nextFieldName)}Step`; + private getStepInterfaceName(nextFieldName: string, types: Set): string { + const pascalCaseFieldName = pascalCase(nextFieldName); + const stepInterfaceName = `${pascalCaseFieldName}Step`; + + if (types.has(stepInterfaceName)) { + return `${pascalCaseFieldName}BuildStep`; + } + + return stepInterfaceName; + } + + /** + * Get set of all types used by a model + * @param model + * @return Set + */ + private getTypesUsedByModel(model: CodeGenModel): Set { + return new Set(model.fields.map(field => field.type)); } protected generateModelAnnotations(model: CodeGenModel): string[] { From 8279f35d84cb10f0df3c4fb0f4a141f86dbc3e60 Mon Sep 17 00:00:00 2001 From: Al Harris <91494052+alharris-at@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:46:53 -0700 Subject: [PATCH 02/11] feat: support running codegen without an initialized amplify backend set up locally. (#702) * feat: codegen add --region (#683) * fix: download introspection schema when apiId is passed (#684) * fix: codegen --apiId broken state (#689) * chore: get existing e2e tests passing (#704) * feat: add e2e tests for codegen add when in a non-amplify project (#705) * feat: add e2e tests for codegen add when in a non-amplify project * chore: add some additional test cases * noinit codegen from graphqlconfig (#706) * fix: add missing awaits * chore: fail typegen if no queries are found --------- Co-authored-by: Dane Pilcher --- .codebuild/e2e_workflow.yml | 9 +- .../src/categories/codegen.ts | 102 +- .../schemas/sdl/schema.graphql | 11 + .../schemas/sdl/schema.json | 1163 +++++++++++++++++ .../src/__tests__/add-codegen-js.test.ts | 46 +- .../uninitialized-project-codegen-js.test.ts | 114 ++ .../src/codegen-tests-base/add-codegen.ts | 125 +- .../amplify-codegen/commands/codegen/add.js | 10 +- packages/amplify-codegen/package.json | 2 +- packages/amplify-codegen/src/commands/add.js | 63 +- .../src/commands/generateStatementsAndType.js | 31 +- .../src/commands/statements.js | 31 +- .../amplify-codegen/src/commands/types.js | 31 +- packages/amplify-codegen/src/constants.js | 9 +- .../src/utils/downloadIntrospectionSchema.js | 6 +- .../src/utils/ensureIntrospectionSchema.js | 7 +- .../src/utils/getAppSyncAPIInfoFromProject.js | 15 + packages/amplify-codegen/src/utils/index.js | 2 + .../amplify-codegen/tests/cli/add.test.js | 144 ++ .../commands/__snapshots__/add.test.js.snap | 73 ++ .../tests/commands/add.test.js | 134 +- 21 files changed, 1990 insertions(+), 138 deletions(-) create mode 100644 packages/amplify-codegen-e2e-tests/schemas/sdl/schema.graphql create mode 100644 packages/amplify-codegen-e2e-tests/schemas/sdl/schema.json create mode 100644 packages/amplify-codegen-e2e-tests/src/__tests__/uninitialized-project-codegen-js.test.ts create mode 100644 packages/amplify-codegen/src/utils/getAppSyncAPIInfoFromProject.js create mode 100644 packages/amplify-codegen/tests/cli/add.test.js create mode 100644 packages/amplify-codegen/tests/commands/__snapshots__/add.test.js.snap diff --git a/.codebuild/e2e_workflow.yml b/.codebuild/e2e_workflow.yml index 9019bd5a2..e75a50bb1 100644 --- a/.codebuild/e2e_workflow.yml +++ b/.codebuild/e2e_workflow.yml @@ -121,22 +121,23 @@ batch: depend-on: - publish_to_local_registry - identifier: >- - build_app_ts_uninitialized_project_modelgen_android_uninitialized_project_modelgen_flutter_uninitialized_project_modelgen_ios + build_app_ts_uninitialized_project_codegen_js_uninitialized_project_modelgen_android_uninitialized_project_modelgen_flutter buildspec: .codebuild/run_e2e_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: TEST_SUITE: >- - src/__tests__/build-app-ts.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts|src/__tests__/uninitialized-project-modelgen-flutter.test.ts|src/__tests__/uninitialized-project-modelgen-ios.test.ts + src/__tests__/build-app-ts.test.ts|src/__tests__/uninitialized-project-codegen-js.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts|src/__tests__/uninitialized-project-modelgen-flutter.test.ts CLI_REGION: ap-southeast-1 depend-on: - publish_to_local_registry - - identifier: uninitialized_project_modelgen_js + - identifier: uninitialized_project_modelgen_ios_uninitialized_project_modelgen_js buildspec: .codebuild/run_e2e_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: - TEST_SUITE: src/__tests__/uninitialized-project-modelgen-js.test.ts + TEST_SUITE: >- + src/__tests__/uninitialized-project-modelgen-ios.test.ts|src/__tests__/uninitialized-project-modelgen-js.test.ts CLI_REGION: ap-southeast-2 depend-on: - publish_to_local_registry diff --git a/packages/amplify-codegen-e2e-core/src/categories/codegen.ts b/packages/amplify-codegen-e2e-core/src/categories/codegen.ts index 0740e90f3..ab5a83a1f 100644 --- a/packages/amplify-codegen-e2e-core/src/categories/codegen.ts +++ b/packages/amplify-codegen-e2e-core/src/categories/codegen.ts @@ -30,9 +30,40 @@ export const generateModelsWithOptions = (cwd: string, options: Record { +export function generateStatementsAndTypes(cwd: string, errorMessage?: string) : Promise { return new Promise((resolve, reject) => { - spawn(getCLIPath(), ['codegen'], { cwd, stripColors: true }) + const chain = spawn(getCLIPath(), ['codegen'], { cwd, stripColors: true }) + + if (errorMessage) { + chain.wait(errorMessage); + } + + return chain.run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }) + }); +} + +export function generateStatements(cwd: string) : Promise { + return new Promise((resolve, reject) => { + spawn(getCLIPath(), ['codegen', 'statements'], { cwd, stripColors: true }) + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }) + }); +} + +export function generateTypes(cwd: string) : Promise { + return new Promise((resolve, reject) => { + spawn(getCLIPath(), ['codegen', 'types'], { cwd, stripColors: true }) .run((err: Error) => { if (!err) { resolve(); @@ -46,7 +77,10 @@ export function generateStatementsAndTypes(cwd: string) : Promise { // CLI workflow to add codegen to Amplify project export function addCodegen(cwd: string, settings: any = {}): Promise { return new Promise((resolve, reject) => { - const chain = spawn(getCLIPath(), ['codegen', 'add'], { cwd, stripColors: true }); + const params = settings.params + ? ['codegen', 'add', ...settings.params] + : ['codegen', 'add']; + const chain = spawn(getCLIPath(), params, { cwd, stripColors: true }); if (settings.isAPINotAdded) { chain.wait("There are no GraphQL APIs available."); chain.wait("Add by running $amplify api add"); @@ -166,22 +200,26 @@ export function generateModelIntrospection(cwd: string, settings: { outputDir?: } // CLI workflow to add codegen to non-Amplify JS project -export function addCodegenNonAmplifyJS(cwd: string): Promise { +export function addCodegenNonAmplifyJS(cwd: string, params: Array, initialFailureMessage?: string): Promise { return new Promise((resolve, reject) => { - const cmdOptions = ['codegen', 'add', '--apiId', 'mockapiid']; - const chain = spawn(getCLIPath(), cmdOptions, { cwd, stripColors: true }); - chain - .wait("Choose the type of app that you're building") - .sendCarriageReturn() - .wait('What javascript framework are you using') - .sendCarriageReturn() - .wait('Choose the code generation language target').sendCarriageReturn() - .wait('Enter the file name pattern of graphql queries, mutations and subscriptions') - .sendCarriageReturn() - .wait('Do you want to generate/update all possible GraphQL operations') - .sendLine('y') - .wait('Enter maximum statement depth [increase from default if your schema is deeply') - .sendCarriageReturn(); + const chain = spawn(getCLIPath(), ['codegen', 'add', ...params], { cwd, stripColors: true }); + + if (initialFailureMessage) { + chain.wait(initialFailureMessage) + } else { + chain + .wait("Choose the type of app that you're building") + .sendCarriageReturn() + .wait('What javascript framework are you using') + .sendCarriageReturn() + .wait('Choose the code generation language target').sendCarriageReturn() + .wait('Enter the file name pattern of graphql queries, mutations and subscriptions') + .sendCarriageReturn() + .wait('Do you want to generate/update all possible GraphQL operations') + .sendLine('y') + .wait('Enter maximum statement depth [increase from default if your schema is deeply') + .sendCarriageReturn(); + } chain.run((err: Error) => { if (!err) { @@ -192,3 +230,31 @@ export function addCodegenNonAmplifyJS(cwd: string): Promise { }); }); } + +export function addCodegenNonAmplifyTS(cwd: string, params: Array, initialFailureMessage?: string): Promise { + return new Promise((resolve, reject) => { + const chain = spawn(getCLIPath(), ['codegen', 'add', ...params], { cwd, stripColors: true }); + + if (initialFailureMessage) { + chain.wait(initialFailureMessage) + } else { + chain + .wait("Choose the type of app that you're building").sendCarriageReturn() + .wait('What javascript framework are you using').sendCarriageReturn() + .wait('Choose the code generation language target').sendKeyDown().sendCarriageReturn() + .wait('Enter the file name pattern of graphql queries, mutations and subscriptions').sendCarriageReturn() + .wait('Do you want to generate/update all possible GraphQL operations').sendLine('y') + .wait('Enter maximum statement depth [increase from default if your schema is deeply').sendCarriageReturn() + .wait('Enter the file name for the generated code').sendCarriageReturn() + .wait('Do you want to generate code for your newly created GraphQL API').sendCarriageReturn(); + } + + chain.run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} \ No newline at end of file diff --git a/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.graphql b/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.graphql new file mode 100644 index 000000000..19f1eb1b3 --- /dev/null +++ b/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.graphql @@ -0,0 +1,11 @@ +type Query { + echo: String +} + +type Mutation { + mymutation: String +} + +type Subscription { + mysub: String +} diff --git a/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.json b/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.json new file mode 100644 index 000000000..b133d772b --- /dev/null +++ b/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.json @@ -0,0 +1,1163 @@ +{ + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": { + "name": "Subscription" + }, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "echo", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "Built-in String", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": null, + "fields": [ + { + "name": "mymutation", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Subscription", + "description": null, + "fields": [ + { + "name": "mysub", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Introspection defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "'A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "'If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": null, + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given __Type is", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "Built-in Boolean", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onOperation", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onFragment", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onField", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "An enum describing valid locations where a directive can be placed", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Indicates the directive is valid on queries.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Indicates the directive is valid on mutations.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Indicates the directive is valid on fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Indicates the directive is valid on fragment definitions.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Indicates the directive is valid on fragment spreads.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Indicates the directive is valid on inline fragments.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Indicates the directive is valid on a schema SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Indicates the directive is valid on a scalar SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates the directive is valid on an object SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Indicates the directive is valid on a field SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Indicates the directive is valid on a field argument SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates the directive is valid on an interface SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates the directive is valid on an union SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates the directive is valid on an enum SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Indicates the directive is valid on an enum value SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates the directive is valid on an input object SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Indicates the directive is valid on an input object field SDL definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": true, + "onField": true + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if`'argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": true, + "onField": true + }, + { + "name": "defer", + "description": "This directive allows results to be deferred during execution", + "locations": [ + "FIELD" + ], + "args": [], + "onOperation": false, + "onFragment": false, + "onField": true + }, + { + "name": "aws_api_key", + "description": "Tells the service this field/object has access authorized by an API key.", + "locations": [ + "OBJECT", + "FIELD_DEFINITION" + ], + "args": [], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_iam", + "description": "Tells the service this field/object has access authorized by sigv4 signing.", + "locations": [ + "OBJECT", + "FIELD_DEFINITION" + ], + "args": [], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_oidc", + "description": "Tells the service this field/object has access authorized by an OIDC token.", + "locations": [ + "OBJECT", + "FIELD_DEFINITION" + ], + "args": [], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_lambda", + "description": "Tells the service this field/object has access authorized by a Lambda Authorizer.", + "locations": [ + "OBJECT", + "FIELD_DEFINITION" + ], + "args": [], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_subscribe", + "description": "Tells the service which mutation triggers this subscription.", + "locations": [ + "FIELD_DEFINITION" + ], + "args": [ + { + "name": "mutations", + "description": "List of mutations which will trigger this subscription when they are called.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_cognito_user_pools", + "description": "Tells the service this field/object has access authorized by a Cognito User Pools token.", + "locations": [ + "OBJECT", + "FIELD_DEFINITION" + ], + "args": [ + { + "name": "cognito_groups", + "description": "List of cognito user pool groups which have access on this field", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "deprecated", + "description": null, + "locations": [ + "FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"" + } + ], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_auth", + "description": "Directs the schema to enforce authorization on a field", + "locations": [ + "FIELD_DEFINITION" + ], + "args": [ + { + "name": "cognito_groups", + "description": "List of cognito user pool groups which have access on this field", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_publish", + "description": "Tells the service which subscriptions will be published to when this mutation is called. This directive is deprecated use @aws_susbscribe directive instead.", + "locations": [ + "FIELD_DEFINITION" + ], + "args": [ + { + "name": "subscriptions", + "description": "List of subscriptions which will be published to when this mutation is called.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": false, + "onField": false + } + ] + } + } +} \ No newline at end of file diff --git a/packages/amplify-codegen-e2e-tests/src/__tests__/add-codegen-js.test.ts b/packages/amplify-codegen-e2e-tests/src/__tests__/add-codegen-js.test.ts index a78fbc75a..2a23c972d 100644 --- a/packages/amplify-codegen-e2e-tests/src/__tests__/add-codegen-js.test.ts +++ b/packages/amplify-codegen-e2e-tests/src/__tests__/add-codegen-js.test.ts @@ -6,13 +6,17 @@ import { createRandomName, addApiWithoutSchema, updateApiSchema, - addCodegenNonAmplifyJS } from "@aws-amplify/amplify-codegen-e2e-core"; -import { existsSync, writeFileSync } from "fs"; +import { existsSync } from "fs"; import path from 'path'; import { isNotEmptyDir } from '../utils'; -import { deleteAmplifyProject, testAddCodegen, testSetupBeforeAddCodegen, -getGraphQLConfigFilePath, testValidGraphQLConfig } from '../codegen-tests-base'; +import { + deleteAmplifyProject, + testAddCodegen, + testSetupBeforeAddCodegen, + getGraphQLConfigFilePath, + testValidGraphQLConfig, +} from '../codegen-tests-base'; const schema = 'simple_model.graphql'; @@ -74,35 +78,7 @@ describe('codegen add tests - JS', () => { await testAddCodegen(config, projectRoot, schema); }); - it(`Adding codegen outside of Amplify project`, async () => { - // init project and add API category - const testSchema = ` - type Query { - echo: String - } - - type Mutation { - mymutation: String - } - - type Subscription { - mysub: String - } - `; - - // Setup the non-amplify project with schema and pre-existing files - const userSourceCodePath = testSetupBeforeAddCodegen(projectRoot, config); - const schemaPath = path.join(projectRoot, 'schema.graphql'); - writeFileSync(schemaPath, testSchema); - - // add codegen without init - await expect(addCodegenNonAmplifyJS(projectRoot)).resolves.not.toThrow(); - - // pre-existing file should still exist - expect(existsSync(userSourceCodePath)).toBe(true); - // GraphQL statements are generated - expect(isNotEmptyDir(path.join(projectRoot, config.graphqlCodegenDir))).toBe(true); - // graphql configuration should be added - expect(existsSync(getGraphQLConfigFilePath(projectRoot))).toBe(true); + it(`supports add codegen with redundant region parameter`, async () => { + await testAddCodegen(config, projectRoot, schema, ['--region', 'us-fake-1']); }); -}); \ No newline at end of file +}); diff --git a/packages/amplify-codegen-e2e-tests/src/__tests__/uninitialized-project-codegen-js.test.ts b/packages/amplify-codegen-e2e-tests/src/__tests__/uninitialized-project-codegen-js.test.ts new file mode 100644 index 000000000..076e7b952 --- /dev/null +++ b/packages/amplify-codegen-e2e-tests/src/__tests__/uninitialized-project-codegen-js.test.ts @@ -0,0 +1,114 @@ +import { createNewProjectDir, DEFAULT_JS_CONFIG, AmplifyFrontend, generateStatementsAndTypes } from '@aws-amplify/amplify-codegen-e2e-core'; +import path from 'path'; +import { deleteAmplifyProject, testAddCodegenUninitialized } from '../codegen-tests-base'; +import { rmSync } from "fs-extra"; + +describe('codegen add tests - JS', () => { + let projectRoot: string; + const javascriptConfig = DEFAULT_JS_CONFIG; + const typescriptConfig = { + ...DEFAULT_JS_CONFIG, + frontendType: AmplifyFrontend.typescript, + }; + + beforeEach(async () => { + projectRoot = await createNewProjectDir('uninitializedProjectCodegenJS'); + }); + + afterEach(async () => { + await deleteAmplifyProject(projectRoot); + }); + + it(`graphql sdl file`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: javascriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.js', 'queries.js', 'subscriptions.js'], + }); + }); + + it(`region is ignored if schema file is provided`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: javascriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.js', 'queries.js', 'subscriptions.js'], + additionalParams: ['--region', 'us-fake-1'], + }); + }); + + it(`json sdl file`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: javascriptConfig, + sdlFilename: 'schema.json', + expectedFilenames: ['mutations.js', 'queries.js', 'subscriptions.js'], + }); + }); + + it(`typescript`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: typescriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.ts', 'queries.ts', 'subscriptions.ts'], + }); + }); + + it(`drop and regenerate`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: typescriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.ts', 'queries.ts', 'subscriptions.ts'], + dropAndRunCodegen: true, + }); + }); + + it(`drop and regenerate statements`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: typescriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.ts', 'queries.ts', 'subscriptions.ts'], + dropAndRunCodegenStatements: true, + }); + }); + + it(`drop and regenerate types`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: typescriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.ts', 'queries.ts', 'subscriptions.ts'], + dropAndRunCodegenStatements: true, + dropAndRunCodegenTypes: true, + }); + }); + + it(`throws a sane warning on missing graphqlconfig file`, async () => { + // Add codegen + await testAddCodegenUninitialized({ + projectRoot, + config: javascriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.js', 'queries.js', 'subscriptions.js'], + }); + + // Remove .graphqlconfig.yml file + rmSync(path.join(projectRoot, '.graphqlconfig.yml')); + + // Run and expect failure message + await generateStatementsAndTypes(projectRoot, 'code generation is not configured'); + }); + + it(`throws a sane warning on missing sdl schema and no api id specified`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: javascriptConfig, + expectedFilenames: ['mutations.js', 'queries.js', 'subscriptions.js'], + initialFailureMessage: 'Provide an AppSync API ID with --apiId or manually download schema.graphql or schema.json' + }); + }); +}); diff --git a/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/add-codegen.ts b/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/add-codegen.ts index 9df7e9d96..4ae362489 100644 --- a/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/add-codegen.ts +++ b/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/add-codegen.ts @@ -4,14 +4,20 @@ import { updateApiSchema, addCodegen, AmplifyFrontendConfig, - createRandomName + createRandomName, + addCodegenNonAmplifyJS, + addCodegenNonAmplifyTS, + AmplifyFrontend, + generateStatementsAndTypes, + generateStatements, + generateTypes, } from "@aws-amplify/amplify-codegen-e2e-core"; -import { existsSync } from "fs"; +import { existsSync, readFileSync, writeFileSync, readdirSync, rmSync } from "fs"; import path from 'path'; import { isNotEmptyDir } from '../utils'; -import { testSetupBeforeAddCodegen, testValidGraphQLConfig } from "./test-setup"; +import { getGraphQLConfigFilePath, testSetupBeforeAddCodegen, testValidGraphQLConfig } from "./test-setup"; -export async function testAddCodegen(config: AmplifyFrontendConfig, projectRoot: string, schema: string) { +export async function testAddCodegen(config: AmplifyFrontendConfig, projectRoot: string, schema: string, additionalParams?: Array) { // init project and add API category await initProjectWithProfile(projectRoot, { ...config }); const projectName = createRandomName(); @@ -21,7 +27,7 @@ export async function testAddCodegen(config: AmplifyFrontendConfig, projectRoot: const userSourceCodePath = testSetupBeforeAddCodegen(projectRoot, config); // add codegen succeeds - await expect(addCodegen(projectRoot, { ...config })).resolves.not.toThrow(); + await expect(addCodegen(projectRoot, { ...config, params: additionalParams ?? [] })).resolves.not.toThrow(); // pre-existing file should still exist expect(existsSync(userSourceCodePath)).toBe(true); @@ -29,3 +35,112 @@ export async function testAddCodegen(config: AmplifyFrontendConfig, projectRoot: expect(isNotEmptyDir(path.join(projectRoot, config.graphqlCodegenDir))).toBe(true); testValidGraphQLConfig(projectRoot, config); } + +export type TestAddCodegenUninitializedProps = { + config: AmplifyFrontendConfig; + projectRoot: string; + sdlFilename?: string; + expectedFilenames: Array; + dropAndRunCodegen?: boolean; + dropAndRunCodegenStatements?: boolean; + dropAndRunCodegenTypes?: boolean; + initialFailureMessage?: string; + additionalParams?: Array; +}; + +const assertTypeFileExists = (projectRoot: string): void => { + expect(existsSync(path.join(projectRoot, 'src', 'API.ts'))).toBe(true) +}; + +/** + * Ensure that all values provided in the expected set are present in the received set, allowing for additional values in received. + * @param expectedValues the expected values to check + * @param receivedValues the received values to check + */ +const ensureAllExpectedValuesAreReceived = (expectedValues: Array, receivedValues: Array): void => { + const receivedValueSet = new Set(receivedValues); + console.log(`Comparing received values: ${JSON.stringify(receivedValues)} to expected values: ${JSON.stringify(expectedValues)}`); + expectedValues.forEach((expectedFilename) => expect(receivedValueSet.has(expectedFilename)).toBe(true)); +}; + +export async function testAddCodegenUninitialized({ + config, + projectRoot, + sdlFilename, + expectedFilenames, + dropAndRunCodegen, + dropAndRunCodegenStatements, + dropAndRunCodegenTypes, + initialFailureMessage, + additionalParams, +}: TestAddCodegenUninitializedProps) { + // Setup the non-amplify project with schema and pre-existing files + const userSourceCodePath = testSetupBeforeAddCodegen(projectRoot, config); + + // Write SDL Schema + if (sdlFilename) { + const sdlSchema = readFileSync(path.join(__dirname, '..', '..', 'schemas', 'sdl', sdlFilename), 'utf-8'); + writeFileSync(path.join(projectRoot, sdlFilename), sdlSchema); + } + + // add codegen without init + switch (config.frontendType) { + case AmplifyFrontend.javascript: + await addCodegenNonAmplifyJS(projectRoot, additionalParams ?? [], initialFailureMessage); + break; + case AmplifyFrontend.typescript: + await addCodegenNonAmplifyTS(projectRoot, additionalParams ?? [], initialFailureMessage); + break; + default: + throw new Error(`Received unexpected frontendType ${config.frontendType}`); + } + + // return if we expected the add command to fail + if (initialFailureMessage) { + return; + } + + // pre-existing file should still exist + expect(existsSync(userSourceCodePath)).toBe(true); + // GraphQL statements are generated + ensureAllExpectedValuesAreReceived(expectedFilenames, readdirSync(path.join(projectRoot, config.graphqlCodegenDir))) + // graphql configuration should be added + expect(existsSync(getGraphQLConfigFilePath(projectRoot))).toBe(true); + if (config.frontendType === AmplifyFrontend.typescript) { + assertTypeFileExists(projectRoot) + } + + if (dropAndRunCodegen || dropAndRunCodegenStatements || dropAndRunCodegenTypes) { + rmSync(path.join(projectRoot, config.graphqlCodegenDir), { recursive: true }); + // pre-existing file should still exist + expect(existsSync(userSourceCodePath)).toBe(true); + // Graphql statements are deleted + expect(existsSync(path.join(projectRoot, config.graphqlCodegenDir))).toBe(false); + } + + if (dropAndRunCodegen) { + await generateStatementsAndTypes(projectRoot); + + // GraphQL statements are regenerated + ensureAllExpectedValuesAreReceived(expectedFilenames, readdirSync(path.join(projectRoot, config.graphqlCodegenDir))) + + if (config.frontendType === AmplifyFrontend.typescript) { + assertTypeFileExists(projectRoot) + } + } + + if (dropAndRunCodegenStatements) { + await generateStatements(projectRoot); + + // GraphQL statements are regenerated + ensureAllExpectedValuesAreReceived(expectedFilenames, readdirSync(path.join(projectRoot, config.graphqlCodegenDir))) + } + + if (dropAndRunCodegenTypes) { + await generateTypes(projectRoot); + + if (config.frontendType === AmplifyFrontend.typescript) { + assertTypeFileExists(projectRoot) + } + } +} diff --git a/packages/amplify-codegen/commands/codegen/add.js b/packages/amplify-codegen/commands/codegen/add.js index 89ac18581..0a64e45fd 100644 --- a/packages/amplify-codegen/commands/codegen/add.js +++ b/packages/amplify-codegen/commands/codegen/add.js @@ -9,14 +9,16 @@ module.exports = { try { const { options = {} } = context.parameters; const keys = Object.keys(options); - if (keys.length && !keys.includes('apiId')) { - const paramMsg = keys.length > 1 ? 'Invalid parameters ' : 'Invalid parameter '; + // frontend and framework are undocumented, but are read when apiId is also supplied + const { apiId = null, region, yes, frontend, framework, debug, ...rest } = options; + const extraOptions = Object.keys(rest); + if (extraOptions.length) { + const paramMsg = extraOptions.length > 1 ? 'Invalid parameters' : 'Invalid parameter'; context.print.info(`${paramMsg} ${keys.join(', ')}`); context.print.info(constants.INFO_MESSAGE_ADD_ERROR); return; } - const apiId = context.parameters.options.apiId || null; - await codeGen.add(context, apiId); + await codeGen.add(context, apiId, region); } catch (ex) { context.print.error(ex.message); } diff --git a/packages/amplify-codegen/package.json b/packages/amplify-codegen/package.json index ee71e86bb..296e27e59 100644 --- a/packages/amplify-codegen/package.json +++ b/packages/amplify-codegen/package.json @@ -46,7 +46,7 @@ "coverageThreshold": { "global": { "branches": 54, - "functions": 65, + "functions": 64, "lines": 72 } }, diff --git a/packages/amplify-codegen/src/commands/add.js b/packages/amplify-codegen/src/commands/add.js index 713e3194b..6b3a609a9 100644 --- a/packages/amplify-codegen/src/commands/add.js +++ b/packages/amplify-codegen/src/commands/add.js @@ -1,4 +1,5 @@ const Ora = require('ora'); +const process = require('process'); const { loadConfig } = require('../codegen-config'); const constants = require('../constants'); const generateStatements = require('./statements'); @@ -23,11 +24,11 @@ const askForFramework = require('../walkthrough/questions/selectFramework'); const frontends = ['android', 'ios', 'javascript']; const frameworks = ['angular', 'ember', 'ionic', 'react', 'react-native', 'vue', 'none']; -async function add(context, apiId = null) { +async function add(context, apiId = null, region = 'us-east-1') { let withoutInit = false; // Determine if working in an amplify project try { - context.amplify.getProjectMeta(); + await context.amplify.getProjectMeta(); } catch (e) { withoutInit = true; const config = loadConfig(context, withoutInit); @@ -37,9 +38,9 @@ async function add(context, apiId = null) { } const schemaPath = ['schema.graphql', 'schema.json'].map(p => path.join(process.cwd(), p)).find(p => fs.existsSync(p)); - if (withoutInit && !schemaPath) { + if (withoutInit && !(apiId || schemaPath)) { throw Error( - `Please download schema.graphql or schema.json and place in ${process.cwd()} before adding codegen when not in an amplify project`, + `Provide an AppSync API ID with --apiId or manually download schema.graphql or schema.json and place in ${process.cwd()} before adding codegen when not in an amplify project`, ); } // Grab the frontend @@ -69,7 +70,6 @@ async function add(context, apiId = null) { } } - let region = 'us-east-1'; if (!withoutInit) { region = getProjectAwsRegion(context); } @@ -78,36 +78,37 @@ async function add(context, apiId = null) { throw new Error(constants.ERROR_CODEGEN_SUPPORT_MAX_ONE_API); } let apiDetails; - if (!withoutInit) { - if (!apiId) { - const availableAppSyncApis = getAppSyncAPIDetails(context); // published and un-published - if (availableAppSyncApis.length === 0) { - throw new NoAppSyncAPIAvailableError(constants.ERROR_CODEGEN_NO_API_AVAILABLE); - } - [apiDetails] = availableAppSyncApis; - apiDetails.isLocal = true; - } else { - let shouldRetry = true; - while (shouldRetry) { - const apiDetailSpinner = new Ora(); - try { - apiDetailSpinner.start('Getting API details'); - apiDetails = await getAppSyncAPIInfo(context, apiId, region); - apiDetailSpinner.succeed(); + if (!withoutInit && !apiId) { + const availableAppSyncApis = getAppSyncAPIDetails(context); // published and un-published + if (availableAppSyncApis.length === 0) { + throw new NoAppSyncAPIAvailableError(constants.ERROR_CODEGEN_NO_API_AVAILABLE); + } + [apiDetails] = availableAppSyncApis; + apiDetails.isLocal = true; + } else if (apiId) { + let shouldRetry = true; + while (shouldRetry) { + const apiDetailSpinner = new Ora(); + try { + apiDetailSpinner.start('Getting API details'); + apiDetails = await getAppSyncAPIInfo(context, apiId, region); + if (!withoutInit) { await updateAmplifyMeta(context, apiDetails); - break; - } catch (e) { - apiDetailSpinner.fail(); - if (e instanceof AmplifyCodeGenAPINotFoundError) { - context.print.info(`AppSync API was not found in region ${region}`); - ({ shouldRetry, region } = await changeAppSyncRegion(context, region)); - } else { - throw e; - } + } + apiDetailSpinner.succeed(); + break; + } catch (e) { + apiDetailSpinner.fail(); + if (e instanceof AmplifyCodeGenAPINotFoundError) { + context.print.info(`AppSync API was not found in region ${region}`); + ({ shouldRetry, region } = await changeAppSyncRegion(context, region)); + } else { + throw e; } } } } + // else no appsync API, but has schema.graphql or schema.json if (!withoutInit && !apiDetails) { return; @@ -123,6 +124,8 @@ async function add(context, apiId = null) { } else { schema = getSDLSchemaLocation(apiDetails.name); } + } else if (apiDetails) { + schema = await downloadIntrospectionSchemaWithProgress(context, apiDetails.id, path.join(process.cwd(), 'schema.json'), region); } else { schema = schemaPath; } diff --git a/packages/amplify-codegen/src/commands/generateStatementsAndType.js b/packages/amplify-codegen/src/commands/generateStatementsAndType.js index 92459dbdc..f04b9c240 100644 --- a/packages/amplify-codegen/src/commands/generateStatementsAndType.js +++ b/packages/amplify-codegen/src/commands/generateStatementsAndType.js @@ -4,7 +4,7 @@ const generateTypes = require('./types'); const generateStatements = require('./statements'); const { loadConfig } = require('../codegen-config'); const constants = require('../constants'); -const { ensureIntrospectionSchema, getAppSyncAPIDetails } = require('../utils'); +const { ensureIntrospectionSchema, getAppSyncAPIDetails, getAppSyncAPIInfoFromProject } = require('../utils'); const path = require('path'); const fs = require('fs-extra'); @@ -17,30 +17,31 @@ async function generateStatementsAndTypes(context, forceDownloadSchema, maxDepth withoutInit = true; } - // Check if introspection schema exists - const schemaPath = ['schema.graphql', 'schema.json'].map(p => path.join(process.cwd(), p)).find(p => fs.existsSync(p)); - if (withoutInit && !schemaPath) { - throw Error( - `Please download the schema.graphql or schema.json and place in ${process.cwd()} before adding codegen when not in an amplify project` - ); - } - - if (withoutInit) { - forceDownloadSchema = false; - } const config = loadConfig(context, withoutInit); const projects = config.getProjects(); if (!projects.length) { throw new NoAppSyncAPIAvailableError(constants.ERROR_CODEGEN_NO_API_CONFIGURED); } + const project = projects[0]; + const schemaPath = ['schema.graphql', 'schema.json'].map(p => path.join(process.cwd(), p)).find(p => fs.existsSync(p)); + if (withoutInit && !project && !schemaPath) { + throw Error( + `Please download the schema.graphql or schema.json and place in ${process.cwd()} before adding codegen when not in an amplify project`, + ); + } + let apis = []; if (!withoutInit) { apis = getAppSyncAPIDetails(context); + } else { + const api = await getAppSyncAPIInfoFromProject(context, project); + if (api) { + apis = [api]; + } } if (!apis.length && !withoutInit) { throw new NoAppSyncAPIAvailableError(constants.ERROR_CODEGEN_NO_API_META); } - const project = projects[0]; const { frontend } = project.amplifyExtension; let projectPath = process.cwd(); if (!withoutInit) { @@ -48,10 +49,10 @@ async function generateStatementsAndTypes(context, forceDownloadSchema, maxDepth } let downloadPromises; - if (!withoutInit) { + if (apis.length) { downloadPromises = projects.map( async cfg => - await ensureIntrospectionSchema(context, join(projectPath, cfg.schema), apis[0], cfg.amplifyExtension.region, forceDownloadSchema) + await ensureIntrospectionSchema(context, join(projectPath, cfg.schema), apis[0], cfg.amplifyExtension.region, forceDownloadSchema), ); await Promise.all(downloadPromises); } diff --git a/packages/amplify-codegen/src/commands/statements.js b/packages/amplify-codegen/src/commands/statements.js index f87c1be26..86184b91d 100644 --- a/packages/amplify-codegen/src/commands/statements.js +++ b/packages/amplify-codegen/src/commands/statements.js @@ -4,7 +4,13 @@ const Ora = require('ora'); const { loadConfig } = require('../codegen-config'); const constants = require('../constants'); -const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails, readSchemaFromFile } = require('../utils'); +const { + ensureIntrospectionSchema, + getFrontEndHandler, + getAppSyncAPIDetails, + readSchemaFromFile, + getAppSyncAPIInfoFromProject, +} = require('../utils'); const { generateGraphQLDocuments } = require('@aws-amplify/graphql-docs-generator'); const { generateStatements: generateStatementsHelper } = require('@aws-amplify/graphql-generator'); @@ -16,9 +22,18 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou } const config = loadConfig(context, withoutInit); const projects = config.getProjects(); + if (!projects.length && withoutInit) { + context.print.info(constants.ERROR_CODEGEN_NO_API_CONFIGURED); + return; + } let apis = []; if (!withoutInit) { apis = getAppSyncAPIDetails(context); + } else { + const api = await getAppSyncAPIInfoFromProject(context, projects[0]); + if (api) { + apis = [api]; + } } let projectPath = process.cwd(); if (!withoutInit) { @@ -30,10 +45,6 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou return; } } - if (!projects.length && withoutInit) { - context.print.info(constants.ERROR_CODEGEN_NO_API_CONFIGURED); - return; - } for (const cfg of projects) { const includeFiles = path.join(projectPath, cfg.includes[0]); @@ -41,15 +52,11 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou ? path.join(projectPath, cfg.amplifyExtension.docsFilePath) : path.dirname(path.dirname(includeFiles)); const schemaPath = path.join(projectPath, cfg.schema); - let region; - let frontend; - if (!withoutInit) { - ({ region } = cfg.amplifyExtension); + if (apis.length) { + const { region } = cfg.amplifyExtension; await ensureIntrospectionSchema(context, schemaPath, apis[0], region, forceDownloadSchema); - frontend = getFrontEndHandler(context); - } else { - frontend = decoupleFrontend; } + const frontend = withoutInit ? cfg.amplifyExtension.frontend : getFrontEndHandler(context); const language = frontend === 'javascript' ? cfg.amplifyExtension.codeGenTarget : 'graphql'; const opsGenSpinner = new Ora(constants.INFO_MESSAGE_OPS_GEN); diff --git a/packages/amplify-codegen/src/commands/types.js b/packages/amplify-codegen/src/commands/types.js index c45c3b17c..5da70cddf 100644 --- a/packages/amplify-codegen/src/commands/types.js +++ b/packages/amplify-codegen/src/commands/types.js @@ -5,7 +5,7 @@ const glob = require('glob-all'); const constants = require('../constants'); const { loadConfig } = require('../codegen-config'); -const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails } = require('../utils'); +const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails, getAppSyncAPIInfoFromProject } = require('../utils'); const { generateTypes: generateTypesHelper } = require('@aws-amplify/graphql-generator'); const { extractDocumentFromJavascript } = require('@aws-amplify/graphql-types-generator'); @@ -22,9 +22,18 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, if (frontend !== 'android') { const config = loadConfig(context, withoutInit); const projects = config.getProjects(); + if (!projects.length && withoutInit) { + context.print.info(constants.ERROR_CODEGEN_NO_API_CONFIGURED); + return; + } let apis = []; if (!withoutInit) { apis = getAppSyncAPIDetails(context); + } else { + const api = await getAppSyncAPIInfoFromProject(context, projects[0]); + if (api) { + apis = [api]; + } } if (!projects.length || !apis.length) { if (!withoutInit) { @@ -48,7 +57,7 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, const target = cfg.amplifyExtension.codeGenTarget; const excludes = cfg.excludes.map(pattern => `!${pattern}`); - const queries = glob + const queryFiles = glob .sync([...includeFiles, ...excludes], { cwd: projectPath, absolute: true, @@ -64,14 +73,22 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, return extractDocumentFromJavascript(fileContents, ''); } return fileContents; - }) - .join('\n'); + }); + if (queryFiles.length === 0) { + throw new Error('No queries found to generate types for, you may need to run \'codegen statements\' first'); + } + const queries = queryFiles.join('\n'); const schemaPath = path.join(projectPath, cfg.schema); - let region; - if (!withoutInit) { - ({ region } = cfg.amplifyExtension); + + const outputPath = path.join(projectPath, generatedFileName); + if (apis.length) { + const { region } = cfg.amplifyExtension; await ensureIntrospectionSchema(context, schemaPath, apis[0], region, forceDownloadSchema); + } else { + if (!fs.existsSync(schemaPath)) { + throw new Error(`Cannot find GraphQL schema file: ${schemaPath}`); + } } const codeGenSpinner = new Ora(constants.INFO_MESSAGE_CODEGEN_GENERATE_STARTED); codeGenSpinner.start(); diff --git a/packages/amplify-codegen/src/constants.js b/packages/amplify-codegen/src/constants.js index 3a6cb077d..111122e6e 100644 --- a/packages/amplify-codegen/src/constants.js +++ b/packages/amplify-codegen/src/constants.js @@ -19,7 +19,8 @@ module.exports = { PROMPT_MSG_SELECT_PROJECT: 'Choose the AppSync API', PROMPT_MSG_SELECT_REGION: 'Choose AWS Region', ERROR_CODEGEN_TARGET_NOT_SUPPORTED: 'is not supported by codegen plugin', - ERROR_FLUTTER_CODEGEN_NOT_SUPPORTED: 'Flutter only supports the command $amplify codegen models. All the other codegen commands are not supported.', + ERROR_FLUTTER_CODEGEN_NOT_SUPPORTED: + 'Flutter only supports the command $amplify codegen models. All the other codegen commands are not supported.', ERROR_CODEGEN_FRONTEND_NOT_SUPPORTED: 'The project frontend is not supported by codegen', ERROR_MSG_MAX_DEPTH: 'Depth should be a integer greater than 0', ERROR_CODEGEN_NO_API_AVAILABLE: 'There are no GraphQL APIs available.\nAdd by running $amplify api add', @@ -37,7 +38,8 @@ module.exports = { CMD_DESCRIPTION_CONFIGURE: 'Change/Update codegen configuration', ERROR_CODEGEN_NO_API_CONFIGURED: 'code generation is not configured. Configure it by running \n$amplify codegen add', ERROR_CODEGEN_PENDING_API_PUSH: 'AppSync API is not pushed to the cloud. Did you forget to do \n$amplify api push', - ERROR_CODEGEN_NO_API_META: 'Cannot find API metadata. Please reset codegen by running $amplify codegen remove && amplify codegen add --apiId YOUR_API_ID', + ERROR_CODEGEN_NO_API_META: + 'Cannot find API metadata. Please reset codegen by running $amplify codegen remove && amplify codegen add --apiId YOUR_API_ID', WARNING_CODEGEN_PENDING_API_PUSH: 'The APIs listed below are not pushed to the cloud. Run amplify api push', ERROR_APPSYNC_API_NOT_FOUND: 'Could not find the AppSync API. If you have removed the AppSync API in the console run amplify codegen remove', @@ -55,5 +57,6 @@ module.exports = { INFO_MESSAGE_DOWNLOAD_ERROR: 'Downloading schema failed', INFO_MESSAGE_OPS_GEN: 'Generating GraphQL operations', INFO_MESSAGE_OPS_GEN_SUCCESS: 'Generated GraphQL operations successfully and saved at ', - INFO_MESSAGE_ADD_ERROR: 'amplify codegen add takes only apiId as parameter. \n$ amplify codegen add [--apiId ]', + INFO_MESSAGE_ADD_ERROR: + 'amplify codegen add takes only apiId and region as parameters. \n$ amplify codegen add [--apiId ] [--region ]', }; diff --git a/packages/amplify-codegen/src/utils/downloadIntrospectionSchema.js b/packages/amplify-codegen/src/utils/downloadIntrospectionSchema.js index 5fd3bb880..240eeebe7 100644 --- a/packages/amplify-codegen/src/utils/downloadIntrospectionSchema.js +++ b/packages/amplify-codegen/src/utils/downloadIntrospectionSchema.js @@ -16,7 +16,11 @@ async function downloadIntrospectionSchema(context, apiId, downloadLocation, reg const introspectionDir = dirname(downloadLocation); fs.ensureDirSync(introspectionDir); fs.writeFileSync(downloadLocation, schema, 'utf8'); - return relative(amplify.getEnvInfo().projectPath, downloadLocation); + try { + return relative(amplify.getEnvInfo().projectPath, downloadLocation); + } catch { + return downloadLocation; + } } catch (ex) { if (ex.code === 'NotFoundException') { throw new AmplifyCodeGenAPINotFoundError(constants.ERROR_APPSYNC_API_NOT_FOUND); diff --git a/packages/amplify-codegen/src/utils/ensureIntrospectionSchema.js b/packages/amplify-codegen/src/utils/ensureIntrospectionSchema.js index fc74b6633..f765d3f11 100644 --- a/packages/amplify-codegen/src/utils/ensureIntrospectionSchema.js +++ b/packages/amplify-codegen/src/utils/ensureIntrospectionSchema.js @@ -4,9 +4,12 @@ const generateIntrospectionSchema = require('./generateIntrospectionSchema'); const downloadIntrospectionSchemaWithProgress = require('./generateIntrospectionSchemaWithProgress'); async function ensureIntrospectionSchema(context, schemaPath, apiConfig, region, forceDownloadSchema) { - const meta = context.amplify.getProjectMeta(); + let meta; + try { + meta = context.amplify.getProjectMeta(); + } catch {} const { id, name } = apiConfig; - const isTransformedAPI = Object.keys(meta.api || {}).includes(name) && meta.api[name].providerPlugin === 'awscloudformation'; + const isTransformedAPI = meta && Object.keys(meta.api || {}).includes(name) && meta.api[name].providerPlugin === 'awscloudformation'; if (isTransformedAPI && getFrontendHandler(context) === 'android') { generateIntrospectionSchema(context, name); } else if (schemaPath.endsWith('.json')) { diff --git a/packages/amplify-codegen/src/utils/getAppSyncAPIInfoFromProject.js b/packages/amplify-codegen/src/utils/getAppSyncAPIInfoFromProject.js new file mode 100644 index 000000000..083f54941 --- /dev/null +++ b/packages/amplify-codegen/src/utils/getAppSyncAPIInfoFromProject.js @@ -0,0 +1,15 @@ +const getAppSyncAPIInfo = require('./getAppSyncAPIInfo'); + +/* Get AppSync api info if api id and region are avialable. + * Otherwise return undefined. + */ +async function getAppSyncAPIInfoFromProject(context, project) { + if (project.amplifyExtension.apiId && project.amplifyExtension.region) { + const { + amplifyExtension: { apiId, region }, + } = project; + return getAppSyncAPIInfo(context, apiId, region); + } + return undefined; +} +module.exports = getAppSyncAPIInfoFromProject; diff --git a/packages/amplify-codegen/src/utils/index.js b/packages/amplify-codegen/src/utils/index.js index 184cffd74..35dbbbbb8 100644 --- a/packages/amplify-codegen/src/utils/index.js +++ b/packages/amplify-codegen/src/utils/index.js @@ -5,6 +5,7 @@ const downloadIntrospectionSchema = require('./downloadIntrospectionSchema'); const getSchemaDownloadLocation = require('./getSchemaDownloadLocation'); const getIncludePattern = require('./getIncludePattern'); const getAppSyncAPIInfo = require('./getAppSyncAPIInfo'); +const getAppSyncAPIInfoFromProject = require('./getAppSyncAPIInfoFromProject'); const getProjectAwsRegion = require('./getProjectAWSRegion'); const getGraphQLDocPath = require('./getGraphQLDocPath'); const downloadIntrospectionSchemaWithProgress = require('./generateIntrospectionSchemaWithProgress'); @@ -25,6 +26,7 @@ module.exports = { downloadIntrospectionSchemaWithProgress, getIncludePattern, getAppSyncAPIInfo, + getAppSyncAPIInfoFromProject, getProjectAwsRegion, getGraphQLDocPath, isAppSyncApiPendingPush, diff --git a/packages/amplify-codegen/tests/cli/add.test.js b/packages/amplify-codegen/tests/cli/add.test.js new file mode 100644 index 000000000..5a460920f --- /dev/null +++ b/packages/amplify-codegen/tests/cli/add.test.js @@ -0,0 +1,144 @@ +const add = require('../../commands/codegen/add'); +const codeGen = require('../../src/index'); + +jest.mock('../../src/index'); + +describe('cli - add', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('feature name', () => { + expect(add.name).toEqual('add'); + }); + + describe('run', () => { + test('executes codegen add', async () => { + const context = { + parameters: {}, + }; + await add.run(context); + expect(codeGen.add).toHaveBeenCalledWith(context, null, undefined); + }); + + test('catches error in codegen add', async () => { + const error = jest.fn(); + const context = { + parameters: {}, + print: { + error, + }, + }; + + const codegenError = new Error('failed to read file'); + codeGen.add.mockRejectedValueOnce(codegenError); + await add.run(context); + expect(error).toHaveBeenCalledWith(codegenError.message); + }); + + test('passes apiId', async () => { + const apiId = 'apiid'; + const context = { + parameters: { + options: { + apiId, + }, + }, + }; + await add.run(context); + expect(codeGen.add).toHaveBeenCalledWith(context, apiId, undefined); + }); + + test('passes region', async () => { + const region = 'region'; + const context = { + parameters: { + options: { + region, + }, + }, + }; + await add.run(context); + expect(codeGen.add).toHaveBeenCalledWith(context, null, region); + }); + + test('throws error on invalid arg', async () => { + const badArg = 'badArg'; + const info = jest.fn(); + const context = { + parameters: { + options: { + badArg, + }, + }, + print: { + info, + }, + }; + await add.run(context); + expect(info).toHaveBeenCalledWith('Invalid parameter badArg'); + + expect(info).toHaveBeenCalledWith( + 'amplify codegen add takes only apiId and region as parameters. \n$ amplify codegen add [--apiId ] [--region ]', + ); + }); + + test('throws error on invalid args', async () => { + const badArgOne = 'badArgOne'; + const badArgTwo = 'badArgTwo'; + const info = jest.fn(); + const context = { + parameters: { + options: { + badArgOne, + badArgTwo, + }, + }, + print: { + info, + }, + }; + await add.run(context); + expect(info).toHaveBeenCalledWith('Invalid parameters badArgOne, badArgTwo'); + + expect(info).toHaveBeenCalledWith( + 'amplify codegen add takes only apiId and region as parameters. \n$ amplify codegen add [--apiId ] [--region ]', + ); + }); + + test('allows undocummented frontend and framework', async () => { + const frontend = 'frontend'; + const framework = 'framework'; + const info = jest.fn(); + const context = { + parameters: { + options: { + frontend, + framework, + }, + }, + print: { + info, + }, + }; + await add.run(context); + expect(info).not.toHaveBeenCalled(); + }); + + test('ignores yes arg', async () => { + const yes = true; + const info = jest.fn(); + const context = { + parameters: { + options: { + yes, + }, + }, + print: { + info, + }, + }; + await add.run(context); + }); + }); +}); diff --git a/packages/amplify-codegen/tests/commands/__snapshots__/add.test.js.snap b/packages/amplify-codegen/tests/commands/__snapshots__/add.test.js.snap new file mode 100644 index 000000000..beb912eb0 --- /dev/null +++ b/packages/amplify-codegen/tests/commands/__snapshots__/add.test.js.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`command - add without init should download introspection schema when api id 1`] = ` +Object { + "amplifyExtension": Object { + "apiId": "MOCK_API_ID", + "codeGenTarget": "TYPE_SCRIPT_OR_FLOW_OR_ANY_OTHER_LANGUAGE", + "docsFilePath": "MOCK_DOCS_FILE_PATH", + "framework": "react", + "frontend": "javascript", + "generatedFileName": "API.TS", + "region": "us-east-1", + }, + "excludes": "MOCK_EXCLUDE", + "includes": "MOCK_INCLUDE", + "projectName": "Codegen Project", + "schema": "/user/foo/project/schema.json", +} +`; + +exports[`command - add without init should read frontend and framework from options 1`] = ` +Object { + "amplifyExtension": Object { + "apiId": "MOCK_API_ID", + "codeGenTarget": "TYPE_SCRIPT_OR_FLOW_OR_ANY_OTHER_LANGUAGE", + "docsFilePath": "MOCK_DOCS_FILE_PATH", + "framework": "vue", + "frontend": "javascript", + "generatedFileName": "API.TS", + "region": "us-east-1", + }, + "excludes": "MOCK_EXCLUDE", + "includes": "MOCK_INCLUDE", + "projectName": "Codegen Project", + "schema": "/user/foo/project/schema.json", +} +`; + +exports[`command - add without init should use existing schema if no api id 1`] = ` +Object { + "amplifyExtension": Object { + "apiId": null, + "codeGenTarget": "TYPE_SCRIPT_OR_FLOW_OR_ANY_OTHER_LANGUAGE", + "docsFilePath": "MOCK_DOCS_FILE_PATH", + "framework": "react", + "frontend": "javascript", + "generatedFileName": "API.TS", + "region": "us-east-1", + }, + "excludes": "MOCK_EXCLUDE", + "includes": "MOCK_INCLUDE", + "projectName": "Codegen Project", + "schema": "/user/foo/project/schema.graphql", +} +`; + +exports[`command - add without init should use region supplied when without init 1`] = ` +Object { + "amplifyExtension": Object { + "apiId": "MOCK_API_ID", + "codeGenTarget": "TYPE_SCRIPT_OR_FLOW_OR_ANY_OTHER_LANGUAGE", + "docsFilePath": "MOCK_DOCS_FILE_PATH", + "framework": "react", + "frontend": "javascript", + "generatedFileName": "API.TS", + "region": "us-west-2", + }, + "excludes": "MOCK_EXCLUDE", + "includes": "MOCK_INCLUDE", + "projectName": "Codegen Project", + "schema": "/user/foo/project/schema.json", +} +`; diff --git a/packages/amplify-codegen/tests/commands/add.test.js b/packages/amplify-codegen/tests/commands/add.test.js index 5e921f4f3..f353d6548 100644 --- a/packages/amplify-codegen/tests/commands/add.test.js +++ b/packages/amplify-codegen/tests/commands/add.test.js @@ -1,13 +1,23 @@ +const fs = require('fs'); +const path = require('path'); const { loadConfig } = require('../../src/codegen-config'); const generateStatements = require('../../src/commands/statements'); const generateTypes = require('../../src/commands/types'); const addWalkthrough = require('../../src/walkthrough/add'); +const askForFrontend = require('../../src/walkthrough/questions/selectFrontend'); +const askForFramework = require('../../src/walkthrough/questions/selectFramework'); const changeAppSyncRegions = require('../../src/walkthrough/changeAppSyncRegions'); const { AmplifyCodeGenAPINotFoundError } = require('../../src/errors'); const add = require('../../src/commands/add'); -const { getAppSyncAPIDetails, getAppSyncAPIInfo, getProjectAwsRegion, getSDLSchemaLocation } = require('../../src/utils'); +const { + getAppSyncAPIDetails, + getAppSyncAPIInfo, + getProjectAwsRegion, + getSDLSchemaLocation, + downloadIntrospectionSchemaWithProgress, +} = require('../../src/utils'); const MOCK_CONTEXT = { print: { @@ -16,13 +26,23 @@ const MOCK_CONTEXT = { amplify: { getProjectMeta: jest.fn(), }, + parameters: { + options: {}, + }, }; +const mockProjectDir = '/user/foo/project'; +jest.mock('fs'); jest.mock('../../src/walkthrough/add'); +jest.mock('../../src/walkthrough/questions/selectFrontend'); +jest.mock('../../src/walkthrough/questions/selectFramework'); jest.mock('../../src/walkthrough/changeAppSyncRegions'); jest.mock('../../src/commands/types'); jest.mock('../../src/commands/statements'); jest.mock('../../src/codegen-config'); jest.mock('../../src/utils'); +jest.mock('process', () => ({ + cwd: () => mockProjectDir, +})); const MOCK_INCLUDE_PATTERN = 'MOCK_INCLUDE'; const MOCK_EXCLUDE_PATTERN = 'MOCK_EXCLUDE'; @@ -68,6 +88,7 @@ describe('command - add', () => { loadConfig.mockReturnValue(LOAD_CONFIG_METHODS); getProjectAwsRegion.mockReturnValue(MOCK_AWS_REGION); getSDLSchemaLocation.mockReturnValue(MOCK_SCHEMA_FILE_LOCATION); + downloadIntrospectionSchemaWithProgress.mockReturnValue(); }); it('should walkthrough add questions', async () => { @@ -148,4 +169,115 @@ describe('command - add', () => { await add(MOCK_CONTEXT); expect(generateTypes).not.toHaveBeenCalled(); }); + + it('should ignore region supplied when with init', async () => { + const region = 'us-west-2'; + await add(MOCK_CONTEXT, MOCK_API_ID, region); + expect(getProjectAwsRegion).toHaveBeenCalled(); + expect(getAppSyncAPIInfo).toHaveBeenCalledWith(MOCK_CONTEXT, MOCK_API_ID, MOCK_AWS_REGION); + }); + + describe('without init', () => { + const getProjectMeta = jest.fn(); + const schemaPath = path.join(mockProjectDir, 'schema.json'); + beforeEach(() => { + loadConfig.mockReturnValue({ ...LOAD_CONFIG_METHODS, getProjects: jest.fn().mockReturnValue([]) }); + askForFrontend.mockReturnValue('javascript'); + askForFramework.mockReturnValue('react'); + getProjectMeta.mockRejectedValue('no init'); + fs.existsSync.mockReturnValue(false); + getAppSyncAPIInfo.mockReturnValue(MOCK_APPSYNC_API_DETAIL); + downloadIntrospectionSchemaWithProgress.mockReturnValue(schemaPath); + }); + + afterEach(() => { + loadConfig.mockReset(); + askForFrontend.mockReset(); + askForFramework.mockReset(); + getProjectMeta.mockReset(); + fs.existsSync.mockReset(); + getAppSyncAPIInfo.mockReset(); + downloadIntrospectionSchemaWithProgress.mockReset(); + }); + + it('should download introspection schema when api id', async () => { + const context = { ...MOCK_CONTEXT, amplify: { getProjectMeta } }; + const defaultRegion = 'us-east-1'; + await add(context, MOCK_API_ID); + expect(getAppSyncAPIInfo).toHaveBeenCalledWith(context, MOCK_API_ID, defaultRegion); + expect(downloadIntrospectionSchemaWithProgress).toHaveBeenCalledWith(context, MOCK_API_ID, schemaPath, defaultRegion); + expect(LOAD_CONFIG_METHODS.addProject.mock.calls[0][0]).toMatchSnapshot(); + }); + + it('should use existing schema if no api id', async () => { + fs.existsSync.mockReturnValue(true); + const context = { ...MOCK_CONTEXT, amplify: { getProjectMeta } }; + const defaultRegion = 'us-east-1'; + await add(context); + expect(getAppSyncAPIInfo).not.toHaveBeenCalled(); + expect(downloadIntrospectionSchemaWithProgress).not.toHaveBeenCalled(); + expect(LOAD_CONFIG_METHODS.addProject.mock.calls[0][0]).toMatchSnapshot(); + }); + + it('should read frontend and framework from options', async () => { + const parameters = { + options: { + frontend: 'javascript', + framework: 'vue', + }, + }; + await add({ ...MOCK_CONTEXT, amplify: { getProjectMeta }, parameters }, MOCK_API_ID); + expect(askForFrontend).not.toHaveBeenCalled(); + expect(askForFramework).not.toHaveBeenCalled(); + expect(LOAD_CONFIG_METHODS.addProject).toHaveBeenCalled(); + expect(LOAD_CONFIG_METHODS.addProject.mock.calls[0][0]).toMatchSnapshot(); + }); + + it('should use region supplied when without init', async () => { + const region = 'us-west-2'; + const context = { ...MOCK_CONTEXT, amplify: { getProjectMeta } }; + await add(context, MOCK_API_ID, region); + expect(getProjectAwsRegion).not.toHaveBeenCalled(); + expect(getAppSyncAPIInfo).toHaveBeenCalledWith(context, MOCK_API_ID, region); + expect(LOAD_CONFIG_METHODS.addProject).toHaveBeenCalled(); + expect(LOAD_CONFIG_METHODS.addProject.mock.calls[0][0]).toMatchSnapshot(); + }); + + it('should error on invalid frontend', () => { + const parameters = { + options: { + frontend: 'foo', + }, + }; + expect(add({ ...MOCK_CONTEXT, amplify: { getProjectMeta }, parameters }, MOCK_API_ID)).rejects.toThrowError( + 'Invalid frontend provided', + ); + }); + + it('should error on invalid framework', () => { + const parameters = { + options: { + frontend: 'javascript', + framework: 'foo', + }, + }; + expect(add({ ...MOCK_CONTEXT, amplify: { getProjectMeta }, parameters }, MOCK_API_ID)).rejects.toThrowError( + 'Invalid framework provided', + ); + }); + + it('should error if codegen project already exists', () => { + loadConfig.mockReturnValue({ ...LOAD_CONFIG_METHODS, getProjects: jest.fn().mockReturnValue(['foo']) }); + expect(add({ ...MOCK_CONTEXT, amplify: { getProjectMeta } }, MOCK_API_ID)).rejects.toThrowError( + 'Codegen support only one GraphQL API per project', + ); + }); + + it('should error if codegen project already exists', () => { + fs.existsSync.mockReturnValue(false); + expect(add({ ...MOCK_CONTEXT, amplify: { getProjectMeta } })).rejects.toThrowError( + 'Provide an AppSync API ID with --apiId or manually download schema.graphql or schema.json and place in /user/foo/project before adding codegen when not in an amplify project', + ); + }); + }); }); From 23efa078b0aefe10358bd9551c6ae633f83a7eae Mon Sep 17 00:00:00 2001 From: amplify-data-ci Date: Tue, 19 Sep 2023 20:44:22 +0000 Subject: [PATCH 03/11] chore(release): Publish [ci skip] - @aws-amplify/amplify-codegen-e2e-core@1.6.0 - @aws-amplify/amplify-codegen-e2e-tests@2.44.0 - amplify-codegen@4.6.0 - @aws-amplify/appsync-modelgen-plugin@2.7.1 - @aws-amplify/graphql-generator@0.1.1 --- packages/amplify-codegen-e2e-core/CHANGELOG.md | 7 +++++++ packages/amplify-codegen-e2e-core/package.json | 2 +- packages/amplify-codegen-e2e-tests/CHANGELOG.md | 7 +++++++ packages/amplify-codegen-e2e-tests/package.json | 4 ++-- packages/amplify-codegen/CHANGELOG.md | 7 +++++++ packages/amplify-codegen/package.json | 4 ++-- packages/appsync-modelgen-plugin/CHANGELOG.md | 6 ++++++ packages/appsync-modelgen-plugin/package.json | 2 +- packages/graphql-generator/CHANGELOG.md | 4 ++++ packages/graphql-generator/package.json | 4 ++-- 10 files changed, 39 insertions(+), 8 deletions(-) diff --git a/packages/amplify-codegen-e2e-core/CHANGELOG.md b/packages/amplify-codegen-e2e-core/CHANGELOG.md index 0c212ca4f..65ff86296 100644 --- a/packages/amplify-codegen-e2e-core/CHANGELOG.md +++ b/packages/amplify-codegen-e2e-core/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.6.0](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/amplify-codegen-e2e-core@1.5.4...@aws-amplify/amplify-codegen-e2e-core@1.6.0) (2023-09-19) + +### Features + +- merge noinit-modelgen into main to support executing amplify codegen models without an initialized amplify app ([#698](https://github.com/aws-amplify/amplify-codegen/issues/698)) ([d9ce4ea](https://github.com/aws-amplify/amplify-codegen/commit/d9ce4eab58ec4ac72fc737461c11783562d96167)), closes [#686](https://github.com/aws-amplify/amplify-codegen/issues/686) [#693](https://github.com/aws-amplify/amplify-codegen/issues/693) [#694](https://github.com/aws-amplify/amplify-codegen/issues/694) [#696](https://github.com/aws-amplify/amplify-codegen/issues/696) +- support running codegen without an initialized amplify backend set up locally. ([#702](https://github.com/aws-amplify/amplify-codegen/issues/702)) ([8279f35](https://github.com/aws-amplify/amplify-codegen/commit/8279f35d84cb10f0df3c4fb0f4a141f86dbc3e60)), closes [#683](https://github.com/aws-amplify/amplify-codegen/issues/683) [#684](https://github.com/aws-amplify/amplify-codegen/issues/684) [#689](https://github.com/aws-amplify/amplify-codegen/issues/689) [#704](https://github.com/aws-amplify/amplify-codegen/issues/704) [#705](https://github.com/aws-amplify/amplify-codegen/issues/705) [#706](https://github.com/aws-amplify/amplify-codegen/issues/706) + ## [1.5.4](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/amplify-codegen-e2e-core@1.5.3...@aws-amplify/amplify-codegen-e2e-core@1.5.4) (2023-08-15) **Note:** Version bump only for package @aws-amplify/amplify-codegen-e2e-core diff --git a/packages/amplify-codegen-e2e-core/package.json b/packages/amplify-codegen-e2e-core/package.json index 5fbdfc838..180609a3f 100644 --- a/packages/amplify-codegen-e2e-core/package.json +++ b/packages/amplify-codegen-e2e-core/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/amplify-codegen-e2e-core", - "version": "1.5.4", + "version": "1.6.0", "description": "", "repository": { "type": "git", diff --git a/packages/amplify-codegen-e2e-tests/CHANGELOG.md b/packages/amplify-codegen-e2e-tests/CHANGELOG.md index 12dff39be..7c670b7fb 100644 --- a/packages/amplify-codegen-e2e-tests/CHANGELOG.md +++ b/packages/amplify-codegen-e2e-tests/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.44.0](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/amplify-codegen-e2e-tests@2.43.4...@aws-amplify/amplify-codegen-e2e-tests@2.44.0) (2023-09-19) + +### Features + +- merge noinit-modelgen into main to support executing amplify codegen models without an initialized amplify app ([#698](https://github.com/aws-amplify/amplify-codegen/issues/698)) ([d9ce4ea](https://github.com/aws-amplify/amplify-codegen/commit/d9ce4eab58ec4ac72fc737461c11783562d96167)), closes [#686](https://github.com/aws-amplify/amplify-codegen/issues/686) [#693](https://github.com/aws-amplify/amplify-codegen/issues/693) [#694](https://github.com/aws-amplify/amplify-codegen/issues/694) [#696](https://github.com/aws-amplify/amplify-codegen/issues/696) +- support running codegen without an initialized amplify backend set up locally. ([#702](https://github.com/aws-amplify/amplify-codegen/issues/702)) ([8279f35](https://github.com/aws-amplify/amplify-codegen/commit/8279f35d84cb10f0df3c4fb0f4a141f86dbc3e60)), closes [#683](https://github.com/aws-amplify/amplify-codegen/issues/683) [#684](https://github.com/aws-amplify/amplify-codegen/issues/684) [#689](https://github.com/aws-amplify/amplify-codegen/issues/689) [#704](https://github.com/aws-amplify/amplify-codegen/issues/704) [#705](https://github.com/aws-amplify/amplify-codegen/issues/705) [#706](https://github.com/aws-amplify/amplify-codegen/issues/706) + ## [2.43.4](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/amplify-codegen-e2e-tests@2.43.3...@aws-amplify/amplify-codegen-e2e-tests@2.43.4) (2023-08-15) **Note:** Version bump only for package @aws-amplify/amplify-codegen-e2e-tests diff --git a/packages/amplify-codegen-e2e-tests/package.json b/packages/amplify-codegen-e2e-tests/package.json index 5aff186a7..078218555 100644 --- a/packages/amplify-codegen-e2e-tests/package.json +++ b/packages/amplify-codegen-e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/amplify-codegen-e2e-tests", - "version": "2.43.4", + "version": "2.44.0", "description": "", "repository": { "type": "git", @@ -22,7 +22,7 @@ "clean-e2e-resources": "ts-node ./src/cleanup-e2e-resources.ts" }, "dependencies": { - "@aws-amplify/amplify-codegen-e2e-core": "1.5.4", + "@aws-amplify/amplify-codegen-e2e-core": "1.6.0", "@aws-amplify/graphql-schema-test-library": "^1.1.18", "aws-amplify": "^5.3.3", "aws-appsync": "^4.1.9", diff --git a/packages/amplify-codegen/CHANGELOG.md b/packages/amplify-codegen/CHANGELOG.md index d5d04b7a5..2a099c53c 100644 --- a/packages/amplify-codegen/CHANGELOG.md +++ b/packages/amplify-codegen/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.6.0](https://github.com/aws-amplify/amplify-codegen/compare/amplify-codegen@4.5.0...amplify-codegen@4.6.0) (2023-09-19) + +### Features + +- merge noinit-modelgen into main to support executing amplify codegen models without an initialized amplify app ([#698](https://github.com/aws-amplify/amplify-codegen/issues/698)) ([d9ce4ea](https://github.com/aws-amplify/amplify-codegen/commit/d9ce4eab58ec4ac72fc737461c11783562d96167)), closes [#686](https://github.com/aws-amplify/amplify-codegen/issues/686) [#693](https://github.com/aws-amplify/amplify-codegen/issues/693) [#694](https://github.com/aws-amplify/amplify-codegen/issues/694) [#696](https://github.com/aws-amplify/amplify-codegen/issues/696) +- support running codegen without an initialized amplify backend set up locally. ([#702](https://github.com/aws-amplify/amplify-codegen/issues/702)) ([8279f35](https://github.com/aws-amplify/amplify-codegen/commit/8279f35d84cb10f0df3c4fb0f4a141f86dbc3e60)), closes [#683](https://github.com/aws-amplify/amplify-codegen/issues/683) [#684](https://github.com/aws-amplify/amplify-codegen/issues/684) [#689](https://github.com/aws-amplify/amplify-codegen/issues/689) [#704](https://github.com/aws-amplify/amplify-codegen/issues/704) [#705](https://github.com/aws-amplify/amplify-codegen/issues/705) [#706](https://github.com/aws-amplify/amplify-codegen/issues/706) + # [4.5.0](https://github.com/aws-amplify/amplify-codegen/compare/amplify-codegen@4.4.0...amplify-codegen@4.5.0) (2023-09-12) ### Features diff --git a/packages/amplify-codegen/package.json b/packages/amplify-codegen/package.json index 296e27e59..f6d97b8aa 100644 --- a/packages/amplify-codegen/package.json +++ b/packages/amplify-codegen/package.json @@ -1,6 +1,6 @@ { "name": "amplify-codegen", - "version": "4.5.0", + "version": "4.6.0", "description": "Amplify Code Generator", "repository": { "type": "git", @@ -21,7 +21,7 @@ "extract-api": "ts-node ../../scripts/extract-api.ts" }, "dependencies": { - "@aws-amplify/graphql-generator": "0.1.0", + "@aws-amplify/graphql-generator": "0.1.1", "@aws-amplify/graphql-types-generator": "3.4.0", "@graphql-codegen/core": "2.6.6", "chalk": "^3.0.0", diff --git a/packages/appsync-modelgen-plugin/CHANGELOG.md b/packages/appsync-modelgen-plugin/CHANGELOG.md index 12c12b575..6983cf1d0 100644 --- a/packages/appsync-modelgen-plugin/CHANGELOG.md +++ b/packages/appsync-modelgen-plugin/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.7.1](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/appsync-modelgen-plugin@2.7.0...@aws-amplify/appsync-modelgen-plugin@2.7.1) (2023-09-19) + +### Bug Fixes + +- avoid name collision on java model build step ([#700](https://github.com/aws-amplify/amplify-codegen/issues/700)) ([f44358a](https://github.com/aws-amplify/amplify-codegen/commit/f44358aef41556a9dbc8e511ad69d254a1e7ee67)) + # [2.7.0](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/appsync-modelgen-plugin@2.6.0...@aws-amplify/appsync-modelgen-plugin@2.7.0) (2023-09-12) ### Features diff --git a/packages/appsync-modelgen-plugin/package.json b/packages/appsync-modelgen-plugin/package.json index 20230b0c3..c289ee3f0 100644 --- a/packages/appsync-modelgen-plugin/package.json +++ b/packages/appsync-modelgen-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/appsync-modelgen-plugin", - "version": "2.7.0", + "version": "2.7.1", "repository": { "type": "git", "url": "https://github.com/aws-amplify/amplify-codegen.git", diff --git a/packages/graphql-generator/CHANGELOG.md b/packages/graphql-generator/CHANGELOG.md index d8496306a..bc5ab4a4f 100644 --- a/packages/graphql-generator/CHANGELOG.md +++ b/packages/graphql-generator/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.1](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/graphql-generator@0.1.0...@aws-amplify/graphql-generator@0.1.1) (2023-09-19) + +**Note:** Version bump only for package @aws-amplify/graphql-generator + # 0.1.0 (2023-09-12) ### Features diff --git a/packages/graphql-generator/package.json b/packages/graphql-generator/package.json index 6bebc09c9..461d7bc64 100644 --- a/packages/graphql-generator/package.json +++ b/packages/graphql-generator/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/graphql-generator", - "version": "0.1.0", + "version": "0.1.1", "description": "GraphQL API code generator", "repository": { "type": "git", @@ -24,7 +24,7 @@ "extract-api": "ts-node ../../scripts/extract-api.ts" }, "dependencies": { - "@aws-amplify/appsync-modelgen-plugin": "2.7.0", + "@aws-amplify/appsync-modelgen-plugin": "2.7.1", "@aws-amplify/graphql-docs-generator": "4.2.0", "@aws-amplify/graphql-types-generator": "3.4.0", "@graphql-codegen/core": "^2.6.6", From f7a024d9ec4f7b0156c72b7f7d05139bbb339cbd Mon Sep 17 00:00:00 2001 From: Al Harris <91494052+alharris-at@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:54:49 -0700 Subject: [PATCH 04/11] fix: move types from runtime deps to dev deps (#708) --- packages/appsync-modelgen-plugin/package.json | 6 ++--- packages/graphql-types-generator/package.json | 10 ++++----- yarn.lock | 22 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/appsync-modelgen-plugin/package.json b/packages/appsync-modelgen-plugin/package.json index c289ee3f0..15b0ff8b4 100644 --- a/packages/appsync-modelgen-plugin/package.json +++ b/packages/appsync-modelgen-plugin/package.json @@ -29,8 +29,6 @@ "@graphql-codegen/plugin-helpers": "^1.18.8", "@graphql-codegen/visitor-plugin-common": "^1.22.0", "@graphql-tools/utils": "^6.0.18", - "@types/node": "^12.12.6", - "@types/pluralize": "0.0.29", "chalk": "^3.0.0", "change-case": "^4.1.1", "lower-case-first": "^2.0.1", @@ -43,7 +41,9 @@ "@graphql-codegen/typescript": "^2.8.3", "graphql": "^15.5.0", "java-ast": "^0.3.0", - "ts-json-schema-generator": "1.0.0" + "ts-json-schema-generator": "1.0.0", + "@types/node": "^12.12.6", + "@types/pluralize": "0.0.29" }, "peerDependencies": { "graphql": "^15.5.0" diff --git a/packages/graphql-types-generator/package.json b/packages/graphql-types-generator/package.json index 374522e64..4224f6a70 100644 --- a/packages/graphql-types-generator/package.json +++ b/packages/graphql-types-generator/package.json @@ -37,10 +37,6 @@ "dependencies": { "@babel/generator": "7.0.0-beta.4", "@babel/types": "7.0.0-beta.4", - "@types/babel-generator": "^6.25.0", - "@types/fs-extra": "^8.1.0", - "@types/prettier": "^1.19.0", - "@types/rimraf": "^3.0.0", "babel-generator": "^6.26.1", "babel-types": "^6.26.0", "change-case": "^4.1.1", @@ -60,7 +56,11 @@ "@types/glob": "^7.1.1", "@types/inflected": "^1.1.29", "@types/node": "^10.17.13", - "@types/yargs": "^15.0.1" + "@types/yargs": "^15.0.1", + "@types/babel-generator": "^6.25.0", + "@types/fs-extra": "^8.1.0", + "@types/prettier": "^1.19.0", + "@types/rimraf": "^3.0.0" }, "publishConfig": { "access": "public" diff --git a/yarn.lock b/yarn.lock index 34e7b820c..8502228bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3744,9 +3744,9 @@ value-or-promise "^1.0.12" "@graphql-tools/utils@^10.0.0": - version "10.0.5" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.0.5.tgz#b76c7b5b7fc3f67da734b51cf6e33c52dee09974" - integrity sha512-ZTioQqg9z9eCG3j+KDy54k1gp6wRIsLqkx5yi163KVvXVkfjsrdErCyZjrEug21QnKE9piP4tyxMpMMOT1RuRw== + version "10.0.6" + resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.0.6.tgz#8a809d6bc0df27ffe8964696f182af2383b5974b" + integrity sha512-hZMjl/BbX10iagovakgf3IiqArx8TPsotq5pwBld37uIX1JiZoSbgbCIFol7u55bh32o6cfDEiiJgfAD5fbeyQ== dependencies: "@graphql-typed-document-node/core" "^3.1.1" dset "^3.1.2" @@ -6128,17 +6128,17 @@ integrity sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w== "@whatwg-node/fetch@^0.9.0": - version "0.9.9" - resolved "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.9.tgz#65e68aaf8353755c20657b803f2fe983dbdabf91" - integrity sha512-OTVoDm039CNyAWSRc2WBimMl/N9J4Fk2le21Xzcf+3OiWPNNSIbMnpWKBUyraPh2d9SAEgoBdQxTfVNihXgiUw== + version "0.9.13" + resolved "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.13.tgz#1d084cd546b9cd425ae89cbb1252a3e47a9a2e1c" + integrity sha512-PPtMwhjtS96XROnSpowCQM85gCUG2m7AXZFw0PZlGbhzx2GK7f2iOXilfgIJ0uSlCuuGbOIzfouISkA7C4FJOw== dependencies: - "@whatwg-node/node-fetch" "^0.4.8" + "@whatwg-node/node-fetch" "^0.4.17" urlpattern-polyfill "^9.0.0" -"@whatwg-node/node-fetch@^0.4.8": - version "0.4.13" - resolved "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.4.13.tgz#523b046f9511e6e62ac98365653b5ce63175614b" - integrity sha512-Wijn8jtXq6VBX6EttABXHJIQBcoOP6RRQllXbiaHGORACTDr1xg6g2UnkoggY3dbDkm1VsMjdSe7NVBPc4ukYg== +"@whatwg-node/node-fetch@^0.4.17": + version "0.4.19" + resolved "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.4.19.tgz#29c72ff65a8e450949238612ff17a3d3717736d3" + integrity sha512-AW7/m2AuweAoSXmESrYQr/KBafueScNbn2iNO0u6xFr2JZdPmYsSm5yvAXYk6yDLv+eDmSSKrf7JnFZ0CsJIdA== dependencies: "@whatwg-node/events" "^0.1.0" busboy "^1.6.0" From 756c3751bf236a64a5f028f61523cb96e0d7a7fa Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Fri, 22 Sep 2023 13:10:14 -0600 Subject: [PATCH 05/11] fix: typo in graphql-generator arg (#709) * fix: useExperimentalPipelinedTranformer->useExperimentalPipelinedTransformer (no s in Transformer) * chore: extract api --- packages/graphql-generator/API.md | 2 +- packages/graphql-generator/src/models.ts | 4 ++-- packages/graphql-generator/src/typescript.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/graphql-generator/API.md b/packages/graphql-generator/API.md index 169907436..75571257c 100644 --- a/packages/graphql-generator/API.md +++ b/packages/graphql-generator/API.md @@ -22,7 +22,7 @@ export type GenerateModelsOptions = { directives: string; generateIndexRules?: boolean; emitAuthProvider?: boolean; - useExperimentalPipelinedTranformer?: boolean; + useExperimentalPipelinedTransformer?: boolean; transformerVersion?: boolean; respectPrimaryKeyAttributesOnConnectionField?: boolean; generateModelsForLazyLoadAndCustomSelectionSet?: boolean; diff --git a/packages/graphql-generator/src/models.ts b/packages/graphql-generator/src/models.ts index 3dfb490ed..0871a7854 100644 --- a/packages/graphql-generator/src/models.ts +++ b/packages/graphql-generator/src/models.ts @@ -13,7 +13,7 @@ export async function generateModels(options: GenerateModelsOptions): Promise Date: Mon, 25 Sep 2023 12:17:29 -0600 Subject: [PATCH 06/11] fix: multiple swift file names (#713) --- .../amplify-codegen/src/commands/types.js | 73 ++++++++++--------- .../tests/commands/types.test.js | 28 +++++++ 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/packages/amplify-codegen/src/commands/types.js b/packages/amplify-codegen/src/commands/types.js index 5da70cddf..ee28bf6ff 100644 --- a/packages/amplify-codegen/src/commands/types.js +++ b/packages/amplify-codegen/src/commands/types.js @@ -7,7 +7,7 @@ const constants = require('../constants'); const { loadConfig } = require('../codegen-config'); const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails, getAppSyncAPIInfoFromProject } = require('../utils'); const { generateTypes: generateTypesHelper } = require('@aws-amplify/graphql-generator'); -const { extractDocumentFromJavascript } = require('@aws-amplify/graphql-types-generator'); +const { generate, extractDocumentFromJavascript } = require('@aws-amplify/graphql-types-generator'); async function generateTypes(context, forceDownloadSchema, withoutInit = false, decoupleFrontend = '') { let frontend = decoupleFrontend; @@ -57,25 +57,24 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, const target = cfg.amplifyExtension.codeGenTarget; const excludes = cfg.excludes.map(pattern => `!${pattern}`); - const queryFiles = glob - .sync([...includeFiles, ...excludes], { - cwd: projectPath, - absolute: true, - }) - .map(queryFilePath => { - const fileContents = fs.readFileSync(queryFilePath, 'utf8'); - if ( - queryFilePath.endsWith('.jsx') || - queryFilePath.endsWith('.js') || - queryFilePath.endsWith('.tsx') || - queryFilePath.endsWith('.ts') - ) { - return extractDocumentFromJavascript(fileContents, ''); - } - return fileContents; - }); + const queryFilePaths = glob.sync([...includeFiles, ...excludes], { + cwd: projectPath, + absolute: true, + }); + const queryFiles = queryFilePaths.map(queryFilePath => { + const fileContents = fs.readFileSync(queryFilePath, 'utf8'); + if ( + queryFilePath.endsWith('.jsx') || + queryFilePath.endsWith('.js') || + queryFilePath.endsWith('.tsx') || + queryFilePath.endsWith('.ts') + ) { + return extractDocumentFromJavascript(fileContents, ''); + } + return fileContents; + }); if (queryFiles.length === 0) { - throw new Error('No queries found to generate types for, you may need to run \'codegen statements\' first'); + throw new Error("No queries found to generate types for, you may need to run 'codegen statements' first"); } const queries = queryFiles.join('\n'); @@ -96,22 +95,30 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, const introspection = path.extname(schemaPath) === '.json'; try { - const output = await generateTypesHelper({ - schema, - queries, - target, - introspection, - }); - const outputs = Object.entries(output); - - const outputPath = path.join(projectPath, generatedFileName); - if (outputs.length === 1) { - const [[, contents]] = outputs; - fs.outputFileSync(path.resolve(outputPath), contents); + if (target === 'swift' && fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory()) { + generate(queryFilePaths, schemaPath, outputPath, '', target, '', { + addTypename: true, + complexObjectSupport: 'auto', + }); } else { - outputs.forEach(([filepath, contents]) => { - fs.outputFileSync(path.resolve(path.join(outputPath, filepath)), contents); + const output = await generateTypesHelper({ + schema, + queries, + target, + introspection, + multipleSwiftFiles: false, }); + const outputs = Object.entries(output); + + const outputPath = path.join(projectPath, generatedFileName); + if (outputs.length === 1) { + const [[, contents]] = outputs; + fs.outputFileSync(path.resolve(outputPath), contents); + } else { + outputs.forEach(([filepath, contents]) => { + fs.outputFileSync(path.resolve(path.join(outputPath, filepath)), contents); + }); + } } codeGenSpinner.succeed(`${constants.INFO_MESSAGE_CODEGEN_GENERATE_SUCCESS} ${path.relative(path.resolve('.'), outputPath)}`); } catch (err) { diff --git a/packages/amplify-codegen/tests/commands/types.test.js b/packages/amplify-codegen/tests/commands/types.test.js index a9cd3c36f..b492e0d55 100644 --- a/packages/amplify-codegen/tests/commands/types.test.js +++ b/packages/amplify-codegen/tests/commands/types.test.js @@ -1,6 +1,7 @@ const { sync } = require('glob-all'); const path = require('path'); const { generateTypes: generateTypesHelper } = require('@aws-amplify/graphql-generator'); +const { generate: legacyGenerate } = require('@aws-amplify/graphql-types-generator'); const fs = require('fs-extra'); const { loadConfig } = require('../../src/codegen-config'); @@ -20,6 +21,7 @@ const MOCK_CONTEXT = { jest.mock('glob-all'); jest.mock('@aws-amplify/graphql-generator'); +jest.mock('@aws-amplify/graphql-types-generator'); jest.mock('../../src/codegen-config'); jest.mock('../../src/utils'); jest.mock('fs-extra'); @@ -81,9 +83,35 @@ describe('command - types', () => { schema: 'schema', target: 'TYPE_SCRIPT_OR_FLOW_OR_ANY_OTHER_LANGUAGE', introspection: false, + multipleSwiftFiles: false, }); }); + it('should use legacy types generation when generating multiple swift files', async () => { + MOCK_PROJECT.amplifyExtension.codeGenTarget = 'swift'; + MOCK_PROJECT.amplifyExtension.generatedFileName = 'typesDirectory'; + const forceDownload = false; + fs.readFileSync + .mockReturnValueOnce('query 1') + .mockReturnValueOnce('query 2') + .mockReturnValueOnce('schema'); + fs.existsSync.mockReturnValueOnce(true); + fs.statSync.mockReturnValueOnce({ + isDirectory: jest.fn().mockReturnValue(true), + }); + await generateTypes(MOCK_CONTEXT, forceDownload); + expect(generateTypesHelper).not.toHaveBeenCalled(); + expect(legacyGenerate).toHaveBeenCalledWith( + ['q1.gql', 'q2.gql'], + 'MOCK_PROJECT_ROOT/INTROSPECTION_SCHEMA.JSON', + 'MOCK_PROJECT_ROOT/typesDirectory', + '', + 'swift', + '', + { addTypename: true, complexObjectSupport: 'auto' }, + ); + }); + it('should not generate type if the frontend is android', async () => { const forceDownload = false; getFrontEndHandler.mockReturnValue('android'); From fdd721079261e10849d478b52f37b2afe41d1b0c Mon Sep 17 00:00:00 2001 From: amplify-data-ci Date: Mon, 25 Sep 2023 19:19:48 +0000 Subject: [PATCH 07/11] chore(release): Publish [ci skip] - amplify-codegen@4.6.1 - @aws-amplify/appsync-modelgen-plugin@2.7.2 - @aws-amplify/graphql-generator@0.1.2 - @aws-amplify/graphql-types-generator@3.4.1 --- packages/amplify-codegen/CHANGELOG.md | 6 ++++++ packages/amplify-codegen/package.json | 6 +++--- packages/appsync-modelgen-plugin/CHANGELOG.md | 6 ++++++ packages/appsync-modelgen-plugin/package.json | 8 ++++---- packages/graphql-generator/CHANGELOG.md | 6 ++++++ packages/graphql-generator/package.json | 6 +++--- packages/graphql-types-generator/CHANGELOG.md | 6 ++++++ packages/graphql-types-generator/package.json | 10 +++++----- 8 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/amplify-codegen/CHANGELOG.md b/packages/amplify-codegen/CHANGELOG.md index 2a099c53c..7b019771d 100644 --- a/packages/amplify-codegen/CHANGELOG.md +++ b/packages/amplify-codegen/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.6.1](https://github.com/aws-amplify/amplify-codegen/compare/amplify-codegen@4.6.0...amplify-codegen@4.6.1) (2023-09-25) + +### Bug Fixes + +- multiple swift file names ([#713](https://github.com/aws-amplify/amplify-codegen/issues/713)) ([a901362](https://github.com/aws-amplify/amplify-codegen/commit/a90136266944812001913b4b49972dda87750763)) + # [4.6.0](https://github.com/aws-amplify/amplify-codegen/compare/amplify-codegen@4.5.0...amplify-codegen@4.6.0) (2023-09-19) ### Features diff --git a/packages/amplify-codegen/package.json b/packages/amplify-codegen/package.json index f6d97b8aa..858511cc3 100644 --- a/packages/amplify-codegen/package.json +++ b/packages/amplify-codegen/package.json @@ -1,6 +1,6 @@ { "name": "amplify-codegen", - "version": "4.6.0", + "version": "4.6.1", "description": "Amplify Code Generator", "repository": { "type": "git", @@ -21,8 +21,8 @@ "extract-api": "ts-node ../../scripts/extract-api.ts" }, "dependencies": { - "@aws-amplify/graphql-generator": "0.1.1", - "@aws-amplify/graphql-types-generator": "3.4.0", + "@aws-amplify/graphql-generator": "0.1.2", + "@aws-amplify/graphql-types-generator": "3.4.1", "@graphql-codegen/core": "2.6.6", "chalk": "^3.0.0", "fs-extra": "^8.1.0", diff --git a/packages/appsync-modelgen-plugin/CHANGELOG.md b/packages/appsync-modelgen-plugin/CHANGELOG.md index 6983cf1d0..48e778c65 100644 --- a/packages/appsync-modelgen-plugin/CHANGELOG.md +++ b/packages/appsync-modelgen-plugin/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.7.2](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/appsync-modelgen-plugin@2.7.1...@aws-amplify/appsync-modelgen-plugin@2.7.2) (2023-09-25) + +### Bug Fixes + +- move types from runtime deps to dev deps ([#708](https://github.com/aws-amplify/amplify-codegen/issues/708)) ([f7a024d](https://github.com/aws-amplify/amplify-codegen/commit/f7a024d9ec4f7b0156c72b7f7d05139bbb339cbd)) + ## [2.7.1](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/appsync-modelgen-plugin@2.7.0...@aws-amplify/appsync-modelgen-plugin@2.7.1) (2023-09-19) ### Bug Fixes diff --git a/packages/appsync-modelgen-plugin/package.json b/packages/appsync-modelgen-plugin/package.json index 15b0ff8b4..e4fc1cd67 100644 --- a/packages/appsync-modelgen-plugin/package.json +++ b/packages/appsync-modelgen-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/appsync-modelgen-plugin", - "version": "2.7.1", + "version": "2.7.2", "repository": { "type": "git", "url": "https://github.com/aws-amplify/amplify-codegen.git", @@ -39,11 +39,11 @@ "devDependencies": { "@graphql-codegen/testing": "^1.17.7", "@graphql-codegen/typescript": "^2.8.3", + "@types/node": "^12.12.6", + "@types/pluralize": "0.0.29", "graphql": "^15.5.0", "java-ast": "^0.3.0", - "ts-json-schema-generator": "1.0.0", - "@types/node": "^12.12.6", - "@types/pluralize": "0.0.29" + "ts-json-schema-generator": "1.0.0" }, "peerDependencies": { "graphql": "^15.5.0" diff --git a/packages/graphql-generator/CHANGELOG.md b/packages/graphql-generator/CHANGELOG.md index bc5ab4a4f..8061bd031 100644 --- a/packages/graphql-generator/CHANGELOG.md +++ b/packages/graphql-generator/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.2](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/graphql-generator@0.1.1...@aws-amplify/graphql-generator@0.1.2) (2023-09-25) + +### Bug Fixes + +- typo in graphql-generator arg ([#709](https://github.com/aws-amplify/amplify-codegen/issues/709)) ([756c375](https://github.com/aws-amplify/amplify-codegen/commit/756c3751bf236a64a5f028f61523cb96e0d7a7fa)) + ## [0.1.1](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/graphql-generator@0.1.0...@aws-amplify/graphql-generator@0.1.1) (2023-09-19) **Note:** Version bump only for package @aws-amplify/graphql-generator diff --git a/packages/graphql-generator/package.json b/packages/graphql-generator/package.json index 461d7bc64..10a63a00b 100644 --- a/packages/graphql-generator/package.json +++ b/packages/graphql-generator/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/graphql-generator", - "version": "0.1.1", + "version": "0.1.2", "description": "GraphQL API code generator", "repository": { "type": "git", @@ -24,9 +24,9 @@ "extract-api": "ts-node ../../scripts/extract-api.ts" }, "dependencies": { - "@aws-amplify/appsync-modelgen-plugin": "2.7.1", + "@aws-amplify/appsync-modelgen-plugin": "2.7.2", "@aws-amplify/graphql-docs-generator": "4.2.0", - "@aws-amplify/graphql-types-generator": "3.4.0", + "@aws-amplify/graphql-types-generator": "3.4.1", "@graphql-codegen/core": "^2.6.6", "@graphql-tools/apollo-engine-loader": "^8.0.0", "graphql": "^15.5.0" diff --git a/packages/graphql-types-generator/CHANGELOG.md b/packages/graphql-types-generator/CHANGELOG.md index 7328e12f6..f6de0e6e4 100644 --- a/packages/graphql-types-generator/CHANGELOG.md +++ b/packages/graphql-types-generator/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.4.1](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/graphql-types-generator@3.4.0...@aws-amplify/graphql-types-generator@3.4.1) (2023-09-25) + +### Bug Fixes + +- move types from runtime deps to dev deps ([#708](https://github.com/aws-amplify/amplify-codegen/issues/708)) ([f7a024d](https://github.com/aws-amplify/amplify-codegen/commit/f7a024d9ec4f7b0156c72b7f7d05139bbb339cbd)) + # [3.4.0](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/graphql-types-generator@3.3.0...@aws-amplify/graphql-types-generator@3.4.0) (2023-09-12) ### Features diff --git a/packages/graphql-types-generator/package.json b/packages/graphql-types-generator/package.json index 4224f6a70..d9e821f72 100644 --- a/packages/graphql-types-generator/package.json +++ b/packages/graphql-types-generator/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/graphql-types-generator", - "version": "3.4.0", + "version": "3.4.1", "description": "Generate API code or type annotations based on a GraphQL schema and statements", "repository": { "type": "git", @@ -52,15 +52,15 @@ "yargs": "^15.1.0" }, "devDependencies": { + "@types/babel-generator": "^6.25.0", "@types/common-tags": "^1.8.0", + "@types/fs-extra": "^8.1.0", "@types/glob": "^7.1.1", "@types/inflected": "^1.1.29", "@types/node": "^10.17.13", - "@types/yargs": "^15.0.1", - "@types/babel-generator": "^6.25.0", - "@types/fs-extra": "^8.1.0", "@types/prettier": "^1.19.0", - "@types/rimraf": "^3.0.0" + "@types/rimraf": "^3.0.0", + "@types/yargs": "^15.0.1" }, "publishConfig": { "access": "public" From 73d38776aaae270201aeddc87d6b97bef860c7f1 Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Mon, 25 Sep 2023 15:25:32 -0600 Subject: [PATCH 08/11] fix: relative types path in same dir as documents (#714) --- .../src/commands/statements.js | 6 ++-- .../src/utils/getRelativeTypesPath.js | 19 ++++++++++++ packages/amplify-codegen/src/utils/index.js | 2 ++ .../tests/commands/statements.test.js | 27 +++++++++++++--- .../tests/utils/getRelativeTypesPath.test.js | 31 +++++++++++++++++++ 5 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 packages/amplify-codegen/src/utils/getRelativeTypesPath.js create mode 100644 packages/amplify-codegen/tests/utils/getRelativeTypesPath.test.js diff --git a/packages/amplify-codegen/src/commands/statements.js b/packages/amplify-codegen/src/commands/statements.js index 86184b91d..522a605da 100644 --- a/packages/amplify-codegen/src/commands/statements.js +++ b/packages/amplify-codegen/src/commands/statements.js @@ -10,6 +10,7 @@ const { getAppSyncAPIDetails, readSchemaFromFile, getAppSyncAPIInfoFromProject, + getRelativeTypesPath, } = require('../utils'); const { generateGraphQLDocuments } = require('@aws-amplify/graphql-docs-generator'); const { generateStatements: generateStatementsHelper } = require('@aws-amplify/graphql-generator'); @@ -64,9 +65,6 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou try { const schemaData = readSchemaFromFile(schemaPath); - const relativeTypesPath = cfg.amplifyExtension.generatedFileName - ? path.relative(opsGenDirectory, cfg.amplifyExtension.generatedFileName) - : null; const generatedOps = generateStatementsHelper({ schema: schemaData, target: language, @@ -75,7 +73,7 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou // default typenameIntrospection to true when not set typenameIntrospection: cfg.amplifyExtension.typenameIntrospection === undefined ? true : !!cfg.amplifyExtension.typenameIntrospection, - relativeTypesPath, + relativeTypesPath: getRelativeTypesPath(opsGenDirectory, cfg.amplifyExtension.generatedFileName), }); if (!generatedOps) { context.print.warning('No GraphQL statements are generated. Check if the introspection schema has GraphQL operations defined.'); diff --git a/packages/amplify-codegen/src/utils/getRelativeTypesPath.js b/packages/amplify-codegen/src/utils/getRelativeTypesPath.js new file mode 100644 index 000000000..7d3f8cb5b --- /dev/null +++ b/packages/amplify-codegen/src/utils/getRelativeTypesPath.js @@ -0,0 +1,19 @@ +const path = require('path'); + +function getRelativeTypesPath(opsGenDirectory, generatedFileName) { + if (generatedFileName) { + const relativePath = path.relative(opsGenDirectory, generatedFileName); + + // generatedFileName is in same directory as opsGenDirectory + // i.e. generatedFileName: src/graphql/API.ts, opsGenDirectory: src/graphql + if (!relativePath.startsWith('.')) { + // path.join will strip prefixed ./ + return `./${relativePath}`; + } + + return relativePath; + } + return null; +} + +module.exports = getRelativeTypesPath; diff --git a/packages/amplify-codegen/src/utils/index.js b/packages/amplify-codegen/src/utils/index.js index 35dbbbbb8..25d2a1e89 100644 --- a/packages/amplify-codegen/src/utils/index.js +++ b/packages/amplify-codegen/src/utils/index.js @@ -17,6 +17,7 @@ const switchToSDLSchema = require('./switchToSDLSchema'); const ensureIntrospectionSchema = require('./ensureIntrospectionSchema'); const { readSchemaFromFile } = require('./readSchemaFromFile'); const defaultDirectiveDefinitions = require('./defaultDirectiveDefinitions'); +const getRelativeTypesPath = require('./getRelativeTypesPath'); module.exports = { getAppSyncAPIDetails, getFrontEndHandler, @@ -37,4 +38,5 @@ module.exports = { ensureIntrospectionSchema, readSchemaFromFile, defaultDirectiveDefinitions, + getRelativeTypesPath, }; diff --git a/packages/amplify-codegen/tests/commands/statements.test.js b/packages/amplify-codegen/tests/commands/statements.test.js index f1e8a5102..21440dc71 100644 --- a/packages/amplify-codegen/tests/commands/statements.test.js +++ b/packages/amplify-codegen/tests/commands/statements.test.js @@ -3,8 +3,15 @@ const fs = require('fs-extra'); const { loadConfig } = require('../../src/codegen-config'); const generateStatements = require('../../src/commands/statements'); +const { generateStatements: generateStatementsHelper } = require('@aws-amplify/graphql-generator'); const constants = require('../../src/constants'); -const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails, readSchemaFromFile } = require('../../src/utils'); +const { + ensureIntrospectionSchema, + getFrontEndHandler, + getAppSyncAPIDetails, + readSchemaFromFile, + getRelativeTypesPath, +} = require('../../src/utils'); const MOCK_CONTEXT = { print: { @@ -16,6 +23,7 @@ const MOCK_CONTEXT = { }, }; +jest.mock('@aws-amplify/graphql-generator'); jest.mock('../../src/codegen-config'); jest.mock('../../src/utils'); jest.mock('fs-extra'); @@ -67,21 +75,32 @@ describe('command - statements', () => { MOCK_CONTEXT.amplify.getEnvInfo.mockReturnValue({ projectPath: MOCK_PROJECT_ROOT }); getAppSyncAPIDetails.mockReturnValue(MOCK_APIS); readSchemaFromFile.mockReturnValue(MOCK_SCHEMA); + generateStatementsHelper.mockReturnValue({ + 'queries.js': 'queries', + }); }); it('should generate statements', async () => { const forceDownload = false; + const relativePath = './relative_path'; + getRelativeTypesPath.mockReturnValueOnce(relativePath); await generateStatements(MOCK_CONTEXT, forceDownload); expect(getFrontEndHandler).toHaveBeenCalledWith(MOCK_CONTEXT); expect(loadConfig).toHaveBeenCalledWith(MOCK_CONTEXT, false); + expect(getRelativeTypesPath).toHaveBeenCalledWith('MOCK_PROJECT_ROOT/MOCK_STATEMENTS_PATH', 'API.TS'); + expect(generateStatementsHelper).toHaveBeenCalledWith({ + relativeTypesPath: relativePath, + schema: MOCK_SCHEMA, + target: MOCK_TARGET_LANGUAGE, + typenameIntrospection: true, + useExternalFragmentForS3Object: false, + }); }); it('should generate graphql statements for non JS projects', async () => { getFrontEndHandler.mockReturnValue('ios'); loadConfig.mockReturnValue({ - getProjects: jest.fn().mockReturnValue([ - { ...MOCK_PROJECT, ...{ amplifyExtension: { codeGenTarget: 'javascript' }} } - ]), + getProjects: jest.fn().mockReturnValue([{ ...MOCK_PROJECT, ...{ amplifyExtension: { codeGenTarget: 'javascript' } } }]), }); const forceDownload = false; await generateStatements(MOCK_CONTEXT, forceDownload); diff --git a/packages/amplify-codegen/tests/utils/getRelativeTypesPath.test.js b/packages/amplify-codegen/tests/utils/getRelativeTypesPath.test.js new file mode 100644 index 000000000..56bd46a9b --- /dev/null +++ b/packages/amplify-codegen/tests/utils/getRelativeTypesPath.test.js @@ -0,0 +1,31 @@ +const getRelativeTypesPath = require('../../src/utils/getRelativeTypesPath'); + +describe('getRelativeTypesPath', () => { + test('in same directory', () => { + expect(getRelativeTypesPath('src/graphql', 'src/graphql/API.ts')).toEqual('./API.ts'); + }); + + test('one dir up', () => { + expect(getRelativeTypesPath('src/graphql', 'src/API.ts')).toEqual('../API.ts'); + }); + + test('two dir up', () => { + expect(getRelativeTypesPath('src/graphql', 'API.ts')).toEqual('../../API.ts'); + }); + + test('one dir down', () => { + expect(getRelativeTypesPath('src/graphql', 'src/graphql/types/API.ts')).toEqual('./types/API.ts'); + }); + + test('two dir down', () => { + expect(getRelativeTypesPath('src/graphql', 'src/graphql/types/foo/API.ts')).toEqual('./types/foo/API.ts'); + }); + + test('sibling dirs', () => { + expect(getRelativeTypesPath('src/graphql', 'src/types/API.ts')).toEqual('../types/API.ts'); + }); + + test('no types file', () => { + expect(getRelativeTypesPath('src/graphql', null)).toEqual(null); + }); +}); From 348f87c5372a19156f532ff161cf93e8beffe94d Mon Sep 17 00:00:00 2001 From: amplify-data-ci Date: Mon, 25 Sep 2023 22:40:16 +0000 Subject: [PATCH 09/11] chore(release): Publish [ci skip] - amplify-codegen@4.6.2 --- packages/amplify-codegen/CHANGELOG.md | 6 ++++++ packages/amplify-codegen/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/amplify-codegen/CHANGELOG.md b/packages/amplify-codegen/CHANGELOG.md index 7b019771d..05815ef37 100644 --- a/packages/amplify-codegen/CHANGELOG.md +++ b/packages/amplify-codegen/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.6.2](https://github.com/aws-amplify/amplify-codegen/compare/amplify-codegen@4.6.1...amplify-codegen@4.6.2) (2023-09-25) + +### Bug Fixes + +- relative types path in same dir as documents ([#714](https://github.com/aws-amplify/amplify-codegen/issues/714)) ([73d3877](https://github.com/aws-amplify/amplify-codegen/commit/73d38776aaae270201aeddc87d6b97bef860c7f1)) + ## [4.6.1](https://github.com/aws-amplify/amplify-codegen/compare/amplify-codegen@4.6.0...amplify-codegen@4.6.1) (2023-09-25) ### Bug Fixes diff --git a/packages/amplify-codegen/package.json b/packages/amplify-codegen/package.json index 858511cc3..568acc99d 100644 --- a/packages/amplify-codegen/package.json +++ b/packages/amplify-codegen/package.json @@ -1,6 +1,6 @@ { "name": "amplify-codegen", - "version": "4.6.1", + "version": "4.6.2", "description": "Amplify Code Generator", "repository": { "type": "git", From e33eb4a52aec37caac93164f7af575fda66c2fae Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Tue, 26 Sep 2023 12:41:28 -0700 Subject: [PATCH 10/11] chore: enable no-extraneous-dependencies lint rule and fix offenders (#719) --- .eslintrc.js | 2 +- packages/amplify-codegen-e2e-core/package.json | 4 ++++ packages/amplify-codegen-e2e-tests/package.json | 3 +++ packages/amplify-codegen/package.json | 2 ++ packages/appsync-modelgen-plugin/package.json | 10 +++++++--- packages/graphql-generator/package.json | 3 +++ packages/graphql-types-generator/package.json | 5 ++++- 7 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 955c70e00..a91367dba 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -190,7 +190,7 @@ module.exports = { '@typescript-eslint/interface-name-prefix': 'off', 'no-throw-literal': 'off', 'react/static-property-placement': 'off', - 'import/no-extraneous-dependencies': 'off', + 'import/no-extraneous-dependencies': ['error', {devDependencies: true}], 'spaced-comment': 'off', '@typescript-eslint/no-array-constructor': 'off', 'prefer-rest-params': 'off', diff --git a/packages/amplify-codegen-e2e-core/package.json b/packages/amplify-codegen-e2e-core/package.json index 180609a3f..43379dd52 100644 --- a/packages/amplify-codegen-e2e-core/package.json +++ b/packages/amplify-codegen-e2e-core/package.json @@ -22,10 +22,14 @@ "clean": "rimraf ./lib" }, "dependencies": { + "aws-sdk": "^2.1465.0", "chalk": "^3.0.0", + "dotenv": "^8.6.0", "execa": "^4.1.0", "fs-extra": "^8.1.0", + "glob": "^10.3.9", "ini": "^3.0.1", + "jest-circus": "^27.5.1", "jest-environment-node": "^26.6.2", "lodash": "^4.17.19", "node-pty": "beta", diff --git a/packages/amplify-codegen-e2e-tests/package.json b/packages/amplify-codegen-e2e-tests/package.json index 078218555..d9290b06a 100644 --- a/packages/amplify-codegen-e2e-tests/package.json +++ b/packages/amplify-codegen-e2e-tests/package.json @@ -24,6 +24,7 @@ "dependencies": { "@aws-amplify/amplify-codegen-e2e-core": "1.6.0", "@aws-amplify/graphql-schema-test-library": "^1.1.18", + "amazon-cognito-identity-js": "^6.3.6", "aws-amplify": "^5.3.3", "aws-appsync": "^4.1.9", "aws-sdk": "^2.1413.0", @@ -33,7 +34,9 @@ "graphql-tag": "^2.10.1", "js-yaml": "^4.0.0", "lodash": "^4.17.19", + "node-fetch": "^3.3.2", "uuid": "^3.4.0", + "ws": "^8.14.2", "yargs": "^15.1.0" }, "devDependencies": { diff --git a/packages/amplify-codegen/package.json b/packages/amplify-codegen/package.json index 568acc99d..01a0a07c3 100644 --- a/packages/amplify-codegen/package.json +++ b/packages/amplify-codegen/package.json @@ -23,7 +23,9 @@ "dependencies": { "@aws-amplify/graphql-generator": "0.1.2", "@aws-amplify/graphql-types-generator": "3.4.1", + "@aws-amplify/graphql-docs-generator": "4.2.0", "@graphql-codegen/core": "2.6.6", + "aws-sdk": "^2.1465.0", "chalk": "^3.0.0", "fs-extra": "^8.1.0", "glob-all": "^3.1.0", diff --git a/packages/appsync-modelgen-plugin/package.json b/packages/appsync-modelgen-plugin/package.json index e4fc1cd67..5855859c2 100644 --- a/packages/appsync-modelgen-plugin/package.json +++ b/packages/appsync-modelgen-plugin/package.json @@ -29,21 +29,25 @@ "@graphql-codegen/plugin-helpers": "^1.18.8", "@graphql-codegen/visitor-plugin-common": "^1.22.0", "@graphql-tools/utils": "^6.0.18", + "ajv": "^6.10.0", "chalk": "^3.0.0", "change-case": "^4.1.1", + "graphql-transformer-common": "^4.25.1", "lower-case-first": "^2.0.1", "pluralize": "^8.0.0", "strip-indent": "^3.0.0", - "ts-dedent": "^1.1.0" + "ts-dedent": "^1.1.0", + "ts-json-schema-generator": "1.0.0" }, "devDependencies": { "@graphql-codegen/testing": "^1.17.7", "@graphql-codegen/typescript": "^2.8.3", + "@types/fs-extra": "^8.1.2", "@types/node": "^12.12.6", "@types/pluralize": "0.0.29", + "graphql": "^15.5.0", - "java-ast": "^0.3.0", - "ts-json-schema-generator": "1.0.0" + "java-ast": "^0.3.0" }, "peerDependencies": { "graphql": "^15.5.0" diff --git a/packages/graphql-generator/package.json b/packages/graphql-generator/package.json index 10a63a00b..d354456fe 100644 --- a/packages/graphql-generator/package.json +++ b/packages/graphql-generator/package.json @@ -31,6 +31,9 @@ "@graphql-tools/apollo-engine-loader": "^8.0.0", "graphql": "^15.5.0" }, + "devDependencies": { + "@types/prettier": "^1.0.0" + }, "typescript": { "definition": "lib/index.d.ts" }, diff --git a/packages/graphql-types-generator/package.json b/packages/graphql-types-generator/package.json index d9e821f72..8500b934d 100644 --- a/packages/graphql-types-generator/package.json +++ b/packages/graphql-types-generator/package.json @@ -35,8 +35,10 @@ "extract-api": "ts-node ../../scripts/extract-api.ts" }, "dependencies": { + "@aws-amplify/api": "^5.4.5", "@babel/generator": "7.0.0-beta.4", "@babel/types": "7.0.0-beta.4", + "aws-amplify": "^5.3.11", "babel-generator": "^6.26.1", "babel-types": "^6.26.0", "change-case": "^4.1.1", @@ -60,7 +62,8 @@ "@types/node": "^10.17.13", "@types/prettier": "^1.19.0", "@types/rimraf": "^3.0.0", - "@types/yargs": "^15.0.1" + "@types/yargs": "^15.0.1", + "@types/zen-observable": "^0.8.4" }, "publishConfig": { "access": "public" From 1e484afe39a76ac633208698e3f780214819e44e Mon Sep 17 00:00:00 2001 From: Dane Pilcher Date: Tue, 26 Sep 2023 14:02:28 -0600 Subject: [PATCH 11/11] fix: generate multiple swift files in graphql-generator (#718) --- .../amplify-codegen/src/commands/types.js | 49 +- .../tests/commands/types.test.js | 21 +- packages/graphql-generator/API.md | 3 +- .../__snapshots__/types.test.ts.snap | 897 +++++------------- .../src/__tests__/types.test.ts | 35 +- packages/graphql-generator/src/types.ts | 2 +- packages/graphql-generator/src/typescript.ts | 3 +- packages/graphql-types-generator/API.md | 4 +- .../graphql-types-generator/src/generate.ts | 8 +- 9 files changed, 306 insertions(+), 716 deletions(-) diff --git a/packages/amplify-codegen/src/commands/types.js b/packages/amplify-codegen/src/commands/types.js index ee28bf6ff..c1bd220af 100644 --- a/packages/amplify-codegen/src/commands/types.js +++ b/packages/amplify-codegen/src/commands/types.js @@ -2,12 +2,13 @@ const path = require('path'); const fs = require('fs-extra'); const Ora = require('ora'); const glob = require('glob-all'); +const { Source } = require('graphql'); const constants = require('../constants'); const { loadConfig } = require('../codegen-config'); const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails, getAppSyncAPIInfoFromProject } = require('../utils'); const { generateTypes: generateTypesHelper } = require('@aws-amplify/graphql-generator'); -const { generate, extractDocumentFromJavascript } = require('@aws-amplify/graphql-types-generator'); +const { extractDocumentFromJavascript } = require('@aws-amplify/graphql-types-generator'); async function generateTypes(context, forceDownloadSchema, withoutInit = false, decoupleFrontend = '') { let frontend = decoupleFrontend; @@ -61,7 +62,7 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, cwd: projectPath, absolute: true, }); - const queryFiles = queryFilePaths.map(queryFilePath => { + const queries = queryFilePaths.map(queryFilePath => { const fileContents = fs.readFileSync(queryFilePath, 'utf8'); if ( queryFilePath.endsWith('.jsx') || @@ -71,12 +72,11 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, ) { return extractDocumentFromJavascript(fileContents, ''); } - return fileContents; + return new Source(fileContents, queryFilePath); }); - if (queryFiles.length === 0) { + if (queries.length === 0) { throw new Error("No queries found to generate types for, you may need to run 'codegen statements' first"); } - const queries = queryFiles.join('\n'); const schemaPath = path.join(projectPath, cfg.schema); @@ -93,32 +93,25 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, codeGenSpinner.start(); const schema = fs.readFileSync(schemaPath, 'utf8'); const introspection = path.extname(schemaPath) === '.json'; - + const multipleSwiftFiles = target === 'swift' && fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory(); try { - if (target === 'swift' && fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory()) { - generate(queryFilePaths, schemaPath, outputPath, '', target, '', { - addTypename: true, - complexObjectSupport: 'auto', - }); + const output = await generateTypesHelper({ + schema, + queries, + target, + introspection, + multipleSwiftFiles, + }); + const outputs = Object.entries(output); + + const outputPath = path.join(projectPath, generatedFileName); + if (outputs.length === 1) { + const [[, contents]] = outputs; + fs.outputFileSync(path.resolve(outputPath), contents); } else { - const output = await generateTypesHelper({ - schema, - queries, - target, - introspection, - multipleSwiftFiles: false, + outputs.forEach(([filepath, contents]) => { + fs.outputFileSync(path.resolve(path.join(outputPath, filepath)), contents); }); - const outputs = Object.entries(output); - - const outputPath = path.join(projectPath, generatedFileName); - if (outputs.length === 1) { - const [[, contents]] = outputs; - fs.outputFileSync(path.resolve(outputPath), contents); - } else { - outputs.forEach(([filepath, contents]) => { - fs.outputFileSync(path.resolve(path.join(outputPath, filepath)), contents); - }); - } } codeGenSpinner.succeed(`${constants.INFO_MESSAGE_CODEGEN_GENERATE_SUCCESS} ${path.relative(path.resolve('.'), outputPath)}`); } catch (err) { diff --git a/packages/amplify-codegen/tests/commands/types.test.js b/packages/amplify-codegen/tests/commands/types.test.js index b492e0d55..c885209c1 100644 --- a/packages/amplify-codegen/tests/commands/types.test.js +++ b/packages/amplify-codegen/tests/commands/types.test.js @@ -1,8 +1,8 @@ const { sync } = require('glob-all'); const path = require('path'); const { generateTypes: generateTypesHelper } = require('@aws-amplify/graphql-generator'); -const { generate: legacyGenerate } = require('@aws-amplify/graphql-types-generator'); const fs = require('fs-extra'); +const { Source } = require('graphql'); const { loadConfig } = require('../../src/codegen-config'); const generateTypes = require('../../src/commands/types'); @@ -60,6 +60,9 @@ describe('command - types', () => { beforeEach(() => { jest.clearAllMocks(); fs.existsSync.mockReturnValue(true); + fs.statSync.mockReturnValue({ + isDirectory: jest.fn().mockReturnValue(false), + }); getFrontEndHandler.mockReturnValue('javascript'); loadConfig.mockReturnValue({ getProjects: jest.fn().mockReturnValue([MOCK_PROJECT]), @@ -79,7 +82,7 @@ describe('command - types', () => { expect(loadConfig).toHaveBeenCalledWith(MOCK_CONTEXT, false); expect(sync).toHaveBeenCalledWith([MOCK_INCLUDE_PATH, `!${MOCK_EXCLUDE_PATH}`], { cwd: MOCK_PROJECT_ROOT, absolute: true }); expect(generateTypesHelper).toHaveBeenCalledWith({ - queries: 'query 1\nquery 2', + queries: [new Source('query 1', 'q1.gql'), new Source('query 2', 'q2.gql')], schema: 'schema', target: 'TYPE_SCRIPT_OR_FLOW_OR_ANY_OTHER_LANGUAGE', introspection: false, @@ -87,7 +90,7 @@ describe('command - types', () => { }); }); - it('should use legacy types generation when generating multiple swift files', async () => { + it('should use generate multiple swift files', async () => { MOCK_PROJECT.amplifyExtension.codeGenTarget = 'swift'; MOCK_PROJECT.amplifyExtension.generatedFileName = 'typesDirectory'; const forceDownload = false; @@ -100,16 +103,6 @@ describe('command - types', () => { isDirectory: jest.fn().mockReturnValue(true), }); await generateTypes(MOCK_CONTEXT, forceDownload); - expect(generateTypesHelper).not.toHaveBeenCalled(); - expect(legacyGenerate).toHaveBeenCalledWith( - ['q1.gql', 'q2.gql'], - 'MOCK_PROJECT_ROOT/INTROSPECTION_SCHEMA.JSON', - 'MOCK_PROJECT_ROOT/typesDirectory', - '', - 'swift', - '', - { addTypename: true, complexObjectSupport: 'auto' }, - ); }); it('should not generate type if the frontend is android', async () => { @@ -121,6 +114,7 @@ describe('command - types', () => { it('should download the schema if forceDownload flag is passed', async () => { const forceDownload = true; + fs.readFileSync.mockReturnValueOnce('query 1').mockReturnValueOnce('query 2'); await generateTypes(MOCK_CONTEXT, forceDownload); expect(ensureIntrospectionSchema).toHaveBeenCalledWith( MOCK_CONTEXT, @@ -134,6 +128,7 @@ describe('command - types', () => { it('should download the schema if the schema file is missing', async () => { fs.existsSync.mockReturnValue(false); const forceDownload = false; + fs.readFileSync.mockReturnValueOnce('query 1').mockReturnValueOnce('query 2'); await generateTypes(MOCK_CONTEXT, forceDownload); expect(ensureIntrospectionSchema).toHaveBeenCalledWith( MOCK_CONTEXT, diff --git a/packages/graphql-generator/API.md b/packages/graphql-generator/API.md index 75571257c..5af155a79 100644 --- a/packages/graphql-generator/API.md +++ b/packages/graphql-generator/API.md @@ -4,6 +4,7 @@ ```ts +import { Source } from 'graphql'; import { Target } from '@aws-amplify/appsync-modelgen-plugin'; import { Target as Target_2 } from '@aws-amplify/graphql-types-generator'; @@ -49,7 +50,7 @@ export function generateTypes(options: GenerateTypesOptions): Promise { - value: GraphQLResult; -} + public var id: GraphQLID -export type Blog = { - __typename: \\"Blog\\"; - id: string; - name: string; - posts?: ModelPostConnection | null; - createdAt: string; - updatedAt: string; -}; + public init(id: GraphQLID) { + self.id = id + } -export type ModelPostConnection = { - __typename: \\"ModelPostConnection\\"; - items: Array; - nextToken?: string | null; -}; + public var variables: GraphQLMap? { + return [\\"id\\": id] + } -export type Post = { - __typename: \\"Post\\"; - id: string; - title: string; - blog?: Blog | null; - comments?: ModelCommentConnection | null; - createdAt: string; - updatedAt: string; - blogPostsId?: string | null; -}; + public struct Data: GraphQLSelectionSet { + public static let possibleTypes = [\\"Query\\"] -export type ModelCommentConnection = { - __typename: \\"ModelCommentConnection\\"; - items: Array; - nextToken?: string | null; -}; + public static let selections: [GraphQLSelection] = [ + GraphQLField(\\"getBlog\\", arguments: [\\"id\\": GraphQLVariable(\\"id\\")], type: .object(GetBlog.selections)), + ] -export type Comment = { - __typename: \\"Comment\\"; - id: string; - post?: Post | null; - content: string; - createdAt: string; - updatedAt: string; - postCommentsId?: string | null; -}; + public var snapshot: Snapshot -export type GetBlogQuery = { - __typename: \\"Blog\\"; - id: string; - name: string; - posts?: { - __typename: \\"ModelPostConnection\\"; - nextToken?: string | null; - } | null; - createdAt: string; - updatedAt: string; -}; + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } -@Injectable({ - providedIn: \\"root\\" -}) -export class APIService { - async GetBlog(id: string): Promise { - const statement = \`query GetBlog($id: ID!) { - getBlog(id: $id) { - __typename - id - name - posts { - __typename - nextToken - } + public init(getBlog: GetBlog? = nil) { + self.init(snapshot: [\\"__typename\\": \\"Query\\", \\"getBlog\\": getBlog.flatMap { $0.snapshot }]) + } + + public var getBlog: GetBlog? { + get { + return (snapshot[\\"getBlog\\"] as? Snapshot).flatMap { GetBlog(snapshot: $0) } + } + set { + snapshot.updateValue(newValue?.snapshot, forKey: \\"getBlog\\") + } + } + + public struct GetBlog: GraphQLSelectionSet { + public static let possibleTypes = [\\"Blog\\"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField(\\"__typename\\", type: .nonNull(.scalar(String.self))), + GraphQLField(\\"id\\", type: .nonNull(.scalar(GraphQLID.self))), + GraphQLField(\\"name\\", type: .nonNull(.scalar(String.self))), + GraphQLField(\\"posts\\", type: .object(Post.selections)), + GraphQLField(\\"createdAt\\", type: .nonNull(.scalar(String.self))), + GraphQLField(\\"updatedAt\\", type: .nonNull(.scalar(String.self))), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(id: GraphQLID, name: String, posts: Post? = nil, createdAt: String, updatedAt: String) { + self.init(snapshot: [\\"__typename\\": \\"Blog\\", \\"id\\": id, \\"name\\": name, \\"posts\\": posts.flatMap { $0.snapshot }, \\"createdAt\\": createdAt, \\"updatedAt\\": updatedAt]) + } + + public var __typename: String { + get { + return snapshot[\\"__typename\\"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: \\"__typename\\") + } + } + + public var id: GraphQLID { + get { + return snapshot[\\"id\\"]! as! GraphQLID + } + set { + snapshot.updateValue(newValue, forKey: \\"id\\") + } + } + + public var name: String { + get { + return snapshot[\\"name\\"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: \\"name\\") + } + } + + public var posts: Post? { + get { + return (snapshot[\\"posts\\"] as? Snapshot).flatMap { Post(snapshot: $0) } + } + set { + snapshot.updateValue(newValue?.snapshot, forKey: \\"posts\\") + } + } + + public var createdAt: String { + get { + return snapshot[\\"createdAt\\"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: \\"createdAt\\") + } + } + + public var updatedAt: String { + get { + return snapshot[\\"updatedAt\\"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: \\"updatedAt\\") + } + } + + public struct Post: GraphQLSelectionSet { + public static let possibleTypes = [\\"ModelPostConnection\\"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField(\\"__typename\\", type: .nonNull(.scalar(String.self))), + GraphQLField(\\"nextToken\\", type: .scalar(String.self)), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(nextToken: String? = nil) { + self.init(snapshot: [\\"__typename\\": \\"ModelPostConnection\\", \\"nextToken\\": nextToken]) + } + + public var __typename: String { + get { + return snapshot[\\"__typename\\"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: \\"__typename\\") + } + } + + public var nextToken: String? { + get { + return snapshot[\\"nextToken\\"] as? String + } + set { + snapshot.updateValue(newValue, forKey: \\"nextToken\\") + } + } + } + } + } +}", +} +`; + +exports[`generateTypes targets basic angular 1`] = ` +Object { + "api.service.ts": "/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. +import { Injectable } from \\"@angular/core\\"; +import API, { graphqlOperation, GraphQLResult } from \\"@aws-amplify/api-graphql\\"; +import { Observable } from \\"zen-observable-ts\\"; + +export interface SubscriptionResponse { + value: GraphQLResult; +} + +export type Blog = { + __typename: \\"Blog\\"; + id: string; + name: string; + posts?: ModelPostConnection | null; + createdAt: string; + updatedAt: string; +}; + +export type ModelPostConnection = { + __typename: \\"ModelPostConnection\\"; + items: Array; + nextToken?: string | null; +}; + +export type Post = { + __typename: \\"Post\\"; + id: string; + title: string; + blog?: Blog | null; + comments?: ModelCommentConnection | null; + createdAt: string; + updatedAt: string; + blogPostsId?: string | null; +}; + +export type ModelCommentConnection = { + __typename: \\"ModelCommentConnection\\"; + items: Array; + nextToken?: string | null; +}; + +export type Comment = { + __typename: \\"Comment\\"; + id: string; + post?: Post | null; + content: string; + createdAt: string; + updatedAt: string; + postCommentsId?: string | null; +}; + +export type GetBlogQuery = { + __typename: \\"Blog\\"; + id: string; + name: string; + posts?: { + __typename: \\"ModelPostConnection\\"; + nextToken?: string | null; + } | null; + createdAt: string; + updatedAt: string; +}; + +@Injectable({ + providedIn: \\"root\\" +}) +export class APIService { + async GetBlog(id: string): Promise { + const statement = \`query GetBlog($id: ID!) { + getBlog(id: $id) { + __typename + id + name + posts { + __typename + nextToken + } createdAt updatedAt } @@ -1733,7 +1733,7 @@ object GetBlogQuery extends com.apollographql.scalajs.GraphQLQuery { exports[`generateTypes targets basic swift 1`] = ` Object { - "GraphQL request.swift": "// This file was automatically generated and should not be edited. + "API.swift": "// This file was automatically generated and should not be edited. #if canImport(AWSAPIPlugin) import Foundation @@ -2312,429 +2312,6 @@ public final class GetBlogQuery: GraphQLQuery { } } }", - "Types.graphql.swift": "// This file was automatically generated and should not be edited. - -#if canImport(AWSAPIPlugin) -import Foundation - -public protocol GraphQLInputValue { -} - -public struct GraphQLVariable { - let name: String - - public init(_ name: String) { - self.name = name - } -} - -extension GraphQLVariable: GraphQLInputValue { -} - -extension JSONEncodable { - public func evaluate(with variables: [String: JSONEncodable]?) throws -> Any { - return jsonValue - } -} - -public typealias GraphQLMap = [String: JSONEncodable?] - -extension Dictionary where Key == String, Value == JSONEncodable? { - public var withNilValuesRemoved: Dictionary { - var filtered = Dictionary(minimumCapacity: count) - for (key, value) in self { - if value != nil { - filtered[key] = value - } - } - return filtered - } -} - -public protocol GraphQLMapConvertible: JSONEncodable { - var graphQLMap: GraphQLMap { get } -} - -public extension GraphQLMapConvertible { - var jsonValue: Any { - return graphQLMap.withNilValuesRemoved.jsonValue - } -} - -public typealias GraphQLID = String - -public protocol APISwiftGraphQLOperation: AnyObject { - - static var operationString: String { get } - static var requestString: String { get } - static var operationIdentifier: String? { get } - - var variables: GraphQLMap? { get } - - associatedtype Data: GraphQLSelectionSet -} - -public extension APISwiftGraphQLOperation { - static var requestString: String { - return operationString - } - - static var operationIdentifier: String? { - return nil - } - - var variables: GraphQLMap? { - return nil - } -} - -public protocol GraphQLQuery: APISwiftGraphQLOperation {} - -public protocol GraphQLMutation: APISwiftGraphQLOperation {} - -public protocol GraphQLSubscription: APISwiftGraphQLOperation {} - -public protocol GraphQLFragment: GraphQLSelectionSet { - static var possibleTypes: [String] { get } -} - -public typealias Snapshot = [String: Any?] - -public protocol GraphQLSelectionSet: Decodable { - static var selections: [GraphQLSelection] { get } - - var snapshot: Snapshot { get } - init(snapshot: Snapshot) -} - -extension GraphQLSelectionSet { - public init(from decoder: Decoder) throws { - if let jsonObject = try? APISwiftJSONValue(from: decoder) { - let encoder = JSONEncoder() - let jsonData = try encoder.encode(jsonObject) - let decodedDictionary = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any] - let optionalDictionary = decodedDictionary.mapValues { $0 as Any? } - - self.init(snapshot: optionalDictionary) - } else { - self.init(snapshot: [:]) - } - } -} - -enum APISwiftJSONValue: Codable { - case array([APISwiftJSONValue]) - case boolean(Bool) - case number(Double) - case object([String: APISwiftJSONValue]) - case string(String) - case null - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - if let value = try? container.decode([String: APISwiftJSONValue].self) { - self = .object(value) - } else if let value = try? container.decode([APISwiftJSONValue].self) { - self = .array(value) - } else if let value = try? container.decode(Double.self) { - self = .number(value) - } else if let value = try? container.decode(Bool.self) { - self = .boolean(value) - } else if let value = try? container.decode(String.self) { - self = .string(value) - } else { - self = .null - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - switch self { - case .array(let value): - try container.encode(value) - case .boolean(let value): - try container.encode(value) - case .number(let value): - try container.encode(value) - case .object(let value): - try container.encode(value) - case .string(let value): - try container.encode(value) - case .null: - try container.encodeNil() - } - } -} - -public protocol GraphQLSelection { -} - -public struct GraphQLField: GraphQLSelection { - let name: String - let alias: String? - let arguments: [String: GraphQLInputValue]? - - var responseKey: String { - return alias ?? name - } - - let type: GraphQLOutputType - - public init(_ name: String, alias: String? = nil, arguments: [String: GraphQLInputValue]? = nil, type: GraphQLOutputType) { - self.name = name - self.alias = alias - - self.arguments = arguments - - self.type = type - } -} - -public indirect enum GraphQLOutputType { - case scalar(JSONDecodable.Type) - case object([GraphQLSelection]) - case nonNull(GraphQLOutputType) - case list(GraphQLOutputType) - - var namedType: GraphQLOutputType { - switch self { - case .nonNull(let innerType), .list(let innerType): - return innerType.namedType - case .scalar, .object: - return self - } - } -} - -public struct GraphQLBooleanCondition: GraphQLSelection { - let variableName: String - let inverted: Bool - let selections: [GraphQLSelection] - - public init(variableName: String, inverted: Bool, selections: [GraphQLSelection]) { - self.variableName = variableName - self.inverted = inverted; - self.selections = selections; - } -} - -public struct GraphQLTypeCondition: GraphQLSelection { - let possibleTypes: [String] - let selections: [GraphQLSelection] - - public init(possibleTypes: [String], selections: [GraphQLSelection]) { - self.possibleTypes = possibleTypes - self.selections = selections; - } -} - -public struct GraphQLFragmentSpread: GraphQLSelection { - let fragment: GraphQLFragment.Type - - public init(_ fragment: GraphQLFragment.Type) { - self.fragment = fragment - } -} - -public struct GraphQLTypeCase: GraphQLSelection { - let variants: [String: [GraphQLSelection]] - let \`default\`: [GraphQLSelection] - - public init(variants: [String: [GraphQLSelection]], default: [GraphQLSelection]) { - self.variants = variants - self.default = \`default\`; - } -} - -public typealias JSONObject = [String: Any] - -public protocol JSONDecodable { - init(jsonValue value: Any) throws -} - -public protocol JSONEncodable: GraphQLInputValue { - var jsonValue: Any { get } -} - -public enum JSONDecodingError: Error, LocalizedError { - case missingValue - case nullValue - case wrongType - case couldNotConvert(value: Any, to: Any.Type) - - public var errorDescription: String? { - switch self { - case .missingValue: - return \\"Missing value\\" - case .nullValue: - return \\"Unexpected null value\\" - case .wrongType: - return \\"Wrong type\\" - case .couldNotConvert(let value, let expectedType): - return \\"Could not convert \\\\\\"\\\\(value)\\\\\\" to \\\\(expectedType)\\" - } - } -} - -extension String: JSONDecodable, JSONEncodable { - public init(jsonValue value: Any) throws { - guard let string = value as? String else { - throw JSONDecodingError.couldNotConvert(value: value, to: String.self) - } - self = string - } - - public var jsonValue: Any { - return self - } -} - -extension Int: JSONDecodable, JSONEncodable { - public init(jsonValue value: Any) throws { - guard let number = value as? NSNumber else { - throw JSONDecodingError.couldNotConvert(value: value, to: Int.self) - } - self = number.intValue - } - - public var jsonValue: Any { - return self - } -} - -extension Float: JSONDecodable, JSONEncodable { - public init(jsonValue value: Any) throws { - guard let number = value as? NSNumber else { - throw JSONDecodingError.couldNotConvert(value: value, to: Float.self) - } - self = number.floatValue - } - - public var jsonValue: Any { - return self - } -} - -extension Double: JSONDecodable, JSONEncodable { - public init(jsonValue value: Any) throws { - guard let number = value as? NSNumber else { - throw JSONDecodingError.couldNotConvert(value: value, to: Double.self) - } - self = number.doubleValue - } - - public var jsonValue: Any { - return self - } -} - -extension Bool: JSONDecodable, JSONEncodable { - public init(jsonValue value: Any) throws { - guard let bool = value as? Bool else { - throw JSONDecodingError.couldNotConvert(value: value, to: Bool.self) - } - self = bool - } - - public var jsonValue: Any { - return self - } -} - -extension RawRepresentable where RawValue: JSONDecodable { - public init(jsonValue value: Any) throws { - let rawValue = try RawValue(jsonValue: value) - if let tempSelf = Self(rawValue: rawValue) { - self = tempSelf - } else { - throw JSONDecodingError.couldNotConvert(value: value, to: Self.self) - } - } -} - -extension RawRepresentable where RawValue: JSONEncodable { - public var jsonValue: Any { - return rawValue.jsonValue - } -} - -extension Optional where Wrapped: JSONDecodable { - public init(jsonValue value: Any) throws { - if value is NSNull { - self = .none - } else { - self = .some(try Wrapped(jsonValue: value)) - } - } -} - -extension Optional: JSONEncodable { - public var jsonValue: Any { - switch self { - case .none: - return NSNull() - case .some(let wrapped as JSONEncodable): - return wrapped.jsonValue - default: - fatalError(\\"Optional is only JSONEncodable if Wrapped is\\") - } - } -} - -extension Dictionary: JSONEncodable { - public var jsonValue: Any { - return jsonObject - } - - public var jsonObject: JSONObject { - var jsonObject = JSONObject(minimumCapacity: count) - for (key, value) in self { - if case let (key as String, value as JSONEncodable) = (key, value) { - jsonObject[key] = value.jsonValue - } else { - fatalError(\\"Dictionary is only JSONEncodable if Value is (and if Key is String)\\") - } - } - return jsonObject - } -} - -extension Array: JSONEncodable { - public var jsonValue: Any { - return map() { element -> (Any) in - if case let element as JSONEncodable = element { - return element.jsonValue - } else { - fatalError(\\"Array is only JSONEncodable if Element is\\") - } - } - } -} - -extension URL: JSONDecodable, JSONEncodable { - public init(jsonValue value: Any) throws { - guard let string = value as? String else { - throw JSONDecodingError.couldNotConvert(value: value, to: URL.self) - } - self.init(string: string)! - } - - public var jsonValue: Any { - return self.absoluteString - } -} - -extension Dictionary { - static func += (lhs: inout Dictionary, rhs: Dictionary) { - lhs.merge(rhs) { (_, new) in new } - } -} - -#elseif canImport(AWSAppSync) -import AWSAppSync -#endif", } `; diff --git a/packages/graphql-generator/src/__tests__/types.test.ts b/packages/graphql-generator/src/__tests__/types.test.ts index 6c668d276..63ecf6f23 100644 --- a/packages/graphql-generator/src/__tests__/types.test.ts +++ b/packages/graphql-generator/src/__tests__/types.test.ts @@ -1,3 +1,4 @@ +import { Source } from 'graphql'; import { generateTypes, GenerateTypesOptions, TypesTarget } from '..'; import { readSchema } from './utils'; @@ -37,15 +38,31 @@ describe('generateTypes', () => { }); }); - test('multipleSwiftFiles', async () => { - const options: GenerateTypesOptions = { - schema: sdlSchema, - queries, - target: 'swift', - multipleSwiftFiles: true, - }; + describe('multipleSwiftFiles', () => { + test('generates multiple files', async () => { + const filename = 'queries.graphql'; + const options: GenerateTypesOptions = { + schema: sdlSchema, + queries: [new Source(queries, filename)], + target: 'swift', + multipleSwiftFiles: true, + }; - const types = await generateTypes(options); - expect(types).toMatchSnapshot(); + const types = await generateTypes(options); + expect(Object.keys(types)).toEqual(['Types.graphql.swift', `${filename}.swift`]); + expect(types).toMatchSnapshot(); + }); + + test('throws error if not using Source', async () => { + const filename = 'queries.graphql'; + const options: GenerateTypesOptions = { + schema: sdlSchema, + queries: queries, + target: 'swift', + multipleSwiftFiles: true, + }; + + expect(generateTypes(options)).rejects.toThrow('Query documents must be of type Source[] when generating multiple Swift files.'); + }); }); }); diff --git a/packages/graphql-generator/src/types.ts b/packages/graphql-generator/src/types.ts index e7da18f92..bbae55687 100644 --- a/packages/graphql-generator/src/types.ts +++ b/packages/graphql-generator/src/types.ts @@ -3,7 +3,7 @@ import { generate, generateFromString } from '@aws-amplify/graphql-types-generat import { GenerateTypesOptions, GeneratedOutput } from './typescript'; export async function generateTypes(options: GenerateTypesOptions): Promise { - const { schema, target, queries, multipleSwiftFiles = true, introspection = false } = options; + const { schema, target, queries, multipleSwiftFiles = false, introspection = false } = options; const generatedOutput = await generateFromString(schema, introspection, queries, target, multipleSwiftFiles, { addTypename: true, diff --git a/packages/graphql-generator/src/typescript.ts b/packages/graphql-generator/src/typescript.ts index 035b44bc6..1d83ee922 100644 --- a/packages/graphql-generator/src/typescript.ts +++ b/packages/graphql-generator/src/typescript.ts @@ -1,3 +1,4 @@ +import { Source } from 'graphql'; import { Target as GraphqlTypesGeneratorTarget } from '@aws-amplify/graphql-types-generator'; import { Target as AppsyncModelgenPluginTarget } from '@aws-amplify/appsync-modelgen-plugin'; @@ -10,7 +11,7 @@ export type FileExtension = 'js' | 'graphql' | 'ts'; export type GenerateTypesOptions = { schema: string; target: TypesTarget; - queries: string; + queries: string | Source[]; introspection?: boolean; multipleSwiftFiles?: boolean; // only used when target is swift }; diff --git a/packages/graphql-types-generator/API.md b/packages/graphql-types-generator/API.md index f37264a4b..70845b871 100644 --- a/packages/graphql-types-generator/API.md +++ b/packages/graphql-types-generator/API.md @@ -4,6 +4,8 @@ ```ts +import { Source } from 'graphql'; + // @public (undocumented) export function extractDocumentFromJavascript(content: string, tagName?: string): string | null; @@ -11,7 +13,7 @@ export function extractDocumentFromJavascript(content: string, tagName?: string) export function generate(inputPaths: string[], schemaPath: string, outputPath: string, only: string, target: Target, tagName: string, options: any): void; // @public (undocumented) -export function generateFromString(schema: string, introspection: boolean, queryDocuments: string, target: Target, multipleSwiftFiles: boolean, options: any): { +export function generateFromString(schema: string, introspection: boolean, queryDocuments: string | Source[], target: Target, multipleSwiftFiles: boolean, options: any): { [filepath: string]: string; }; diff --git a/packages/graphql-types-generator/src/generate.ts b/packages/graphql-types-generator/src/generate.ts index 26c7474eb..b60d8d589 100644 --- a/packages/graphql-types-generator/src/generate.ts +++ b/packages/graphql-types-generator/src/generate.ts @@ -84,13 +84,17 @@ function generateFromFile( export function generateFromString( schema: string, introspection: boolean, - queryDocuments: string, + queryDocuments: string | Source[], target: Target, multipleSwiftFiles: boolean, options: any, ): { [filepath: string]: string } { + if (typeof queryDocuments === 'string' && multipleSwiftFiles) { + throw new Error('Query documents must be of type Source[] when generating multiple Swift files.'); + } const graphqlSchema = parseSchema(schema, introspection); - const document = parseAndMergeQueryDocuments([new Source(queryDocuments)]); + const queryDocumentSources = typeof queryDocuments === 'string' ? [new Source(queryDocuments)] : queryDocuments; + const document = parseAndMergeQueryDocuments(queryDocumentSources); validateQueryDocument(graphqlSchema, document); const output = generateForTarget(graphqlSchema, document, '', target, multipleSwiftFiles, options);