@balena/abstract-sql-to-typescript
Advanced tools
Comparing version 2.3.0-build-foreign-key-ref-interface-7fa840da8577407e4b90e2c0b06df917ba6f83ec-1 to 3.0.0-build-3-x-cdcb52735998e2ca1b783fcb8d10c8e474c0b72f-1
@@ -7,4 +7,6 @@ # Change Log | ||
## 2.3.0 - 2024-04-17 | ||
## 3.0.0 - 2024-04-17 | ||
* Use types directly from sbvr-types [Pagan Gazzard] | ||
* Expose read vs write selection in generated types [Pagan Gazzard] | ||
* Improve foreign key typings by referencing the appropriate interface [Pagan Gazzard] | ||
@@ -11,0 +13,0 @@ |
@@ -0,7 +1,12 @@ | ||
export type { Types } from '@balena/sbvr-types'; | ||
export type Expanded<T> = Extract<T, any[]>; | ||
export type PickExpanded<T, K extends keyof T> = { | ||
[P in K]-?: Expanded<T[P]>; | ||
}; | ||
export type Deferred<T> = Exclude<T, any[]>; | ||
export type PickDeferred<T, K extends keyof T> = { | ||
[P in K]: Deferred<T[P]>; | ||
}; | ||
import type { AbstractSqlModel } from '@balena/abstract-sql-compiler'; | ||
type RequiredModelSubset = Pick<AbstractSqlModel, 'tables' | 'relationships' | 'synonyms'>; | ||
export interface Options { | ||
mode?: 'read' | 'write'; | ||
} | ||
export declare const abstractSqlToTypescriptTypes: (m: RequiredModelSubset, opts?: Options) => string; | ||
export {}; | ||
export declare const abstractSqlToTypescriptTypes: (m: RequiredModelSubset) => string; |
@@ -6,23 +6,3 @@ "use strict"; | ||
const common_tags_1 = require("common-tags"); | ||
const typeHelpers = { | ||
read: ` | ||
export type DateString = string; | ||
export type Expanded<T> = Extract<T, any[]>; | ||
export type PickExpanded<T, K extends keyof T> = { | ||
[P in K]-?: Expanded<T[P]>; | ||
}; | ||
export type Deferred<T> = Exclude<T, any[]>; | ||
export type PickDeferred<T, K extends keyof T> = { | ||
[P in K]: Deferred<T[P]>; | ||
}; | ||
export interface WebResource { | ||
filename: string; | ||
href: string; | ||
content_type?: string; | ||
content_disposition?: string; | ||
size?: number; | ||
}; | ||
`, | ||
write: '', | ||
}; | ||
const typeHelpers = `import type { Types } from '@balena/abstract-sql-to-typescript';\n`; | ||
const trimNL = new common_tags_1.TemplateTag((0, common_tags_1.replaceResultTransformer)(/^[\r\n]*|[\r\n]*$/g, '')); | ||
@@ -33,3 +13,4 @@ const modelNameToCamelCaseName = (s) => s | ||
.join(''); | ||
const sqlTypeToTypescriptType = (m, f, opts) => { | ||
const getReferencedInterface = (modelName, mode) => `${modelNameToCamelCaseName(modelName)}['${mode}']`; | ||
const sqlTypeToTypescriptType = (m, f, mode) => { | ||
if (!['ForeignKey', 'ConceptType'].includes(f.dataType) && f.checks) { | ||
@@ -45,22 +26,7 @@ const inChecks = f.checks.find((checkTuple) => checkTuple[0] === 'In'); | ||
switch (f.dataType) { | ||
case 'Boolean': | ||
return 'boolean'; | ||
case 'Short Text': | ||
case 'Text': | ||
case 'Hashed': | ||
return 'string'; | ||
case 'Date': | ||
case 'Date Time': | ||
return opts.mode === 'read' ? 'DateString' : 'Date'; | ||
case 'Serial': | ||
case 'Big Serial': | ||
case 'Integer': | ||
case 'Big Integer': | ||
case 'Real': | ||
return 'number'; | ||
case 'ConceptType': | ||
case 'ForeignKey': { | ||
const referencedInterface = modelNameToCamelCaseName(m.tables[f.references.resourceName].name); | ||
const referencedInterface = getReferencedInterface(m.tables[f.references.resourceName].name, mode); | ||
const referencedFieldType = `${referencedInterface}['${f.references.fieldName}']`; | ||
if (opts.mode === 'write') { | ||
if (mode === 'Write') { | ||
return referencedFieldType; | ||
@@ -71,17 +37,11 @@ } | ||
} | ||
case 'File': | ||
return 'Buffer'; | ||
case 'JSON': | ||
return 'object'; | ||
case 'WebResource': | ||
return 'WebResource'; | ||
default: | ||
throw new Error(`Unknown data type: '${f.dataType}'`); | ||
return `Types['${f.dataType}']['${mode}']`; | ||
} | ||
}; | ||
const fieldsToInterfaceProps = (m, fields, opts) => fields.map((f) => { | ||
const fieldsToInterfaceProps = (m, fields, mode) => fields.map((f) => { | ||
const nullable = f.required ? '' : ' | null'; | ||
return `${(0, odata_to_abstract_sql_1.sqlNameToODataName)(f.fieldName)}: ${sqlTypeToTypescriptType(m, f, opts)}${nullable};`; | ||
return `${(0, odata_to_abstract_sql_1.sqlNameToODataName)(f.fieldName)}: ${sqlTypeToTypescriptType(m, f, mode)}${nullable};`; | ||
}); | ||
const recurseRelationships = (m, relationships, inverseSynonyms, opts, currentTable, parentKey) => Object.keys(relationships).flatMap((key) => { | ||
const recurseRelationships = (m, relationships, inverseSynonyms, mode, currentTable, parentKey) => Object.keys(relationships).flatMap((key) => { | ||
if (key === '$') { | ||
@@ -92,3 +52,3 @@ const [localField, referencedField] = relationships.$; | ||
if (referencedTable != null) { | ||
const referencedInterface = modelNameToCamelCaseName(referencedTable.name); | ||
const referencedInterface = getReferencedInterface(referencedTable.name, mode); | ||
const propDefinitons = [`${parentKey}?: ${referencedInterface}[];`]; | ||
@@ -104,5 +64,5 @@ const synonym = inverseSynonyms[(0, odata_to_abstract_sql_1.odataNameToSqlName)(parentKey)]; | ||
} | ||
return recurseRelationships(m, relationships[key], inverseSynonyms, opts, currentTable, `${parentKey}__${key.replace(/ /g, '_')}`); | ||
return recurseRelationships(m, relationships[key], inverseSynonyms, mode, currentTable, `${parentKey}__${key.replace(/ /g, '_')}`); | ||
}); | ||
const relationshipsToInterfaceProps = (m, table, opts) => { | ||
const relationshipsToInterfaceProps = (m, table, mode) => { | ||
const relationships = m.relationships[table.resourceName]; | ||
@@ -120,25 +80,27 @@ if (relationships == null) { | ||
])); | ||
return recurseRelationships(m, relationships[key], inverseSynonyms, opts, table, key.replace(/ /g, '_')); | ||
return recurseRelationships(m, relationships[key], inverseSynonyms, mode, table, key.replace(/ /g, '_')); | ||
}); | ||
}; | ||
const tableToInterface = (m, table, opts) => { | ||
const relationshipProps = opts.mode === 'read' ? relationshipsToInterfaceProps(m, table, opts) : []; | ||
const tableToInterface = (m, table) => { | ||
return trimNL ` | ||
export interface ${modelNameToCamelCaseName(table.name)} { | ||
${[...fieldsToInterfaceProps(m, table.fields, opts), ...relationshipProps].join('\n\t')} | ||
Read: { | ||
${[ | ||
...fieldsToInterfaceProps(m, table.fields, 'Read'), | ||
...relationshipsToInterfaceProps(m, table, 'Read'), | ||
].join('\n\t\t')} | ||
} | ||
Write: { | ||
${[...fieldsToInterfaceProps(m, table.fields, 'Write')].join('\n\t\t')} | ||
} | ||
} | ||
`; | ||
}; | ||
const abstractSqlToTypescriptTypes = (m, opts = {}) => { | ||
const mode = opts.mode ?? 'read'; | ||
const requiredOptions = { | ||
...opts, | ||
mode, | ||
}; | ||
const abstractSqlToTypescriptTypes = (m) => { | ||
return trimNL ` | ||
${typeHelpers[mode]} | ||
${typeHelpers} | ||
${Object.keys(m.tables) | ||
.map((tableName) => { | ||
const t = m.tables[tableName]; | ||
return tableToInterface(m, t, requiredOptions); | ||
return tableToInterface(m, t); | ||
}) | ||
@@ -145,0 +107,0 @@ .join('\n\n')} |
{ | ||
"name": "@balena/abstract-sql-to-typescript", | ||
"version": "2.3.0-build-foreign-key-ref-interface-7fa840da8577407e4b90e2c0b06df917ba6f83ec-1", | ||
"version": "3.0.0-build-3-x-cdcb52735998e2ca1b783fcb8d10c8e474c0b72f-1", | ||
"description": "A translator for abstract sql into typescript types.", | ||
@@ -19,2 +19,3 @@ "main": "out/index.js", | ||
"@balena/odata-to-abstract-sql": "^6.2.3", | ||
"@balena/sbvr-types": "^7.1.0-build-read-write-types-66b9a012e242533372ce34a73e31f6e3aac93d91-1", | ||
"@types/node": "^20.11.24", | ||
@@ -49,4 +50,4 @@ "common-tags": "^1.8.2" | ||
"versionist": { | ||
"publishedAt": "2024-04-17T16:43:14.068Z" | ||
"publishedAt": "2024-04-17T17:23:09.089Z" | ||
} | ||
} |
117
src/index.ts
@@ -0,1 +1,12 @@ | ||
export type { Types } from '@balena/sbvr-types'; | ||
export type Expanded<T> = Extract<T, any[]>; | ||
export type PickExpanded<T, K extends keyof T> = { | ||
[P in K]-?: Expanded<T[P]>; | ||
}; | ||
export type Deferred<T> = Exclude<T, any[]>; | ||
export type PickDeferred<T, K extends keyof T> = { | ||
[P in K]: Deferred<T[P]>; | ||
}; | ||
import type { | ||
@@ -21,23 +32,3 @@ AbstractSqlField, | ||
const typeHelpers = { | ||
read: ` | ||
export type DateString = string; | ||
export type Expanded<T> = Extract<T, any[]>; | ||
export type PickExpanded<T, K extends keyof T> = { | ||
[P in K]-?: Expanded<T[P]>; | ||
}; | ||
export type Deferred<T> = Exclude<T, any[]>; | ||
export type PickDeferred<T, K extends keyof T> = { | ||
[P in K]: Deferred<T[P]>; | ||
}; | ||
export interface WebResource { | ||
filename: string; | ||
href: string; | ||
content_type?: string; | ||
content_disposition?: string; | ||
size?: number; | ||
}; | ||
`, | ||
write: '', | ||
}; | ||
const typeHelpers = `import type { Types } from '@balena/abstract-sql-to-typescript';\n`; | ||
@@ -54,6 +45,9 @@ const trimNL = new TemplateTag( | ||
const getReferencedInterface = (modelName: string, mode: Mode) => | ||
`${modelNameToCamelCaseName(modelName)}['${mode}']`; | ||
const sqlTypeToTypescriptType = ( | ||
m: RequiredModelSubset, | ||
f: AbstractSqlField, | ||
opts: RequiredOptions, | ||
mode: Mode, | ||
): string => { | ||
@@ -73,24 +67,10 @@ if (!['ForeignKey', 'ConceptType'].includes(f.dataType) && f.checks) { | ||
switch (f.dataType) { | ||
case 'Boolean': | ||
return 'boolean'; | ||
case 'Short Text': | ||
case 'Text': | ||
case 'Hashed': | ||
return 'string'; | ||
case 'Date': | ||
case 'Date Time': | ||
return opts.mode === 'read' ? 'DateString' : 'Date'; | ||
case 'Serial': | ||
case 'Big Serial': | ||
case 'Integer': | ||
case 'Big Integer': | ||
case 'Real': | ||
return 'number'; | ||
case 'ConceptType': | ||
case 'ForeignKey': { | ||
const referencedInterface = modelNameToCamelCaseName( | ||
const referencedInterface = getReferencedInterface( | ||
m.tables[f.references!.resourceName].name, | ||
mode, | ||
); | ||
const referencedFieldType = `${referencedInterface}['${f.references!.fieldName}']`; | ||
if (opts.mode === 'write') { | ||
if (mode === 'Write') { | ||
return referencedFieldType; | ||
@@ -102,10 +82,4 @@ } | ||
} | ||
case 'File': | ||
return 'Buffer'; | ||
case 'JSON': | ||
return 'object'; | ||
case 'WebResource': | ||
return 'WebResource'; | ||
default: | ||
throw new Error(`Unknown data type: '${f.dataType}'`); | ||
return `Types['${f.dataType}']['${mode}']`; | ||
} | ||
@@ -117,3 +91,3 @@ }; | ||
fields: AbstractSqlField[], | ||
opts: RequiredOptions, | ||
mode: Mode, | ||
): string[] => | ||
@@ -125,3 +99,3 @@ fields.map((f) => { | ||
f, | ||
opts, | ||
mode, | ||
)}${nullable};`; | ||
@@ -134,3 +108,3 @@ }); | ||
inverseSynonyms: Record<string, string>, | ||
opts: RequiredOptions, | ||
mode: Mode, | ||
currentTable: AbstractSqlTable, | ||
@@ -147,4 +121,5 @@ parentKey: string, | ||
if (referencedTable != null) { | ||
const referencedInterface = modelNameToCamelCaseName( | ||
const referencedInterface = getReferencedInterface( | ||
referencedTable.name, | ||
mode, | ||
); | ||
@@ -167,3 +142,3 @@ const propDefinitons = [`${parentKey}?: ${referencedInterface}[];`]; | ||
inverseSynonyms, | ||
opts, | ||
mode, | ||
currentTable, | ||
@@ -177,3 +152,3 @@ `${parentKey}__${key.replace(/ /g, '_')}`, | ||
table: AbstractSqlTable, | ||
opts: RequiredOptions, | ||
mode: Mode, | ||
): string[] => { | ||
@@ -199,3 +174,3 @@ const relationships = m.relationships[table.resourceName]; | ||
inverseSynonyms, | ||
opts, | ||
mode, | ||
table, | ||
@@ -207,15 +182,14 @@ key.replace(/ /g, '_'), | ||
const tableToInterface = ( | ||
m: RequiredModelSubset, | ||
table: AbstractSqlTable, | ||
opts: RequiredOptions, | ||
) => { | ||
const relationshipProps = | ||
opts.mode === 'read' ? relationshipsToInterfaceProps(m, table, opts) : []; | ||
const tableToInterface = (m: RequiredModelSubset, table: AbstractSqlTable) => { | ||
return trimNL` | ||
export interface ${modelNameToCamelCaseName(table.name)} { | ||
${[...fieldsToInterfaceProps(m, table.fields, opts), ...relationshipProps].join( | ||
'\n\t', | ||
)} | ||
Read: { | ||
${[ | ||
...fieldsToInterfaceProps(m, table.fields, 'Read'), | ||
...relationshipsToInterfaceProps(m, table, 'Read'), | ||
].join('\n\t\t')} | ||
} | ||
Write: { | ||
${[...fieldsToInterfaceProps(m, table.fields, 'Write')].join('\n\t\t')} | ||
} | ||
} | ||
@@ -225,22 +199,13 @@ `; | ||
export interface Options { | ||
mode?: 'read' | 'write'; | ||
} | ||
type RequiredOptions = Required<Options>; | ||
type Mode = 'Read' | 'Write'; | ||
export const abstractSqlToTypescriptTypes = ( | ||
m: RequiredModelSubset, | ||
opts: Options = {}, | ||
): string => { | ||
const mode = opts.mode ?? 'read'; | ||
const requiredOptions: RequiredOptions = { | ||
...opts, | ||
mode, | ||
}; | ||
return trimNL` | ||
${typeHelpers[mode]} | ||
${typeHelpers} | ||
${Object.keys(m.tables) | ||
.map((tableName) => { | ||
const t = m.tables[tableName]; | ||
return tableToInterface(m, t, requiredOptions); | ||
return tableToInterface(m, t); | ||
}) | ||
@@ -247,0 +212,0 @@ .join('\n\n')} |
import type { AbstractSqlModel } from '@balena/abstract-sql-compiler'; | ||
import { expect } from 'chai'; | ||
import { source } from 'common-tags'; | ||
import type { Options } from '../src'; | ||
import { abstractSqlToTypescriptTypes } from '../src'; | ||
@@ -11,3 +10,2 @@ | ||
expectation: string, | ||
mode?: Options['mode'], | ||
) => { | ||
@@ -24,28 +22,9 @@ it(`should generate ${msg}`, () => { | ||
}; | ||
const result = abstractSqlToTypescriptTypes(t, { mode }); | ||
const result = abstractSqlToTypescriptTypes(t); | ||
if (mode == null || mode === 'read') { | ||
expect(result).to.equal(source` | ||
export type DateString = string; | ||
export type Expanded<T> = Extract<T, any[]>; | ||
export type PickExpanded<T, K extends keyof T> = { | ||
[P in K]-?: Expanded<T[P]>; | ||
}; | ||
export type Deferred<T> = Exclude<T, any[]>; | ||
export type PickDeferred<T, K extends keyof T> = { | ||
[P in K]: Deferred<T[P]>; | ||
}; | ||
export interface WebResource { | ||
filename: string; | ||
href: string; | ||
content_type?: string; | ||
content_disposition?: string; | ||
size?: number; | ||
}; | ||
expect(result).to.equal(source` | ||
import type { Types } from '@balena/abstract-sql-to-typescript'; | ||
${expectation} | ||
`); | ||
} else { | ||
expect(result).to.equal(expectation); | ||
} | ||
}); | ||
@@ -304,75 +283,72 @@ }; | ||
test( | ||
'correct read types for a test table', | ||
'correct types for a test table', | ||
testTable, | ||
source` | ||
export interface Parent { | ||
created_at: DateString; | ||
modified_at: DateString; | ||
id: number; | ||
Read: { | ||
created_at: Types['Date Time']['Read']; | ||
modified_at: Types['Date Time']['Read']; | ||
id: Types['Serial']['Read']; | ||
} | ||
Write: { | ||
created_at: Types['Date Time']['Write']; | ||
modified_at: Types['Date Time']['Write']; | ||
id: Types['Serial']['Write']; | ||
} | ||
} | ||
export interface Other { | ||
created_at: DateString; | ||
modified_at: DateString; | ||
id: number; | ||
is_referenced_by__test?: Test[]; | ||
Read: { | ||
created_at: Types['Date Time']['Read']; | ||
modified_at: Types['Date Time']['Read']; | ||
id: Types['Serial']['Read']; | ||
is_referenced_by__test?: Test['Read'][]; | ||
} | ||
Write: { | ||
created_at: Types['Date Time']['Write']; | ||
modified_at: Types['Date Time']['Write']; | ||
id: Types['Serial']['Write']; | ||
} | ||
} | ||
export interface Test { | ||
created_at: DateString; | ||
modified_at: DateString; | ||
id: number; | ||
a_date: DateString; | ||
a_file: WebResource; | ||
parent: { __id: Parent['id'] } | [Parent]; | ||
references__other: { __id: Other['id'] } | [Other]; | ||
test__has__tag_key?: TestTag[]; | ||
test_tag?: TestTag[]; | ||
Read: { | ||
created_at: Types['Date Time']['Read']; | ||
modified_at: Types['Date Time']['Read']; | ||
id: Types['Serial']['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']]; | ||
test__has__tag_key?: TestTag['Read'][]; | ||
test_tag?: TestTag['Read'][]; | ||
} | ||
Write: { | ||
created_at: Types['Date Time']['Write']; | ||
modified_at: Types['Date Time']['Write']; | ||
id: Types['Serial']['Write']; | ||
a_date: Types['Date']['Write']; | ||
a_file: Types['WebResource']['Write']; | ||
parent: Parent['Write']['id']; | ||
references__other: Other['Write']['id']; | ||
} | ||
} | ||
export interface TestTag { | ||
created_at: DateString; | ||
modified_at: DateString; | ||
test: { __id: Test['id'] } | [Test]; | ||
tag_key: string; | ||
id: number; | ||
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['Serial']['Read']; | ||
} | ||
Write: { | ||
created_at: Types['Date Time']['Write']; | ||
modified_at: Types['Date Time']['Write']; | ||
test: Test['Write']['id']; | ||
tag_key: Types['Short Text']['Write']; | ||
id: Types['Serial']['Write']; | ||
} | ||
} | ||
`, | ||
); | ||
test( | ||
'correct write types for a test table', | ||
testTable, | ||
source` | ||
export interface Parent { | ||
created_at: Date; | ||
modified_at: Date; | ||
id: number; | ||
} | ||
export interface Other { | ||
created_at: Date; | ||
modified_at: Date; | ||
id: number; | ||
} | ||
export interface Test { | ||
created_at: Date; | ||
modified_at: Date; | ||
id: number; | ||
a_date: Date; | ||
a_file: WebResource; | ||
parent: Parent['id']; | ||
references__other: Other['id']; | ||
} | ||
export interface TestTag { | ||
created_at: Date; | ||
modified_at: Date; | ||
test: Test['id']; | ||
tag_key: string; | ||
id: number; | ||
} | ||
`, | ||
'write', | ||
); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
197112
5
669