-
Notifications
You must be signed in to change notification settings - Fork 89
feat: generate ts schema from pg serial field #2952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2147702
995b74d
3c8f132
c8f39ed
28a83ed
e02d2d7
102c544
b793733
5e5e3c1
2c89d64
759789e
07e6c3a
a1fa7df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -145,6 +145,19 @@ type Story @refersTo(name: \\"stories\\") @model { | |
| " | ||
| `; | ||
|
|
||
| exports[`generate graphqlSchemaFromSQLSchema creates graphql schema from "postgres" "serial" schema 1`] = ` | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This snapshot is not intentional as we are not intending to support GQL generation here. It is generated automatically to test against anyway from the schemas in |
||
| "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\\" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -249,6 +249,108 @@ describe('Type name conversions', () => { | |
| expect(graphqlSchema).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it.each([ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These cases are not exhaustive over every |
||
| { | ||
| 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'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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()); | ||
|
Comment on lines
+97
to
+98
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Copying this from main conversation comment) Can we find documentation that this is the canonical way to determine whether a field is
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In summary we cannot determine if a column was declared Ultimately We can tell if a column is backed by a sequence since its default will be
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the thorough explanation! |
||
| }; | ||
|
|
||
| const getTypescriptDataSchemaType = (type: string): string => { | ||
| const DEFAULT_DATATYPE = TYPESCRIPT_DATA_SCHEMA_CONSTANTS.STRING_METHOD; | ||
| const tsDataSchemaType = GQL_TYPESCRIPT_DATA_SCHEMA_TYPE_MAP[type.toLowerCase()]; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the desired behavior of this PR.