@balena/abstract-sql-to-typescript
Advanced tools
Comparing version 2.4.0 to 3.0.0-build-3-x-188ac11c28a722c202732b0ee71760e6e0c0d4e4-1
@@ -7,4 +7,11 @@ # Change Log | ||
## 2.4.0 - 2024-04-18 | ||
## 3.0.0 - 2024-05-02 | ||
* 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] | ||
* Expose read vs write selection in generated types [Pagan Gazzard] | ||
## 2.4.0 - 2024-04-19 | ||
* Improve foreign key typings by referencing the appropriate interface [Pagan Gazzard] | ||
@@ -11,0 +18,0 @@ |
@@ -1,7 +0,22 @@ | ||
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 {}; | ||
import type { Types } from '@balena/sbvr-types'; | ||
export type { Types } from '@balena/sbvr-types'; | ||
export type Expanded<T> = Extract<T, any[]>; | ||
export type PickExpanded<T, K extends keyof T = keyof T> = { | ||
[P in K]-?: Expanded<T[P]>; | ||
}; | ||
export type Deferred<T> = Exclude<T, any[]>; | ||
export type PickDeferred<T, K extends keyof T = keyof T> = { | ||
[P in K]: Deferred<T[P]>; | ||
}; | ||
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; | ||
}; | ||
}; |
139
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 = { | ||
read: ` | ||
export type DateString = string; | ||
export type Expanded<T> = Extract<T, any[]>; | ||
export type PickExpanded<T, K extends keyof T = keyof T> = { | ||
[P in K]-?: Expanded<T[P]>; | ||
}; | ||
export type Deferred<T> = Exclude<T, any[]>; | ||
export type PickDeferred<T, K extends keyof T = 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 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 sqlTypeToTypescriptType = (m, f, opts) => { | ||
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 '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 referencedFieldType = `${referencedInterface}['${f.references.fieldName}']`; | ||
if (opts.mode === 'write') { | ||
return referencedFieldType; | ||
} | ||
const nullable = f.required ? '' : '?'; | ||
return `{ __id: ${referencedFieldType} } | [${referencedInterface}${nullable}]`; | ||
} | ||
case 'File': | ||
return 'Buffer'; | ||
case 'JSON': | ||
return 'object'; | ||
case 'WebResource': | ||
return 'WebResource'; | ||
default: | ||
throw new Error(`Unknown data type: '${f.dataType}'`); | ||
} | ||
}; | ||
const fieldsToInterfaceProps = (m, fields, opts) => fields.map((f) => { | ||
const nullable = f.required ? '' : ' | null'; | ||
return `${(0, odata_to_abstract_sql_1.sqlNameToODataName)(f.fieldName)}: ${sqlTypeToTypescriptType(m, f, opts)}${nullable};`; | ||
}); | ||
const recurseRelationships = (m, relationships, inverseSynonyms, opts, 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 = modelNameToCamelCaseName(referencedTable.name); | ||
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, opts, currentTable, `${parentKey}__${key.replace(/ /g, '_')}`); | ||
}); | ||
const relationshipsToInterfaceProps = (m, table, opts) => { | ||
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, opts, table, key.replace(/ /g, '_')); | ||
}); | ||
}; | ||
const tableToInterface = (m, table, opts) => { | ||
const relationshipProps = opts.mode === 'read' ? relationshipsToInterfaceProps(m, table, opts) : []; | ||
return trimNL ` | ||
export interface ${modelNameToCamelCaseName(table.name)} { | ||
${[...fieldsToInterfaceProps(m, table.fields, opts), ...relationshipProps].join('\n\t')} | ||
} | ||
`; | ||
}; | ||
const abstractSqlToTypescriptTypes = (m, opts = {}) => { | ||
const mode = opts.mode ?? 'read'; | ||
const requiredOptions = { | ||
...opts, | ||
mode, | ||
}; | ||
return trimNL ` | ||
${typeHelpers[mode]} | ||
${Object.keys(m.tables) | ||
.map((tableName) => { | ||
const t = m.tables[tableName]; | ||
return tableToInterface(m, t, requiredOptions); | ||
}) | ||
.join('\n\n')} | ||
`; | ||
}; | ||
exports.abstractSqlToTypescriptTypes = abstractSqlToTypescriptTypes; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@balena/abstract-sql-to-typescript", | ||
"version": "2.4.0", | ||
"version": "3.0.0-build-3-x-188ac11c28a722c202732b0ee71760e6e0c0d4e4-1", | ||
"description": "A translator for abstract sql into typescript types.", | ||
"main": "out/index.js", | ||
"types": "out/index.d.ts", | ||
"exports": { | ||
".": "./out/index.js", | ||
"./generate": "./out/generate.js" | ||
}, | ||
"scripts": { | ||
@@ -19,2 +23,3 @@ "pretest": "npm run lint && npm run prepare", | ||
"@balena/odata-to-abstract-sql": "^6.2.3", | ||
"@balena/sbvr-types": "^7.1.0", | ||
"@types/node": "^20.11.24", | ||
@@ -49,4 +54,4 @@ "common-tags": "^1.8.2" | ||
"versionist": { | ||
"publishedAt": "2024-04-18T14:27:51.263Z" | ||
"publishedAt": "2024-05-02T11:06:29.599Z" | ||
} | ||
} |
233
src/index.ts
@@ -1,24 +0,4 @@ | ||
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'; | ||
import type { Types } from '@balena/sbvr-types'; | ||
export type { Types } from '@balena/sbvr-types'; | ||
type RequiredModelSubset = Pick< | ||
AbstractSqlModel, | ||
'tables' | 'relationships' | 'synonyms' | ||
>; | ||
const typeHelpers = { | ||
read: ` | ||
export type DateString = string; | ||
export type Expanded<T> = Extract<T, any[]>; | ||
@@ -32,206 +12,13 @@ export type PickExpanded<T, K extends keyof T = keyof T> = { | ||
}; | ||
export interface WebResource { | ||
filename: string; | ||
href: string; | ||
content_type?: string; | ||
content_disposition?: string; | ||
size?: number; | ||
}; | ||
`, | ||
write: '', | ||
}; | ||
const trimNL = new TemplateTag( | ||
replaceResultTransformer(/^[\r\n]*|[\r\n]*$/g, ''), | ||
); | ||
type ReadTypes = Types[keyof Types]['Read']; | ||
type WriteTypes = Types[keyof Types]['Write']; | ||
const modelNameToCamelCaseName = (s: string): string => | ||
s | ||
.split(/[ -]/) | ||
.map((p) => p[0].toLocaleUpperCase() + p.slice(1)) | ||
.join(''); | ||
const sqlTypeToTypescriptType = ( | ||
m: RequiredModelSubset, | ||
f: AbstractSqlField, | ||
opts: RequiredOptions, | ||
): 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 '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 referencedFieldType = `${referencedInterface}['${f.references!.fieldName}']`; | ||
if (opts.mode === 'write') { | ||
return referencedFieldType; | ||
} | ||
const nullable = f.required ? '' : '?'; | ||
return `{ __id: ${referencedFieldType} } | [${referencedInterface}${nullable}]`; | ||
} | ||
case 'File': | ||
return 'Buffer'; | ||
case 'JSON': | ||
return 'object'; | ||
case 'WebResource': | ||
return 'WebResource'; | ||
default: | ||
throw new Error(`Unknown data type: '${f.dataType}'`); | ||
} | ||
}; | ||
const fieldsToInterfaceProps = ( | ||
m: RequiredModelSubset, | ||
fields: AbstractSqlField[], | ||
opts: RequiredOptions, | ||
): string[] => | ||
fields.map((f) => { | ||
const nullable = f.required ? '' : ' | null'; | ||
return `${sqlNameToODataName(f.fieldName)}: ${sqlTypeToTypescriptType( | ||
m, | ||
f, | ||
opts, | ||
)}${nullable};`; | ||
}); | ||
const recurseRelationships = ( | ||
m: RequiredModelSubset, | ||
relationships: Relationship, | ||
inverseSynonyms: Record<string, string>, | ||
opts: RequiredOptions, | ||
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 = modelNameToCamelCaseName( | ||
referencedTable.name, | ||
); | ||
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, | ||
opts, | ||
currentTable, | ||
`${parentKey}__${key.replace(/ /g, '_')}`, | ||
); | ||
}); | ||
const relationshipsToInterfaceProps = ( | ||
m: RequiredModelSubset, | ||
table: AbstractSqlTable, | ||
opts: RequiredOptions, | ||
): 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, | ||
opts, | ||
table, | ||
key.replace(/ /g, '_'), | ||
); | ||
}); | ||
}; | ||
const tableToInterface = ( | ||
m: RequiredModelSubset, | ||
table: AbstractSqlTable, | ||
opts: RequiredOptions, | ||
) => { | ||
const relationshipProps = | ||
opts.mode === 'read' ? relationshipsToInterfaceProps(m, table, opts) : []; | ||
return trimNL` | ||
export interface ${modelNameToCamelCaseName(table.name)} { | ||
${[...fieldsToInterfaceProps(m, table.fields, opts), ...relationshipProps].join( | ||
'\n\t', | ||
)} | ||
} | ||
`; | ||
}; | ||
export interface Options { | ||
mode?: 'read' | 'write'; | ||
} | ||
type RequiredOptions = Required<Options>; | ||
export const abstractSqlToTypescriptTypes = ( | ||
m: RequiredModelSubset, | ||
opts: Options = {}, | ||
): string => { | ||
const mode = opts.mode ?? 'read'; | ||
const requiredOptions: RequiredOptions = { | ||
...opts, | ||
mode, | ||
export type Resource<T extends object = object> = { | ||
Read: { | ||
[key in keyof T]: ReadTypes | { __id: ReadTypes } | Resource[]; | ||
}; | ||
return trimNL` | ||
${typeHelpers[mode]} | ||
${Object.keys(m.tables) | ||
.map((tableName) => { | ||
const t = m.tables[tableName]; | ||
return tableToInterface(m, t, requiredOptions); | ||
}) | ||
.join('\n\n')} | ||
`; | ||
Write: { | ||
[key in keyof T]: WriteTypes; | ||
}; | ||
}; |
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'; | ||
import { abstractSqlToTypescriptTypes } from '../src/generate'; | ||
@@ -11,3 +10,2 @@ const test = ( | ||
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 = keyof T> = { | ||
[P in K]-?: Expanded<T[P]>; | ||
}; | ||
export type Deferred<T> = Exclude<T, any[]>; | ||
export type PickDeferred<T, K extends keyof T = 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?: Array<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?: Array<TestTag['Read']>; | ||
test_tag?: Array<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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
199764
18
5
700
2
1
+ Added@balena/sbvr-types@^7.1.0