diff --git a/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/generate-ts-data-schema.test.ts.snap b/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/generate-ts-data-schema.test.ts.snap index 5b490bef0d..6dadb2dbcf 100644 --- a/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/generate-ts-data-schema.test.ts.snap +++ b/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/generate-ts-data-schema.test.ts.snap @@ -245,6 +245,31 @@ export const schema = configure({ " `; +exports[`Type name conversions should annotate scalar int fields with existing default with \`.default()\` 1`] = ` +"/* eslint-disable */ +/* THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. */ +import { a } from \\"@aws-amplify/data-schema\\"; +import { configure } from \\"@aws-amplify/data-schema/internals\\"; +import { secret } from \\"@aws-amplify/backend\\"; + +export const schema = configure({ + database: { + identifier: \\"ID1234567890\\", + engine: \\"postgresql\\", + connectionUri: secret(\\"CONN_STR\\") + } +}).schema({ + \\"CoffeeQueue\\": a.model({ + id: a.integer().default(), + name: a.string(), + orderNumber: a.integer().default() + }).identifier([ + \\"id\\" + ]) +}); +" +`; + exports[`Type name conversions ts schema generator should invoke generate schema 1`] = ` "/* eslint-disable */ /* THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. */ diff --git a/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/generate.test.ts.snap b/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/generate.test.ts.snap index 1cfbcbe6cd..b4120bc9c7 100644 --- a/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/generate.test.ts.snap +++ b/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/generate.test.ts.snap @@ -145,6 +145,19 @@ type Story @refersTo(name: \\"stories\\") @model { " `; +exports[`generate graphqlSchemaFromSQLSchema creates graphql schema from "postgres" "serial" schema 1`] = ` +"input AMPLIFY { + engine: String = \\"postgres\\" +} + + +type SerialTable @refersTo(name: \\"serial_table\\") @model { + id: Int! @primaryKey + number: Int +} +" +`; + exports[`generate graphqlSchemaFromSQLSchema creates graphql schema from "postgres" "todo" schema 1`] = ` "input AMPLIFY { engine: String = \\"postgres\\" diff --git a/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/pg-string-datasource-adapter.test.ts.snap b/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/pg-string-datasource-adapter.test.ts.snap index 59f82fe959..075c5fdf09 100644 --- a/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/pg-string-datasource-adapter.test.ts.snap +++ b/packages/amplify-graphql-schema-generator/src/__tests__/__snapshots__/pg-string-datasource-adapter.test.ts.snap @@ -1,5 +1,52 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`testPostgresStringDataSourceAdapter annotates the model field correctly from a schema with a SERIAL field 1`] = ` +Array [ + Model { + "fields": Array [ + Object { + "default": Object { + "kind": "DB_GENERATED", + "value": "nextval('serial_table_id_seq'::regclass)", + }, + "length": "Null", + "name": "id", + "type": Object { + "kind": "NonNull", + "type": Object { + "kind": "Scalar", + "name": "Int", + }, + }, + }, + Object { + "default": Object { + "kind": "DB_GENERATED", + "value": "nextval('serial_table_number_seq'::regclass)", + }, + "length": "Null", + "name": "number", + "type": Object { + "kind": "NonNull", + "type": Object { + "kind": "Scalar", + "name": "Int", + }, + }, + }, + ], + "indexes": Array [], + "name": "serial_table", + "primaryKey": Index { + "fields": Array [ + "id", + ], + "name": "PRIMARY_KEY", + }, + }, +] +`; + exports[`testPostgresStringDataSourceAdapter sets the correct models from a news schema 1`] = ` Array [ Model { diff --git a/packages/amplify-graphql-schema-generator/src/__tests__/__utils__/schemas.ts b/packages/amplify-graphql-schema-generator/src/__tests__/__utils__/schemas.ts index df7763b2ce..ad6396e5e0 100644 --- a/packages/amplify-graphql-schema-generator/src/__tests__/__utils__/schemas.ts +++ b/packages/amplify-graphql-schema-generator/src/__tests__/__utils__/schemas.ts @@ -155,5 +155,15 @@ comments,comment_text,NULL,4,varchar,varchar(30),NO,30,NULL,NULL,NULL,NULL`, "stories",NULL,NULL,"placeholder",NULL,3,"character","bpchar","NO",1,NULL,NULL,NULL "stories",NULL,NULL,"pub_id",NULL,1,"integer","int4","NO",NULL,"stories_pkey","PRIMARY KEY","pub_id, pub_type" "stories",NULL,NULL,"pub_type","'S'::bpchar",2,"character","bpchar","NO",1,"stories_pkey","PRIMARY KEY","pub_id, pub_type"`, + /* + * CREATE TABLE serial_table ( + * id SERIAL PRIMARY KEY, + * number SERIAL, + * ); + */ + serial: `"table_name","enum_name","enum_values","column_name","column_default","ordinal_position","data_type","udt_name","is_nullable","character_maximum_length","indexname","constraint_type","index_columns" +"serial_table","Null","Null","id","nextval('serial_table_id_seq'::regclass)","1","integer","int4","NO","Null","serial_table_pkey","PRIMARY KEY","id" +"serial_table","Null","Null","number","nextval('serial_table_number_seq'::regclass)","2","integer","int4","NO","Null","Null","Null", + `, }, }; diff --git a/packages/amplify-graphql-schema-generator/src/__tests__/generate-ts-data-schema.test.ts b/packages/amplify-graphql-schema-generator/src/__tests__/generate-ts-data-schema.test.ts index d9d71738c6..f73d78b019 100644 --- a/packages/amplify-graphql-schema-generator/src/__tests__/generate-ts-data-schema.test.ts +++ b/packages/amplify-graphql-schema-generator/src/__tests__/generate-ts-data-schema.test.ts @@ -249,6 +249,108 @@ describe('Type name conversions', () => { expect(graphqlSchema).toMatchSnapshot(); }); + it.each([ + { + case: 'string', + field: () => { + const f = new Field('field', { kind: 'Scalar', name: 'String' }); + f.default = { kind: 'DB_GENERATED', value: 'A squat grey building of only thirty-four stouries' }; + return f; + }, + }, + { + case: 'Float', + field: () => { + const f = new Field('field', { kind: 'Scalar', name: 'Float' }); + f.default = { kind: 'DB_GENERATED', value: 3.14 }; + return f; + }, + }, + { + case: 'List', + field: () => { + const f = new Field('field', { kind: 'List', type: { kind: 'Scalar', name: 'String' } }); + f.default = { kind: 'DB_GENERATED', value: false }; + return f; + }, + }, + { + case: 'CustomType', + field: () => { + const f = new Field('field', { kind: 'Custom', name: 'MyCustomType' }); + f.default = { kind: 'DB_GENERATED', value: 'I could make of both names nothing longer or more explicit than Pip' }; + return f; + }, + }, + { + case: 'Transformer Generated', + field: () => { + const f = new Field('field', { kind: 'Scalar', name: 'Int' }); + f.default = { kind: 'TRANSFORMER_GENERATED', value: 42 }; + return f; + }, + }, + { + case: 'Default Integer Constant', + field: () => { + const f = new Field('field', { kind: 'Scalar', name: 'Int' }); + f.default = { kind: 'DB_GENERATED', value: 42 }; + return f; + }, + }, + { + case: 'No default', + field: () => new Field('field', { kind: 'Scalar', name: 'Int' }), + }, + ])('should not annotate fields with `.default()` where we do not support db generation (case: %case)', (test) => { + const dbschema = new Schema(new Engine('Postgres')); + const model = new Model('User'); + model.addField(new Field('id', { kind: 'NonNull', type: { kind: 'Scalar', name: 'String' } })); + model.setPrimaryKey(['id']); + + model.addField(test.field()); + + dbschema.addModel(model); + const config: DataSourceGenerateConfig = { + identifier: 'ID1234567890', + secretNames: { + connectionUri: 'CONN_STR', + }, + }; + + const graphqlSchema = generateTypescriptDataSchema(dbschema, config); + const containsDefault = graphqlSchema.includes('default()'); + expect(containsDefault).toBe(false); + }); + + it('should annotate scalar int fields with existing default with `.default()`', async () => { + const dbschema = new Schema(new Engine('Postgres')); + + const model = new Model('CoffeeQueue'); + + const serialPKField = new Field('id', { kind: 'NonNull', type: { kind: 'Scalar', name: 'Int' } }); + serialPKField.default = { kind: 'DB_GENERATED', value: "nextval('coffeequeue_id_seq'::regclass)" }; + model.addField(serialPKField); + model.setPrimaryKey(['id']); + + model.addField(new Field('name', { kind: 'Scalar', name: 'String' })); + + const serialField = new Field('orderNumber', { kind: 'Scalar', name: 'Int' }); + serialField.default = { kind: 'DB_GENERATED', value: "nextval('coffeequeue_ordernumber_seq'::regclass)" }; + model.addField(serialField); + + dbschema.addModel(model); + const config: DataSourceGenerateConfig = { + identifier: 'ID1234567890', + secretNames: { + connectionUri: 'CONN_STR', + }, + }; + + const graphqlSchema = generateTypescriptDataSchema(dbschema, config); + expect(graphqlSchema).toMatchSnapshot(); + }); + it('schema with database config without vpc should generate typescript data schema with configure', () => { const dbschema = new Schema(new Engine('MySQL')); let model = new Model('User'); diff --git a/packages/amplify-graphql-schema-generator/src/__tests__/pg-string-datasource-adapter.test.ts b/packages/amplify-graphql-schema-generator/src/__tests__/pg-string-datasource-adapter.test.ts index 7f107f508d..0ede5e71c6 100644 --- a/packages/amplify-graphql-schema-generator/src/__tests__/pg-string-datasource-adapter.test.ts +++ b/packages/amplify-graphql-schema-generator/src/__tests__/pg-string-datasource-adapter.test.ts @@ -131,6 +131,11 @@ describe('testPostgresStringDataSourceAdapter', () => { expect(adapter.getModels()).toMatchSnapshot(); }); + it('annotates the model field correctly from a schema with a SERIAL field', () => { + const adapter = new PostgresStringDataSourceAdapter(schemas.postgres.serial); + expect(adapter.getModels()).toMatchSnapshot(); + }); + it('sets the correct models from a news schema', () => { const adapter = new PostgresStringDataSourceAdapter(schemas.postgres.news); expect(adapter.getModels()).toMatchSnapshot(); diff --git a/packages/amplify-graphql-schema-generator/src/generate.ts b/packages/amplify-graphql-schema-generator/src/generate.ts index a1b22c804d..224c22215c 100644 --- a/packages/amplify-graphql-schema-generator/src/generate.ts +++ b/packages/amplify-graphql-schema-generator/src/generate.ts @@ -4,11 +4,11 @@ import { DocumentNode } from 'graphql'; import { Schema, Engine } from './schema-representation'; import { generateGraphQLSchema } from './schema-generator'; import { constructRDSGlobalAmplifyInput } from './input'; -import { MySQLStringDataSourceAdapter, PostgresStringDataSourceAdapter } from './datasource-adapter'; +import { MySQLStringDataSourceAdapter, PostgresStringDataSourceAdapter, StringDataSourceAdapter } from './datasource-adapter'; const buildSchemaFromString = (stringSchema: string, engineType: ImportedRDSType): Schema => { - let schema; - let adapter; + let schema: Schema; + let adapter: StringDataSourceAdapter; switch (engineType) { case ImportedRDSType.MYSQL: adapter = new MySQLStringDataSourceAdapter(stringSchema); diff --git a/packages/amplify-graphql-schema-generator/src/ts-schema-generator/helpers.ts b/packages/amplify-graphql-schema-generator/src/ts-schema-generator/helpers.ts index da52f8f897..d72c34296e 100644 --- a/packages/amplify-graphql-schema-generator/src/ts-schema-generator/helpers.ts +++ b/packages/amplify-graphql-schema-generator/src/ts-schema-generator/helpers.ts @@ -1,7 +1,7 @@ import ts from 'typescript'; import { TYPESCRIPT_DATA_SCHEMA_CONSTANTS } from 'graphql-transformer-common'; import { VpcConfig } from '@aws-amplify/graphql-transformer-interfaces'; -import { DBEngineType, Field, FieldType, Model, Schema } from '../schema-representation'; +import { DBEngineType, Field, Model, Schema } from '../schema-representation'; const GQL_TYPESCRIPT_DATA_SCHEMA_TYPE_MAP = { string: 'string', @@ -27,32 +27,45 @@ const GQL_TYPESCRIPT_DATA_SCHEMA_TYPE_MAP = { * @returns Typescript data schema property in TS Node format */ const createProperty = (field: Field): ts.Node => { - const typeExpression = createDataType(field.type); + const typeExpression = createDataType(field); return ts.factory.createPropertyAssignment(ts.factory.createIdentifier(field.name), typeExpression as ts.Expression); }; /** * Creates a typescript data schema type from internal SQL schema representation * Example typescript data schema type output: `a.string().required()` - * @param type SQL IR field type + * @param field SQL IR field * @returns Typescript data schema type in TS Node format */ -const createDataType = (type: FieldType): ts.Node => { - if (type.kind === 'Scalar') { +const createDataType = (field: Field): ts.Node => { + if (isSequenceField(field)) { + const baseTypeExpression = + field.type.kind === 'NonNull' + ? createDataType(new Field(field.name, field.type.type)) + : createDataType(new Field(field.name, field.type)); + + return ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(baseTypeExpression as ts.Expression, TYPESCRIPT_DATA_SCHEMA_CONSTANTS.DEFAULT_METHOD), + undefined, + undefined, + ); + } + + if (field.type.kind === 'Scalar') { return ts.factory.createCallExpression( - ts.factory.createIdentifier(`${TYPESCRIPT_DATA_SCHEMA_CONSTANTS.REFERENCE_A}.${getTypescriptDataSchemaType(type.name)}`), + ts.factory.createIdentifier(`${TYPESCRIPT_DATA_SCHEMA_CONSTANTS.REFERENCE_A}.${getTypescriptDataSchemaType(field.type.name)}`), undefined, undefined, ); } - if (type.kind === 'Enum') { + if (field.type.kind === 'Enum') { return ts.factory.createCallExpression( ts.factory.createIdentifier(`${TYPESCRIPT_DATA_SCHEMA_CONSTANTS.REFERENCE_A}.${TYPESCRIPT_DATA_SCHEMA_CONSTANTS.ENUM_METHOD}`), undefined, [ ts.factory.createArrayLiteralExpression( - type.values.map((value) => ts.factory.createStringLiteral(value)), + field.type.values.map((value) => ts.factory.createStringLiteral(value)), true, ), ], @@ -61,7 +74,7 @@ const createDataType = (type: FieldType): ts.Node => { // We do not import any Database type as 'Custom' type. // In case if there is a custom type in the IR schema, we will import it as string. - if (type.kind === 'Custom') { + if (field.type.kind === 'Custom') { return ts.factory.createCallExpression( ts.factory.createIdentifier(`${TYPESCRIPT_DATA_SCHEMA_CONSTANTS.REFERENCE_A}.${TYPESCRIPT_DATA_SCHEMA_CONSTANTS.STRING_METHOD}`), undefined, @@ -70,14 +83,21 @@ const createDataType = (type: FieldType): ts.Node => { } // List or NonNull - const modifier = type.kind === 'List' ? TYPESCRIPT_DATA_SCHEMA_CONSTANTS.ARRAY_METHOD : TYPESCRIPT_DATA_SCHEMA_CONSTANTS.REQUIRED_METHOD; + const modifier = + field.type.kind === 'List' ? TYPESCRIPT_DATA_SCHEMA_CONSTANTS.ARRAY_METHOD : TYPESCRIPT_DATA_SCHEMA_CONSTANTS.REQUIRED_METHOD; + const unwrappedField = new Field(field.name, field.type.type); return ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression(createDataType(type.type) as ts.Expression, ts.factory.createIdentifier(modifier)), + ts.factory.createPropertyAccessExpression(createDataType(unwrappedField) as ts.Expression, ts.factory.createIdentifier(modifier)), undefined, undefined, ); }; +const isSequenceField = (field: Field): boolean => { + const sequenceRegex = /^nextval\(.+::regclass\)$/; + return field.default?.kind === 'DB_GENERATED' && sequenceRegex.test(field.default.value.toString()); +}; + const getTypescriptDataSchemaType = (type: string): string => { const DEFAULT_DATATYPE = TYPESCRIPT_DATA_SCHEMA_CONSTANTS.STRING_METHOD; const tsDataSchemaType = GQL_TYPESCRIPT_DATA_SCHEMA_TYPE_MAP[type.toLowerCase()]; diff --git a/packages/graphql-transformer-common/API.md b/packages/graphql-transformer-common/API.md index f2b51c5989..417b90a07a 100644 --- a/packages/graphql-transformer-common/API.md +++ b/packages/graphql-transformer-common/API.md @@ -512,6 +512,7 @@ export const TYPESCRIPT_DATA_SCHEMA_CONSTANTS: { IDENTIFIER_METHOD: string; ARRAY_METHOD: string; REQUIRED_METHOD: string; + DEFAULT_METHOD: string; STRING_METHOD: string; ENUM_METHOD: string; REFERENCE_A: string; diff --git a/packages/graphql-transformer-common/src/TypescriptSchemaConstants.ts b/packages/graphql-transformer-common/src/TypescriptSchemaConstants.ts index db23c5dcaf..2848a32299 100644 --- a/packages/graphql-transformer-common/src/TypescriptSchemaConstants.ts +++ b/packages/graphql-transformer-common/src/TypescriptSchemaConstants.ts @@ -7,6 +7,7 @@ export const TYPESCRIPT_DATA_SCHEMA_CONSTANTS = { IDENTIFIER_METHOD: 'identifier', ARRAY_METHOD: 'array', REQUIRED_METHOD: 'required', + DEFAULT_METHOD: 'default', STRING_METHOD: 'string', ENUM_METHOD: 'enum', REFERENCE_A: 'a',