From cdf0e4e2fc7f91c3fcadbb32d476e3bb47557f24 Mon Sep 17 00:00:00 2001 From: Josh Bowling Date: Sun, 8 Dec 2024 16:18:46 +0900 Subject: [PATCH] Add option to replace Serial with Integer Change-type: minor --- src/generate.ts | 43 ++++++++++++++++++----- test/index.ts | 90 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 11 deletions(-) diff --git a/src/generate.ts b/src/generate.ts index e082b84..f44d5e3 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -17,10 +17,16 @@ type RequiredModelSubset = Pick< 'tables' | 'relationships' | 'synonyms' >; +export type Options = { + convertSerialToInteger?: boolean; +}; + const trimNL = new TemplateTag( replaceResultTransformer(/^[\r\n]*|[\r\n]*$/g, ''), ); +const replaceSerial = (s: string): string => s.replace(/serial$/i, 'Integer'); + const modelNameToCamelCaseName = (s: string): string => s .split(/[ -]/) @@ -34,6 +40,7 @@ const sqlTypeToTypescriptType = ( m: RequiredModelSubset, f: AbstractSqlField, mode: Mode, + opts: Options, ): string => { if (!['ForeignKey', 'ConceptType'].includes(f.dataType) && f.checks) { const inChecks = f.checks.find( @@ -65,7 +72,7 @@ const sqlTypeToTypescriptType = ( // If the referenced field is computed it won't generate a write typing // so we have to in-line it here as we cannot reference it if (referencedField?.computed != null) { - return `Types['${referencedField.dataType}']['${mode}']`; + return `Types['${opts.convertSerialToInteger ? replaceSerial(referencedField.dataType) : referencedField.dataType}']['${mode}']`; } } return referencedFieldType; @@ -75,7 +82,7 @@ const sqlTypeToTypescriptType = ( return `{ __id: ${referencedFieldType} } | [${referencedInterface}]${nullable}`; } default: - return `Types['${f.dataType}']['${mode}']`; + return `Types['${opts.convertSerialToInteger ? replaceSerial(f.dataType) : f.dataType}']['${mode}']`; } }; @@ -84,6 +91,7 @@ const fieldToInterfaceProps = ( m: RequiredModelSubset, f: AbstractSqlField, mode: Mode, + opts: Options, ): string | undefined => { if (mode === 'Write' && f.computed != null) { // Computed terms cannot be written to @@ -94,6 +102,7 @@ const fieldToInterfaceProps = ( m, f, mode, + opts, )}${nullable};`; }; @@ -101,9 +110,10 @@ const fieldsToInterfaceProps = ( m: RequiredModelSubset, fields: AbstractSqlField[], mode: Mode, + opts: Options, ): string[] => fields - .map((f) => fieldToInterfaceProps(f.fieldName, m, f, mode)) + .map((f) => fieldToInterfaceProps(f.fieldName, m, f, mode, opts)) .filter((f) => f != null); const getSynonyms = ( @@ -126,6 +136,7 @@ const recurseRelationships = ( mode: Mode, currentTable: AbstractSqlTable, parentKey: string, + opts: Options, ): string[] => Object.keys(relationships).flatMap((key) => { if (key === '$') { @@ -161,7 +172,13 @@ const recurseRelationships = ( const addDefinition = (propName: string) => { // Only add the relationship if it doesn't directly match the field name to avoid duplicates if (f.fieldName !== propName) { - const propDefiniton = fieldToInterfaceProps(propName, m, f, mode); + const propDefiniton = fieldToInterfaceProps( + propName, + m, + f, + mode, + opts, + ); if (propDefiniton != null) { propDefinitons.push(propDefiniton); } @@ -186,6 +203,7 @@ const recurseRelationships = ( mode, currentTable, `${parentKey}-${key}`, + opts, ); }); @@ -193,6 +211,7 @@ const relationshipsToInterfaceProps = ( m: RequiredModelSubset, table: AbstractSqlTable, mode: Mode, + opts: Options, ): string[] => { const relationships = m.relationships[table.resourceName]; if (relationships == null) { @@ -216,15 +235,20 @@ const relationshipsToInterfaceProps = ( mode, table, key, + opts, ); }); }; -const tableToInterface = (m: RequiredModelSubset, table: AbstractSqlTable) => { +const tableToInterface = ( + m: RequiredModelSubset, + table: AbstractSqlTable, + opts: Options, +) => { const writableFields = table.definition != null ? [] - : fieldsToInterfaceProps(m, table.fields, 'Write'); + : fieldsToInterfaceProps(m, table.fields, 'Write', opts); const writeType = writableFields.length === 0 ? // If there's a table definition then we cannot write anything @@ -236,8 +260,8 @@ const tableToInterface = (m: RequiredModelSubset, table: AbstractSqlTable) => { export interface ${modelNameToCamelCaseName(table.name)} { Read: { ${[ - ...fieldsToInterfaceProps(m, table.fields, 'Read'), - ...relationshipsToInterfaceProps(m, table, 'Read'), + ...fieldsToInterfaceProps(m, table.fields, 'Read', opts), + ...relationshipsToInterfaceProps(m, table, 'Read', opts), ].join('\n\t\t')} }; Write: ${writeType}; @@ -249,6 +273,7 @@ type Mode = 'Read' | 'Write'; export const abstractSqlToTypescriptTypes = ( m: RequiredModelSubset, + opts: Options = {}, ): string => { return trimNL` // These types were generated by @balena/abstract-sql-to-typescript v${version} @@ -258,7 +283,7 @@ import type { Types } from '@balena/abstract-sql-to-typescript'; ${Object.keys(m.tables) .map((tableName) => { const t = m.tables[tableName]; - return tableToInterface(m, t); + return tableToInterface(m, t, opts); }) .join('\n\n')} diff --git a/test/index.ts b/test/index.ts index 6a60066..4b51b38 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,13 +1,14 @@ import type { AbstractSqlModel } from '@balena/abstract-sql-compiler'; import { expect } from 'chai'; import { source } from 'common-tags'; -import { abstractSqlToTypescriptTypes } from '../src/generate'; +import { abstractSqlToTypescriptTypes, type Options } from '../src/generate'; import { version } from '../package.json'; const test = ( msg: string, model: Partial, expectation: string, + opts?: Options, ) => { it(`should generate ${msg}`, () => { // Set defaults for required props @@ -19,7 +20,7 @@ const test = ( lfInfo: { rules: {} }, ...model, }; - const result = abstractSqlToTypescriptTypes(t); + const result = abstractSqlToTypescriptTypes(t, opts); expect(result).to.equal(source` // These types were generated by @balena/abstract-sql-to-typescript v${version} @@ -386,3 +387,88 @@ test( } `, ); + +test( + 'correct types for a test table with convertSerialToInteger=true', + testTable, + source` + export interface Parent { + Read: { + created_at: Types['Date Time']['Read']; + modified_at: Types['Date Time']['Read']; + id: Types['Integer']['Read']; + }; + Write: { + created_at: Types['Date Time']['Write']; + modified_at: Types['Date Time']['Write']; + }; + } + + export interface Other { + Read: { + created_at: Types['Date Time']['Read']; + modified_at: Types['Date Time']['Read']; + id: Types['Big Integer']['Read']; + is_referenced_by__test?: Array; + }; + Write: { + created_at: Types['Date Time']['Write']; + modified_at: Types['Date Time']['Write']; + }; + } + + export interface Test { + Read: { + created_at: Types['Date Time']['Read']; + modified_at: Types['Date Time']['Read']; + id: Types['Integer']['Read']; + a_date: Types['Date']['Read']; + a_file: Types['WebResource']['Read']; + parent: { __id: Parent['Read']['id'] } | [Parent['Read']]; + references__other: { __id: Other['Read']['id'] } | [Other['Read']]; + aliased__tag?: Array; + references__test__has__tag_key?: Array; + references__test_tag?: Array; + test__has__tag_key?: Array; + test_tag?: Array; + }; + Write: { + created_at: Types['Date Time']['Write']; + modified_at: Types['Date Time']['Write']; + a_date: Types['Date']['Write']; + a_file: Types['WebResource']['Write']; + parent: Types['Integer']['Write']; + references__other: Types['Big Integer']['Write']; + }; + } + + export interface TestTag { + Read: { + created_at: Types['Date Time']['Read']; + modified_at: Types['Date Time']['Read']; + test: { __id: Test['Read']['id'] } | [Test['Read']]; + tag_key: Types['Short Text']['Read']; + id: Types['Integer']['Read']; + }; + Write: { + created_at: Types['Date Time']['Write']; + modified_at: Types['Date Time']['Write']; + test: Types['Integer']['Write']; + tag_key: Types['Short Text']['Write']; + id: Types['Integer']['Write']; + }; + } + + export default interface $Model { + parent: Parent; + other: Other; + test: Test; + test__has__tag_key: TestTag; + // Synonyms + test_tag: TestTag; + } + `, + { + convertSerialToInteger: true, + }, +);