@balena/abstract-sql-to-typescript
Advanced tools
Comparing version 3.0.0-build-3-x-2cef8ca234651a0e6b9281c91357d8002d37b594-1 to 3.0.0-build-3-x-42deabe6053cbc403879216aa353bc5f06c8414c-1
@@ -7,4 +7,6 @@ # Change Log | ||
## 3.0.0 - 2024-04-22 | ||
## 3.0.0 - 2024-04-29 | ||
* Export a `Resource` type which all resources should conform to [Pagan Gazzard] | ||
* Separate the generation code from the exported type helpers [Pagan Gazzard] | ||
* Use types directly from sbvr-types [Pagan Gazzard] | ||
@@ -11,0 +13,0 @@ * Expose read vs write selection in generated types [Pagan Gazzard] |
@@ -0,1 +1,2 @@ | ||
import type { Types } from '@balena/sbvr-types'; | ||
export type { Types } from '@balena/sbvr-types'; | ||
@@ -10,4 +11,13 @@ export type Expanded<T> = Extract<T, any[]>; | ||
}; | ||
import type { AbstractSqlModel } from '@balena/abstract-sql-compiler'; | ||
type RequiredModelSubset = Pick<AbstractSqlModel, 'tables' | 'relationships' | 'synonyms'>; | ||
export declare const abstractSqlToTypescriptTypes: (m: RequiredModelSubset) => string; | ||
type ReadTypes = Types[keyof Types]['Read']; | ||
type WriteTypes = Types[keyof Types]['Write']; | ||
export type Resource<T extends object = object> = { | ||
Read: { | ||
[key in keyof T]: ReadTypes | { | ||
__id: ReadTypes; | ||
} | Resource[]; | ||
}; | ||
Write: { | ||
[key in keyof T]: WriteTypes; | ||
}; | ||
}; |
101
out/index.js
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.abstractSqlToTypescriptTypes = void 0; | ||
const odata_to_abstract_sql_1 = require("@balena/odata-to-abstract-sql"); | ||
const common_tags_1 = require("common-tags"); | ||
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, '')); | ||
const modelNameToCamelCaseName = (s) => s | ||
.split(/[ -]/) | ||
.map((p) => p[0].toLocaleUpperCase() + p.slice(1)) | ||
.join(''); | ||
const getReferencedInterface = (modelName, mode) => `${modelNameToCamelCaseName(modelName)}['${mode}']`; | ||
const sqlTypeToTypescriptType = (m, f, mode) => { | ||
if (!['ForeignKey', 'ConceptType'].includes(f.dataType) && f.checks) { | ||
const inChecks = f.checks.find((checkTuple) => checkTuple[0] === 'In'); | ||
if (inChecks) { | ||
const [, , ...allowedValues] = inChecks; | ||
return allowedValues | ||
.map(([type, value]) => (type === 'Text' ? `'${value}'` : value)) | ||
.join(' | '); | ||
} | ||
} | ||
switch (f.dataType) { | ||
case 'ConceptType': | ||
case 'ForeignKey': { | ||
const referencedInterface = getReferencedInterface(m.tables[f.references.resourceName].name, mode); | ||
const referencedFieldType = `${referencedInterface}['${f.references.fieldName}']`; | ||
if (mode === 'Write') { | ||
return referencedFieldType; | ||
} | ||
const nullable = f.required ? '' : '?'; | ||
return `{ __id: ${referencedFieldType} } | [${referencedInterface}${nullable}]`; | ||
} | ||
default: | ||
return `Types['${f.dataType}']['${mode}']`; | ||
} | ||
}; | ||
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, mode)}${nullable};`; | ||
}); | ||
const recurseRelationships = (m, relationships, inverseSynonyms, mode, currentTable, parentKey) => Object.keys(relationships).flatMap((key) => { | ||
if (key === '$') { | ||
const [localField, referencedField] = relationships.$; | ||
if (currentTable.idField === localField && referencedField != null) { | ||
const referencedTable = m.tables[referencedField[0]]; | ||
if (referencedTable != null) { | ||
const referencedInterface = getReferencedInterface(referencedTable.name, mode); | ||
const propDefinitons = [`${parentKey}?: ${referencedInterface}[];`]; | ||
const synonym = inverseSynonyms[(0, odata_to_abstract_sql_1.odataNameToSqlName)(parentKey)]; | ||
if (synonym != null) { | ||
propDefinitons.push(`${(0, odata_to_abstract_sql_1.sqlNameToODataName)(synonym)}?: ${referencedInterface}[];`); | ||
} | ||
return propDefinitons; | ||
} | ||
} | ||
return []; | ||
} | ||
return recurseRelationships(m, relationships[key], inverseSynonyms, mode, currentTable, `${parentKey}__${key.replace(/ /g, '_')}`); | ||
}); | ||
const relationshipsToInterfaceProps = (m, table, mode) => { | ||
const relationships = m.relationships[table.resourceName]; | ||
if (relationships == null) { | ||
return []; | ||
} | ||
return Object.keys(relationships).flatMap((key) => { | ||
if (key === 'has') { | ||
return []; | ||
} | ||
const inverseSynonyms = Object.fromEntries(Object.entries(m.synonyms).map(([termForm, factType]) => [ | ||
factType, | ||
termForm, | ||
])); | ||
return recurseRelationships(m, relationships[key], inverseSynonyms, mode, table, key.replace(/ /g, '_')); | ||
}); | ||
}; | ||
const tableToInterface = (m, table) => { | ||
return trimNL ` | ||
export interface ${modelNameToCamelCaseName(table.name)} { | ||
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) => { | ||
return trimNL ` | ||
${typeHelpers} | ||
${Object.keys(m.tables) | ||
.map((tableName) => { | ||
const t = m.tables[tableName]; | ||
return tableToInterface(m, t); | ||
}) | ||
.join('\n\n')} | ||
`; | ||
}; | ||
exports.abstractSqlToTypescriptTypes = abstractSqlToTypescriptTypes; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@balena/abstract-sql-to-typescript", | ||
"version": "3.0.0-build-3-x-2cef8ca234651a0e6b9281c91357d8002d37b594-1", | ||
"version": "3.0.0-build-3-x-42deabe6053cbc403879216aa353bc5f06c8414c-1", | ||
"description": "A translator for abstract sql into typescript types.", | ||
"main": "out/index.js", | ||
"types": "out/index.d.ts", | ||
"exports": { | ||
".": "./dist/index.js", | ||
"./generate": "./dist/generate.js" | ||
}, | ||
"scripts": { | ||
@@ -19,3 +23,3 @@ "pretest": "npm run lint && npm run prepare", | ||
"@balena/odata-to-abstract-sql": "^6.2.3", | ||
"@balena/sbvr-types": "^7.1.0-build-read-write-types-66b9a012e242533372ce34a73e31f6e3aac93d91-1", | ||
"@balena/sbvr-types": "^7.1.0", | ||
"@types/node": "^20.11.24", | ||
@@ -50,4 +54,4 @@ "common-tags": "^1.8.2" | ||
"versionist": { | ||
"publishedAt": "2024-04-22T16:41:54.807Z" | ||
"publishedAt": "2024-04-29T17:01:09.598Z" | ||
} | ||
} |
194
src/index.ts
@@ -0,1 +1,2 @@ | ||
import type { Types } from '@balena/sbvr-types'; | ||
export type { Types } from '@balena/sbvr-types'; | ||
@@ -12,191 +13,12 @@ | ||
import type { | ||
AbstractSqlField, | ||
AbstractSqlModel, | ||
AbstractSqlTable, | ||
InNode, | ||
Relationship, | ||
RelationshipInternalNode, | ||
RelationshipLeafNode, | ||
} from '@balena/abstract-sql-compiler'; | ||
import { | ||
odataNameToSqlName, | ||
sqlNameToODataName, | ||
} from '@balena/odata-to-abstract-sql'; | ||
import { replaceResultTransformer, TemplateTag } from 'common-tags'; | ||
type ReadTypes = Types[keyof Types]['Read']; | ||
type WriteTypes = Types[keyof Types]['Write']; | ||
type RequiredModelSubset = Pick< | ||
AbstractSqlModel, | ||
'tables' | 'relationships' | 'synonyms' | ||
>; | ||
const typeHelpers = `import type { Types } from '@balena/abstract-sql-to-typescript';\n`; | ||
const trimNL = new TemplateTag( | ||
replaceResultTransformer(/^[\r\n]*|[\r\n]*$/g, ''), | ||
); | ||
const modelNameToCamelCaseName = (s: string): string => | ||
s | ||
.split(/[ -]/) | ||
.map((p) => p[0].toLocaleUpperCase() + p.slice(1)) | ||
.join(''); | ||
const getReferencedInterface = (modelName: string, mode: Mode) => | ||
`${modelNameToCamelCaseName(modelName)}['${mode}']`; | ||
const sqlTypeToTypescriptType = ( | ||
m: RequiredModelSubset, | ||
f: AbstractSqlField, | ||
mode: Mode, | ||
): string => { | ||
if (!['ForeignKey', 'ConceptType'].includes(f.dataType) && f.checks) { | ||
const inChecks = f.checks.find( | ||
(checkTuple): checkTuple is InNode => checkTuple[0] === 'In', | ||
); | ||
if (inChecks) { | ||
const [, , ...allowedValues] = inChecks; | ||
return allowedValues | ||
.map(([type, value]) => (type === 'Text' ? `'${value}'` : value)) | ||
.join(' | '); | ||
} | ||
} | ||
switch (f.dataType) { | ||
case 'ConceptType': | ||
case 'ForeignKey': { | ||
const referencedInterface = getReferencedInterface( | ||
m.tables[f.references!.resourceName].name, | ||
mode, | ||
); | ||
const referencedFieldType = `${referencedInterface}['${f.references!.fieldName}']`; | ||
if (mode === 'Write') { | ||
return referencedFieldType; | ||
} | ||
const nullable = f.required ? '' : '?'; | ||
return `{ __id: ${referencedFieldType} } | [${referencedInterface}${nullable}]`; | ||
} | ||
default: | ||
return `Types['${f.dataType}']['${mode}']`; | ||
} | ||
}; | ||
const fieldsToInterfaceProps = ( | ||
m: RequiredModelSubset, | ||
fields: AbstractSqlField[], | ||
mode: Mode, | ||
): string[] => | ||
fields.map((f) => { | ||
const nullable = f.required ? '' : ' | null'; | ||
return `${sqlNameToODataName(f.fieldName)}: ${sqlTypeToTypescriptType( | ||
m, | ||
f, | ||
mode, | ||
)}${nullable};`; | ||
}); | ||
const recurseRelationships = ( | ||
m: RequiredModelSubset, | ||
relationships: Relationship, | ||
inverseSynonyms: Record<string, string>, | ||
mode: Mode, | ||
currentTable: AbstractSqlTable, | ||
parentKey: string, | ||
): string[] => | ||
Object.keys(relationships).flatMap((key) => { | ||
if (key === '$') { | ||
const [localField, referencedField] = ( | ||
relationships as RelationshipLeafNode | ||
).$; | ||
if (currentTable.idField === localField && referencedField != null) { | ||
const referencedTable = m.tables[referencedField[0]]; | ||
if (referencedTable != null) { | ||
const referencedInterface = getReferencedInterface( | ||
referencedTable.name, | ||
mode, | ||
); | ||
const propDefinitons = [`${parentKey}?: ${referencedInterface}[];`]; | ||
const synonym = inverseSynonyms[odataNameToSqlName(parentKey)]; | ||
if (synonym != null) { | ||
propDefinitons.push( | ||
`${sqlNameToODataName(synonym)}?: ${referencedInterface}[];`, | ||
); | ||
} | ||
return propDefinitons; | ||
} | ||
} | ||
return []; | ||
} | ||
return recurseRelationships( | ||
m, | ||
(relationships as RelationshipInternalNode)[key], | ||
inverseSynonyms, | ||
mode, | ||
currentTable, | ||
`${parentKey}__${key.replace(/ /g, '_')}`, | ||
); | ||
}); | ||
const relationshipsToInterfaceProps = ( | ||
m: RequiredModelSubset, | ||
table: AbstractSqlTable, | ||
mode: Mode, | ||
): string[] => { | ||
const relationships = m.relationships[table.resourceName]; | ||
if (relationships == null) { | ||
return []; | ||
} | ||
return Object.keys(relationships).flatMap((key) => { | ||
// We skip `has` a the top level as we omit it by convention | ||
if (key === 'has') { | ||
return []; | ||
} | ||
const inverseSynonyms = Object.fromEntries( | ||
Object.entries(m.synonyms).map(([termForm, factType]) => [ | ||
factType, | ||
termForm, | ||
]), | ||
); | ||
return recurseRelationships( | ||
m, | ||
relationships[key], | ||
inverseSynonyms, | ||
mode, | ||
table, | ||
key.replace(/ /g, '_'), | ||
); | ||
}); | ||
}; | ||
const tableToInterface = (m: RequiredModelSubset, table: AbstractSqlTable) => { | ||
return trimNL` | ||
export interface ${modelNameToCamelCaseName(table.name)} { | ||
export type Resource<T extends object = object> = { | ||
Read: { | ||
${[ | ||
...fieldsToInterfaceProps(m, table.fields, 'Read'), | ||
...relationshipsToInterfaceProps(m, table, 'Read'), | ||
].join('\n\t\t')} | ||
} | ||
[key in keyof T]: ReadTypes | { __id: ReadTypes } | Resource[]; | ||
}; | ||
Write: { | ||
${[...fieldsToInterfaceProps(m, table.fields, 'Write')].join('\n\t\t')} | ||
} | ||
} | ||
`; | ||
[key in keyof T]: WriteTypes; | ||
}; | ||
}; | ||
type Mode = 'Read' | 'Write'; | ||
export const abstractSqlToTypescriptTypes = ( | ||
m: RequiredModelSubset, | ||
): string => { | ||
return trimNL` | ||
${typeHelpers} | ||
${Object.keys(m.tables) | ||
.map((tableName) => { | ||
const t = m.tables[tableName]; | ||
return tableToInterface(m, t); | ||
}) | ||
.join('\n\n')} | ||
`; | ||
}; |
import type { AbstractSqlModel } from '@balena/abstract-sql-compiler'; | ||
import { expect } from 'chai'; | ||
import { source } from 'common-tags'; | ||
import { abstractSqlToTypescriptTypes } from '../src'; | ||
import { abstractSqlToTypescriptTypes } from '../src/generate'; | ||
@@ -302,3 +302,3 @@ const test = ( | ||
id: Types['Serial']['Read']; | ||
is_referenced_by__test?: Test['Read'][]; | ||
is_referenced_by__test?: Array<Test['Read']>; | ||
} | ||
@@ -321,4 +321,4 @@ Write: { | ||
references__other: { __id: Other['Read']['id'] } | [Other['Read']]; | ||
test__has__tag_key?: TestTag['Read'][]; | ||
test_tag?: TestTag['Read'][]; | ||
test__has__tag_key?: Array<TestTag['Read']>; | ||
test_tag?: Array<TestTag['Read']>; | ||
} | ||
@@ -325,0 +325,0 @@ Write: { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
199766
9221
18
700
Updated@balena/sbvr-types@^7.1.0