@contember/schema-utils 1.3.0-alpha.6 → 1.3.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/SchemaDatabaseMetadata.d.ts +40 -0
- package/dist/src/SchemaDatabaseMetadata.d.ts.map +1 -0
- package/dist/src/SchemaDatabaseMetadata.js +20 -0
- package/dist/src/SchemaDatabaseMetadata.js.map +1 -0
- package/dist/src/definition-generator/AclDefinitionCodeGenerator.d.ts +19 -0
- package/dist/src/definition-generator/AclDefinitionCodeGenerator.d.ts.map +1 -0
- package/dist/src/definition-generator/AclDefinitionCodeGenerator.js +116 -0
- package/dist/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -0
- package/dist/src/definition-generator/DefinitionCodeGenerator.d.ts +27 -0
- package/dist/src/definition-generator/DefinitionCodeGenerator.d.ts.map +1 -0
- package/dist/src/definition-generator/DefinitionCodeGenerator.js +216 -0
- package/dist/src/definition-generator/DefinitionCodeGenerator.js.map +1 -0
- package/dist/src/definition-generator/DefinitionNamingConventions.d.ts +7 -0
- package/dist/src/definition-generator/DefinitionNamingConventions.d.ts.map +1 -0
- package/dist/src/definition-generator/DefinitionNamingConventions.js +18 -0
- package/dist/src/definition-generator/DefinitionNamingConventions.js.map +1 -0
- package/dist/src/definition-generator/TsDefinitionGenerator.d.ts +4 -18
- package/dist/src/definition-generator/TsDefinitionGenerator.d.ts.map +1 -1
- package/dist/src/definition-generator/TsDefinitionGenerator.js +6 -232
- package/dist/src/definition-generator/TsDefinitionGenerator.js.map +1 -1
- package/dist/src/definition-generator/index.d.ts +3 -0
- package/dist/src/definition-generator/index.d.ts.map +1 -1
- package/dist/src/definition-generator/index.js +3 -0
- package/dist/src/definition-generator/index.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lax/LaxSchemaConverter.js +2 -2
- package/dist/src/model/index.d.ts +0 -1
- package/dist/src/model/index.d.ts.map +1 -1
- package/dist/src/model/index.js +0 -1
- package/dist/src/model/index.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/type-schema/model.d.ts +4 -6
- package/dist/src/type-schema/model.d.ts.map +1 -1
- package/dist/src/type-schema/model.js +5 -6
- package/dist/src/type-schema/model.js.map +1 -1
- package/dist/src/utils/printJsValue.d.ts +13 -0
- package/dist/src/utils/printJsValue.d.ts.map +1 -0
- package/dist/src/utils/printJsValue.js +57 -0
- package/dist/src/utils/printJsValue.js.map +1 -0
- package/dist/src/validation/ModelValidator.d.ts.map +1 -1
- package/dist/src/validation/ModelValidator.js +4 -30
- package/dist/src/validation/ModelValidator.js.map +1 -1
- package/dist/tests/cases/unit/modelValidator.test.js +6 -72
- package/dist/tests/cases/unit/modelValidator.test.js.map +1 -1
- package/dist/tests/cases/unit/printJsvalue.test.d.ts +2 -0
- package/dist/tests/cases/unit/printJsvalue.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/printJsvalue.test.js +90 -0
- package/dist/tests/cases/unit/printJsvalue.test.js.map +1 -0
- package/dist/tests/cases/unit/schemas/acl.d.ts +13 -0
- package/dist/tests/cases/unit/schemas/acl.d.ts.map +1 -0
- package/dist/tests/cases/unit/schemas/acl.js +47 -0
- package/dist/tests/cases/unit/schemas/acl.js.map +1 -0
- package/dist/tests/cases/unit/schemas/basic.d.ts.map +1 -1
- package/dist/tests/cases/unit/schemas/basic.js.map +1 -1
- package/dist/tests/cases/unit/schemas/enum.d.ts.map +1 -1
- package/dist/tests/cases/unit/schemas/enum.js.map +1 -1
- package/dist/tests/cases/unit/schemas/relations.d.ts.map +1 -1
- package/dist/tests/cases/unit/schemas/relations.js.map +1 -1
- package/dist/tests/cases/unit/schemas/unique.d.ts.map +1 -1
- package/dist/tests/cases/unit/schemas/unique.js.map +1 -1
- package/dist/tests/cases/unit/tsDefinitionGenerator.test.js +5 -3
- package/dist/tests/cases/unit/tsDefinitionGenerator.test.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -6
- package/src/SchemaDatabaseMetadata.ts +66 -0
- package/src/definition-generator/AclDefinitionCodeGenerator.ts +126 -0
- package/src/definition-generator/DefinitionCodeGenerator.ts +250 -0
- package/src/definition-generator/DefinitionNamingConventions.ts +16 -0
- package/src/definition-generator/TsDefinitionGenerator.ts +8 -264
- package/src/definition-generator/index.ts +3 -0
- package/src/index.ts +1 -0
- package/src/lax/LaxSchemaConverter.ts +2 -2
- package/src/model/index.ts +0 -1
- package/src/type-schema/model.ts +20 -8
- package/src/utils/printJsValue.ts +62 -0
- package/src/validation/ModelValidator.ts +11 -35
- package/tests/cases/unit/__snapshots__/laxSchemaConverter.test.ts.snap +14 -14
- package/tests/cases/unit/modelValidator.test.ts +6 -73
- package/tests/cases/unit/printJsvalue.test.ts +100 -0
- package/tests/cases/unit/schemas/acl.ts +34 -0
- package/tests/cases/unit/schemas/basic.ts +1 -1
- package/tests/cases/unit/schemas/enum.ts +1 -1
- package/tests/cases/unit/schemas/relations.ts +1 -1
- package/tests/cases/unit/schemas/unique.ts +1 -1
- package/tests/cases/unit/tsDefinitionGenerator.test.ts +6 -4
- package/dist/src/model/NamingHelper.d.ts +0 -9
- package/dist/src/model/NamingHelper.d.ts.map +0 -1
- package/dist/src/model/NamingHelper.js +0 -44
- package/dist/src/model/NamingHelper.js.map +0 -1
- package/src/model/NamingHelper.ts +0 -41
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Model, Schema, Writable } from '@contember/schema'
|
|
2
|
+
import {
|
|
3
|
+
acceptFieldVisitor,
|
|
4
|
+
DefaultNamingConventions,
|
|
5
|
+
isInverseRelation,
|
|
6
|
+
NamingConventions,
|
|
7
|
+
resolveDefaultColumnType,
|
|
8
|
+
} from '../model'
|
|
9
|
+
import { printJsValue } from '../utils/printJsValue'
|
|
10
|
+
import { DefinitionNamingConventions } from './DefinitionNamingConventions'
|
|
11
|
+
import { AclDefinitionCodeGenerator } from './AclDefinitionCodeGenerator'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export class DefinitionCodeGenerator {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly schemaNamingConventions: NamingConventions = new DefaultNamingConventions(),
|
|
17
|
+
private readonly definitionNamingConventions = new DefinitionNamingConventions(),
|
|
18
|
+
private readonly aclGenerator = new AclDefinitionCodeGenerator(),
|
|
19
|
+
) {
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public generate(schema: Schema) {
|
|
23
|
+
const roles = this.aclGenerator.generateRoles({ acl: schema.acl })
|
|
24
|
+
const aclVariables = this.aclGenerator.generateAclVariables({ acl: schema.acl })
|
|
25
|
+
|
|
26
|
+
const enums = Object.entries(schema.model.enums).map(([name, values]) => this.generateEnum({
|
|
27
|
+
name,
|
|
28
|
+
values,
|
|
29
|
+
})).join('')
|
|
30
|
+
|
|
31
|
+
const entities = Object.values(schema.model.entities).map(entity => this.generateEntity({ entity, schema })).join('')
|
|
32
|
+
|
|
33
|
+
return `import { SchemaDefinition as def, AclDefinition as acl } from '@contember/schema-definition'
|
|
34
|
+
${roles}${aclVariables}${enums}${entities}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
private generateEnum({ name, values }: { name: string; values: readonly string[] }): string {
|
|
39
|
+
return `\nexport const ${this.formatIdentifier(name)} = def.createEnum(${values.map(it => printJsValue(it)).join(', ')})\n`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public generateEntity({ entity, schema }: { entity: Model.Entity; schema: Schema }): string {
|
|
43
|
+
const decorators = [
|
|
44
|
+
...Object.values(entity.unique).map(constraint => this.generateUniqueConstraint({ entity, constraint })),
|
|
45
|
+
...Object.values(entity.indexes).map(index => this.generateIndex({ entity, index })),
|
|
46
|
+
this.generateView({ entity }),
|
|
47
|
+
].filter(it => !!it).map(it => `${it}\n`).join('')
|
|
48
|
+
const acl = this.aclGenerator.generateEntityAcl({ entity, schema })
|
|
49
|
+
|
|
50
|
+
return `\n${decorators}${acl}export class ${this.formatIdentifier(entity.name)} {
|
|
51
|
+
${Object.values(entity.fields).map(field => this.generateField({ field, entity, schema })).filter(it => !!it).join('\n')}
|
|
52
|
+
}\n`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private generateUniqueConstraint({ entity, constraint }: {
|
|
56
|
+
entity: Model.Entity
|
|
57
|
+
constraint: Model.UniqueConstraint
|
|
58
|
+
}): string {
|
|
59
|
+
const fieldsList = `${constraint.fields.map(it => printJsValue(it)).join(', ')}`
|
|
60
|
+
return `@def.Unique(${fieldsList})`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private generateIndex({ entity, index }: { entity: Model.Entity; index: Model.Index }): string {
|
|
64
|
+
const fieldsList = `${index.fields.map(it => printJsValue(it)).join(', ')}`
|
|
65
|
+
return `@def.Index(${fieldsList})`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
private generateView({ entity }: { entity: Model.Entity }): string | undefined {
|
|
70
|
+
if (!entity.view) {
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const dependenciesExpr = (entity.view.dependencies?.length ?? 0) > 0
|
|
75
|
+
? `, {\n\tdependencies: () => [${entity.view.dependencies?.map(it => this.formatIdentifier(it))}]\n}`
|
|
76
|
+
: ''
|
|
77
|
+
|
|
78
|
+
return `@def.View(\`${entity.view.sql}\`${dependenciesExpr})`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public generateField({ entity, field, schema }: { entity: Model.Entity; field: Model.AnyField; schema: Schema }): string | undefined {
|
|
82
|
+
const formatRelationFactory = (method: string, relation: Model.AnyRelation) => {
|
|
83
|
+
const otherSide = isInverseRelation(relation) ? relation.ownedBy : relation.inversedBy
|
|
84
|
+
const otherSideFormatted = otherSide ? `, ${printJsValue(otherSide)}` : ''
|
|
85
|
+
return `${method}(${this.formatIdentifier(relation.target)}${otherSideFormatted})`
|
|
86
|
+
}
|
|
87
|
+
const formatEnumRef = (enumName: string, enumValues: Record<string, string>, providedValue?: string, defaultValue?: string): string | undefined => {
|
|
88
|
+
if (!providedValue || providedValue === defaultValue) {
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
const enumValueKey = Object.entries(Model.OrderDirection).find(dir => dir[1] === providedValue)?.[0]
|
|
92
|
+
if (!enumValueKey) {
|
|
93
|
+
throw new Error(`Value ${providedValue} is not defined in enum ${enumName}`)
|
|
94
|
+
}
|
|
95
|
+
return `${enumName}.${enumValueKey}`
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
const formatOrderBy = (orderBy?: readonly Model.OrderBy[]) => {
|
|
99
|
+
return orderBy?.map(it => {
|
|
100
|
+
const enumExpr = formatEnumRef(`Model.OrderDirection`, Model.OrderDirection, it.direction, Model.OrderDirection.asc)
|
|
101
|
+
return `orderBy(${printJsValue(it.path)}${enumExpr ? `, ${enumExpr}` : ''})`
|
|
102
|
+
}) ?? []
|
|
103
|
+
}
|
|
104
|
+
const formatOnDelete = (onDelete?: Model.OnDelete): string | undefined => {
|
|
105
|
+
if (onDelete === Model.OnDelete.cascade) {
|
|
106
|
+
return 'cascadeOnDelete()'
|
|
107
|
+
}
|
|
108
|
+
if (onDelete === Model.OnDelete.setNull) {
|
|
109
|
+
return 'setNullOnDelete()'
|
|
110
|
+
}
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
const formatJoiningColumn = (joiningColumnName: string, fieldName: string): string | undefined => {
|
|
114
|
+
const defaultJoiningColumn = this.schemaNamingConventions.getJoiningColumnName(fieldName)
|
|
115
|
+
if (defaultJoiningColumn === joiningColumnName) {
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
return `joiningColumn(${printJsValue(joiningColumnName)})`
|
|
119
|
+
}
|
|
120
|
+
const definition = acceptFieldVisitor<(string | undefined)[]>(schema.model, entity, field, {
|
|
121
|
+
visitColumn: ctx => {
|
|
122
|
+
return this.generateColumn(ctx)
|
|
123
|
+
},
|
|
124
|
+
visitOneHasMany: ({ relation }) => {
|
|
125
|
+
return [
|
|
126
|
+
formatRelationFactory('oneHasMany', relation),
|
|
127
|
+
...formatOrderBy(relation.orderBy),
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
visitManyHasOne: ({ relation }) => {
|
|
131
|
+
return [
|
|
132
|
+
formatRelationFactory('manyHasOne', relation),
|
|
133
|
+
!relation.nullable ? 'notNull()' : undefined,
|
|
134
|
+
formatOnDelete(relation.joiningColumn.onDelete),
|
|
135
|
+
formatJoiningColumn(relation.joiningColumn.columnName, relation.name),
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
visitOneHasOneInverse: ({ relation }) => {
|
|
139
|
+
return [
|
|
140
|
+
formatRelationFactory('oneHasOneInverse', relation),
|
|
141
|
+
!relation.nullable ? 'notNull()' : undefined,
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
visitOneHasOneOwning: ({ relation }) => {
|
|
145
|
+
return [
|
|
146
|
+
formatRelationFactory('oneHasOne', relation),
|
|
147
|
+
!relation.nullable ? 'notNull()' : undefined,
|
|
148
|
+
formatOnDelete(relation.joiningColumn.onDelete),
|
|
149
|
+
formatJoiningColumn(relation.joiningColumn.columnName, relation.name),
|
|
150
|
+
relation.orphanRemoval ? 'removeOrphan()' : undefined,
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
visitManyHasManyOwning: ({ entity, relation }) => {
|
|
154
|
+
const columnNames = this.schemaNamingConventions.getJoiningTableColumnNames(
|
|
155
|
+
entity.name,
|
|
156
|
+
relation.name,
|
|
157
|
+
relation.target,
|
|
158
|
+
relation.inversedBy,
|
|
159
|
+
)
|
|
160
|
+
const defaultJoiningTable = this.schemaNamingConventions.getJoiningTableName(entity.name, relation.name)
|
|
161
|
+
const joiningTable: Writable<Partial<Model.JoiningTable>> = {}
|
|
162
|
+
if (relation.joiningTable.tableName !== defaultJoiningTable) {
|
|
163
|
+
joiningTable.tableName = relation.joiningTable.tableName
|
|
164
|
+
}
|
|
165
|
+
if (!relation.joiningTable.eventLog.enabled) {
|
|
166
|
+
joiningTable.eventLog = { enabled: false }
|
|
167
|
+
}
|
|
168
|
+
if (columnNames[0] !== relation.joiningTable.joiningColumn.columnName || relation.joiningTable.joiningColumn.onDelete !== Model.OnDelete.cascade) {
|
|
169
|
+
joiningTable.joiningColumn = {
|
|
170
|
+
columnName: relation.joiningTable.joiningColumn.columnName,
|
|
171
|
+
onDelete: relation.joiningTable.joiningColumn.onDelete,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (columnNames[1] !== relation.joiningTable.inverseJoiningColumn.columnName || relation.joiningTable.inverseJoiningColumn.onDelete !== Model.OnDelete.cascade) {
|
|
175
|
+
joiningTable.inverseJoiningColumn = {
|
|
176
|
+
columnName: relation.joiningTable.inverseJoiningColumn.columnName,
|
|
177
|
+
onDelete: relation.joiningTable.inverseJoiningColumn.onDelete,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [
|
|
182
|
+
formatRelationFactory('manyHasMany', relation),
|
|
183
|
+
...formatOrderBy(relation.orderBy),
|
|
184
|
+
Object.keys(joiningTable).length > 0 ? `joiningTable(${printJsValue(joiningTable)})` : undefined,
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
visitManyHasManyInverse: ({ relation }) => {
|
|
188
|
+
return [
|
|
189
|
+
formatRelationFactory('manyHasManyInverse', relation),
|
|
190
|
+
...formatOrderBy(relation.orderBy),
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
const definitionCode = definition.filter(it => !!it).join('.')
|
|
195
|
+
if (field.name === 'id' && definitionCode === 'uuidColumn().notNull()') {
|
|
196
|
+
return undefined
|
|
197
|
+
}
|
|
198
|
+
return `\t${this.formatIdentifier(field.name)} = def.${definitionCode}`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private generateColumn({ entity, column }: { entity: Model.Entity; column: Model.AnyColumn }): string[] {
|
|
202
|
+
let parts: string[] = []
|
|
203
|
+
if (column.type === Model.ColumnType.Enum) {
|
|
204
|
+
parts.push(`enumColumn(${column.columnType})`)
|
|
205
|
+
} else {
|
|
206
|
+
parts.push(`${ColumnToMethodMapping[column.type]}()`)
|
|
207
|
+
const defaultColumnType = resolveDefaultColumnType(column.type)
|
|
208
|
+
if (defaultColumnType !== column.columnType) {
|
|
209
|
+
parts.push(`columnType(${printJsValue(column.columnType)})`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const defaultColumnName = this.schemaNamingConventions.getColumnName(column.name)
|
|
213
|
+
if (defaultColumnName !== column.columnName) {
|
|
214
|
+
parts.push(`columnName(${printJsValue(column.columnName)})`)
|
|
215
|
+
}
|
|
216
|
+
if (!column.nullable) {
|
|
217
|
+
parts.push('notNull()')
|
|
218
|
+
}
|
|
219
|
+
if (column.default !== undefined) {
|
|
220
|
+
parts.push(`default(${printJsValue(column.default)})`)
|
|
221
|
+
}
|
|
222
|
+
if (column.typeAlias) {
|
|
223
|
+
parts.push(`typeAlias(${printJsValue(column.typeAlias)})`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
// todo: sequence
|
|
228
|
+
// todo: maybe single column unique()
|
|
229
|
+
|
|
230
|
+
return parts
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private formatIdentifier(id: string): string {
|
|
234
|
+
return this.definitionNamingConventions.formatIdentifier(id)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
const ColumnToMethodMapping: {
|
|
240
|
+
[K in Exclude<Model.ColumnType, Model.ColumnType.Enum>]: string
|
|
241
|
+
} = {
|
|
242
|
+
[Model.ColumnType.Bool]: 'boolColumn',
|
|
243
|
+
[Model.ColumnType.Date]: 'dateColumn',
|
|
244
|
+
[Model.ColumnType.DateTime]: 'dateTimeColumn',
|
|
245
|
+
[Model.ColumnType.Json]: 'jsonColumn',
|
|
246
|
+
[Model.ColumnType.Double]: 'doubleColumn',
|
|
247
|
+
[Model.ColumnType.Uuid]: 'uuidColumn',
|
|
248
|
+
[Model.ColumnType.Int]: 'intColumn',
|
|
249
|
+
[Model.ColumnType.String]: 'stringColumn',
|
|
250
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class DefinitionNamingConventions {
|
|
2
|
+
private static reservedWords = new Set(['do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'await', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof'])
|
|
3
|
+
|
|
4
|
+
public formatIdentifier(id: string): string {
|
|
5
|
+
// todo: validate
|
|
6
|
+
return id
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
public roleVarName(id: string): string {
|
|
10
|
+
return this.formatIdentifier(`${id}Role`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public variableVarName(role: string, id: string): string {
|
|
14
|
+
return this.formatIdentifier(`${id}${role.charAt(0).toUpperCase() + role.slice(1)}Variable`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
DefaultNamingConventions,
|
|
5
|
-
isInverseRelation,
|
|
6
|
-
NamingConventions,
|
|
7
|
-
NamingHelper,
|
|
8
|
-
resolveDefaultColumnType,
|
|
9
|
-
} from '../model'
|
|
1
|
+
import { Schema } from '@contember/schema'
|
|
2
|
+
import { DefaultNamingConventions, NamingConventions } from '../model'
|
|
3
|
+
import { DefinitionCodeGenerator } from './DefinitionCodeGenerator'
|
|
10
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated use {@link DefinitionCodeGenerator}
|
|
7
|
+
*/
|
|
11
8
|
export class TsDefinitionGenerator {
|
|
12
|
-
private static reservedWords = new Set(['do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'await', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof'])
|
|
13
|
-
|
|
14
9
|
constructor(
|
|
15
10
|
private readonly schema: Schema,
|
|
16
11
|
private readonly conventions: NamingConventions = new DefaultNamingConventions(),
|
|
@@ -18,258 +13,7 @@ export class TsDefinitionGenerator {
|
|
|
18
13
|
}
|
|
19
14
|
|
|
20
15
|
public generate() {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
values,
|
|
24
|
-
})).join('')
|
|
25
|
-
const entities = Object.values(this.schema.model.entities).map(entity => this.generateEntity({ entity })).join('')
|
|
26
|
-
return `import { SchemaDefinition as def } from '@contember/schema-definition'
|
|
27
|
-
${enums}${entities}`
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
private generateEnum({ name, values }: { name: string; values: readonly string[] }): string {
|
|
31
|
-
return `\nexport const ${this.formatIdentifier(name)} = def.createEnum(${values.map(it => this.formatLiteral(it)).join(', ')})\n`
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
public generateEntity({ entity }: { entity: Model.Entity }): string {
|
|
36
|
-
const decorators = [
|
|
37
|
-
...Object.values(entity.unique).map(constraint => this.generateUniqueConstraint({ entity, constraint })),
|
|
38
|
-
...Object.values(entity.indexes).map(index => this.generateIndex({ entity, index })),
|
|
39
|
-
this.generateView({ entity }),
|
|
40
|
-
].filter(it => !!it).map(it => `${it}\n`).join('')
|
|
41
|
-
|
|
42
|
-
return `\n${decorators}export class ${this.formatIdentifier(entity.name)} {
|
|
43
|
-
${Object.values(entity.fields).map(field => this.generateField({ field, entity })).filter(it => !!it).join('\n')}
|
|
44
|
-
}\n`
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private generateUniqueConstraint({ entity, constraint }: { entity: Model.Entity; constraint: Model.UniqueConstraint }): string {
|
|
48
|
-
const defaultName = NamingHelper.createUniqueConstraintName(entity.name, constraint.fields)
|
|
49
|
-
if (defaultName === constraint.name) {
|
|
50
|
-
const fieldsList = `${constraint.fields.map(it => this.formatLiteral(it)).join(', ')}`
|
|
51
|
-
return `@def.Unique(${fieldsList})`
|
|
52
|
-
}
|
|
53
|
-
return `@def.Unique(${this.formatLiteral(constraint)})`
|
|
16
|
+
const generator = new DefinitionCodeGenerator(this.conventions)
|
|
17
|
+
return generator.generate(this.schema)
|
|
54
18
|
}
|
|
55
|
-
|
|
56
|
-
private generateIndex({ entity, index }: { entity: Model.Entity; index: Model.Index }): string {
|
|
57
|
-
const defaultName = NamingHelper.createIndexName(entity.name, index.fields)
|
|
58
|
-
if (defaultName === index.name) {
|
|
59
|
-
const fieldsList = `${index.fields.map(it => this.formatLiteral(it)).join(', ')}`
|
|
60
|
-
return `@def.Index(${fieldsList})`
|
|
61
|
-
}
|
|
62
|
-
return `@def.Index(${this.formatLiteral(index)})`
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private generateView({ entity }: { entity: Model.Entity }): string | undefined {
|
|
66
|
-
if (!entity.view) {
|
|
67
|
-
return undefined
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const dependenciesExpr = (entity.view.dependencies?.length ?? 0) > 0
|
|
71
|
-
? `, {\n\tdependencies: () => [${entity.view.dependencies?.map(it => this.formatIdentifier(it))}]\n}`
|
|
72
|
-
: ''
|
|
73
|
-
|
|
74
|
-
return `@def.View(\`${entity.view.sql}\`${dependenciesExpr})`
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
public generateField({ entity, field }: { entity: Model.Entity; field: Model.AnyField }): string | undefined {
|
|
78
|
-
const formatRelationFactory = (method: string, relation: Model.AnyRelation) => {
|
|
79
|
-
const otherSide = isInverseRelation(relation) ? relation.ownedBy : relation.inversedBy
|
|
80
|
-
const otherSideFormatted = otherSide ? `, ${this.formatLiteral(otherSide)}` : ''
|
|
81
|
-
return `${method}(${this.formatIdentifier(relation.target)}${otherSideFormatted})`
|
|
82
|
-
}
|
|
83
|
-
const formatEnumRef = (enumName: string, enumValues: Record<string, string>, providedValue?: string, defaultValue?: string): string | undefined => {
|
|
84
|
-
if (!providedValue || providedValue === defaultValue) {
|
|
85
|
-
return undefined
|
|
86
|
-
}
|
|
87
|
-
const enumValueKey = Object.entries(Model.OrderDirection).find(dir => dir[1] === providedValue)?.[0]
|
|
88
|
-
if (!enumValueKey) {
|
|
89
|
-
throw new Error(`Value ${providedValue} is not defined in enum ${enumName}`)
|
|
90
|
-
}
|
|
91
|
-
return `${enumName}.${enumValueKey}`
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
const formatOrderBy = (orderBy?: readonly Model.OrderBy[]) => {
|
|
95
|
-
return orderBy?.map(it => {
|
|
96
|
-
const enumExpr = formatEnumRef(`Model.OrderDirection`, Model.OrderDirection, it.direction, Model.OrderDirection.asc)
|
|
97
|
-
return `orderBy(${this.formatLiteral(it.path)}${enumExpr ? `, ${enumExpr}` : ''})`
|
|
98
|
-
}) ?? []
|
|
99
|
-
}
|
|
100
|
-
const formatOnDelete = (onDelete?: Model.OnDelete): string | undefined => {
|
|
101
|
-
if (onDelete === Model.OnDelete.cascade) {
|
|
102
|
-
return 'cascadeOnDelete()'
|
|
103
|
-
}
|
|
104
|
-
if (onDelete === Model.OnDelete.setNull) {
|
|
105
|
-
return 'setNullOnDelete()'
|
|
106
|
-
}
|
|
107
|
-
return undefined
|
|
108
|
-
}
|
|
109
|
-
const formatJoiningColumn = (joiningColumnName: string, fieldName: string): string | undefined => {
|
|
110
|
-
const defaultJoiningColumn = this.conventions.getJoiningColumnName(fieldName)
|
|
111
|
-
if (defaultJoiningColumn === joiningColumnName) {
|
|
112
|
-
return undefined
|
|
113
|
-
}
|
|
114
|
-
return `joiningColumn(${this.formatLiteral(joiningColumnName)})`
|
|
115
|
-
}
|
|
116
|
-
const definition = acceptFieldVisitor<(string | undefined)[]>(this.schema.model, entity, field, {
|
|
117
|
-
visitColumn: ctx => {
|
|
118
|
-
return this.generateColumn(ctx)
|
|
119
|
-
},
|
|
120
|
-
visitOneHasMany: ({ relation }) => {
|
|
121
|
-
return [
|
|
122
|
-
formatRelationFactory('oneHasMany', relation),
|
|
123
|
-
...formatOrderBy(relation.orderBy),
|
|
124
|
-
]
|
|
125
|
-
},
|
|
126
|
-
visitManyHasOne: ({ relation }) => {
|
|
127
|
-
return [
|
|
128
|
-
formatRelationFactory('manyHasOne', relation),
|
|
129
|
-
!relation.nullable ? 'notNull()' : undefined,
|
|
130
|
-
formatOnDelete(relation.joiningColumn.onDelete),
|
|
131
|
-
formatJoiningColumn(relation.joiningColumn.columnName, relation.name),
|
|
132
|
-
]
|
|
133
|
-
},
|
|
134
|
-
visitOneHasOneInverse: ({ relation }) => {
|
|
135
|
-
return [
|
|
136
|
-
formatRelationFactory('oneHasOneInverse', relation),
|
|
137
|
-
!relation.nullable ? 'notNull()' : undefined,
|
|
138
|
-
]
|
|
139
|
-
},
|
|
140
|
-
visitOneHasOneOwning: ({ relation }) => {
|
|
141
|
-
return [
|
|
142
|
-
formatRelationFactory('oneHasOne', relation),
|
|
143
|
-
!relation.nullable ? 'notNull()' : undefined,
|
|
144
|
-
formatOnDelete(relation.joiningColumn.onDelete),
|
|
145
|
-
formatJoiningColumn(relation.joiningColumn.columnName, relation.name),
|
|
146
|
-
relation.orphanRemoval ? 'removeOrphan()' : undefined,
|
|
147
|
-
]
|
|
148
|
-
},
|
|
149
|
-
visitManyHasManyOwning: ({ entity, relation }) => {
|
|
150
|
-
const columnNames = this.conventions.getJoiningTableColumnNames(
|
|
151
|
-
entity.name,
|
|
152
|
-
relation.name,
|
|
153
|
-
relation.target,
|
|
154
|
-
relation.inversedBy,
|
|
155
|
-
)
|
|
156
|
-
const defaultJoiningTable = this.conventions.getJoiningTableName(entity.name, relation.name)
|
|
157
|
-
const joiningTable: Writable<Partial<Model.JoiningTable>> = {}
|
|
158
|
-
if (relation.joiningTable.tableName !== defaultJoiningTable) {
|
|
159
|
-
joiningTable.tableName = relation.joiningTable.tableName
|
|
160
|
-
}
|
|
161
|
-
if (!relation.joiningTable.eventLog.enabled) {
|
|
162
|
-
joiningTable.eventLog = { enabled: false }
|
|
163
|
-
}
|
|
164
|
-
if (columnNames[0] !== relation.joiningTable.joiningColumn.columnName || relation.joiningTable.joiningColumn.onDelete !== Model.OnDelete.cascade) {
|
|
165
|
-
joiningTable.joiningColumn = {
|
|
166
|
-
columnName: relation.joiningTable.joiningColumn.columnName,
|
|
167
|
-
onDelete: relation.joiningTable.joiningColumn.onDelete,
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (columnNames[1] !== relation.joiningTable.inverseJoiningColumn.columnName || relation.joiningTable.inverseJoiningColumn.onDelete !== Model.OnDelete.cascade) {
|
|
171
|
-
joiningTable.inverseJoiningColumn = {
|
|
172
|
-
columnName: relation.joiningTable.inverseJoiningColumn.columnName,
|
|
173
|
-
onDelete: relation.joiningTable.inverseJoiningColumn.onDelete,
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return [
|
|
178
|
-
formatRelationFactory('manyHasMany', relation),
|
|
179
|
-
...formatOrderBy(relation.orderBy),
|
|
180
|
-
Object.keys(joiningTable).length > 0 ? `joiningTable(${this.formatLiteral(joiningTable)})` : undefined,
|
|
181
|
-
]
|
|
182
|
-
},
|
|
183
|
-
visitManyHasManyInverse: ({ relation }) => {
|
|
184
|
-
return [
|
|
185
|
-
formatRelationFactory('manyHasManyInverse', relation),
|
|
186
|
-
...formatOrderBy(relation.orderBy),
|
|
187
|
-
]
|
|
188
|
-
},
|
|
189
|
-
})
|
|
190
|
-
const definitionCode = definition.filter(it => !!it).join('.')
|
|
191
|
-
if (field.name === 'id' && definitionCode === 'uuidColumn().notNull()') {
|
|
192
|
-
return undefined
|
|
193
|
-
}
|
|
194
|
-
return `\t${this.formatIdentifier(field.name)} = def.${definitionCode}`
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private generateColumn({ entity, column }: { entity: Model.Entity; column: Model.AnyColumn }): string[] {
|
|
198
|
-
let parts: string[] = []
|
|
199
|
-
if (column.type === Model.ColumnType.Enum) {
|
|
200
|
-
parts.push(`enumColumn(${column.columnType})`)
|
|
201
|
-
} else {
|
|
202
|
-
parts.push(`${ColumnToMethodMapping[column.type]}()`)
|
|
203
|
-
const defaultColumnType = resolveDefaultColumnType(column.type)
|
|
204
|
-
if (defaultColumnType !== column.columnType) {
|
|
205
|
-
parts.push(`columnType(${this.formatLiteral(column.columnType)})`)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
const defaultColumnName = this.conventions.getColumnName(column.name)
|
|
209
|
-
if (defaultColumnName !== column.columnName) {
|
|
210
|
-
parts.push(`columnName(${this.formatLiteral(column.columnName)})`)
|
|
211
|
-
}
|
|
212
|
-
if (!column.nullable) {
|
|
213
|
-
parts.push('notNull()')
|
|
214
|
-
}
|
|
215
|
-
if (column.default !== undefined) {
|
|
216
|
-
parts.push(`default(${this.formatLiteral(column.default)})`)
|
|
217
|
-
}
|
|
218
|
-
if (column.typeAlias) {
|
|
219
|
-
parts.push(`typeAlias(${this.formatLiteral(column.typeAlias)})`)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// todo: sequence
|
|
224
|
-
// todo: maybe single column unique()
|
|
225
|
-
|
|
226
|
-
return parts
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private formatIdentifier(id: string): string {
|
|
230
|
-
// todo: validate
|
|
231
|
-
return id
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
private formatLiteral(value: any): string {
|
|
235
|
-
if (value === undefined || value === null || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'function') {
|
|
236
|
-
return String(value)
|
|
237
|
-
}
|
|
238
|
-
if (typeof value === 'bigint') {
|
|
239
|
-
return value.toString(10) + 'n'
|
|
240
|
-
}
|
|
241
|
-
if (typeof value === 'string') {
|
|
242
|
-
return `'${value.replaceAll(/'/g, '\\\'')}'`
|
|
243
|
-
}
|
|
244
|
-
if (Array.isArray(value)) {
|
|
245
|
-
return `[${value.map(it => this.formatLiteral(it)).join(', ')}]`
|
|
246
|
-
}
|
|
247
|
-
return `{${Object.entries(value).map(([key, value]) => {
|
|
248
|
-
const formattedKey = this.isSimpleIdentifier(key) ? key : `[${this.formatLiteral(key)}]`
|
|
249
|
-
return `${formattedKey}: ${this.formatLiteral(value)}`
|
|
250
|
-
}).join(', ')}`
|
|
251
|
-
}
|
|
252
|
-
private isValidIdentifier(identifier: string): boolean {
|
|
253
|
-
if (!this.isSimpleIdentifier(identifier)) {
|
|
254
|
-
return false
|
|
255
|
-
}
|
|
256
|
-
return !TsDefinitionGenerator.reservedWords.has(identifier)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private isSimpleIdentifier(identifier: string): boolean {
|
|
260
|
-
return !!identifier.match(/^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const ColumnToMethodMapping: {
|
|
265
|
-
[K in Exclude<Model.ColumnType, Model.ColumnType.Enum>]: string
|
|
266
|
-
} = {
|
|
267
|
-
[Model.ColumnType.Bool]: 'boolColumn',
|
|
268
|
-
[Model.ColumnType.Date]: 'dateColumn',
|
|
269
|
-
[Model.ColumnType.DateTime]: 'dateTimeColumn',
|
|
270
|
-
[Model.ColumnType.Json]: 'jsonColumn',
|
|
271
|
-
[Model.ColumnType.Double]: 'doubleColumn',
|
|
272
|
-
[Model.ColumnType.Uuid]: 'uuidColumn',
|
|
273
|
-
[Model.ColumnType.Int]: 'intColumn',
|
|
274
|
-
[Model.ColumnType.String]: 'stringColumn',
|
|
275
19
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { aclSchema, modelSchema, validationSchema, settingsSchema } from './type
|
|
|
5
5
|
import { actionsSchema } from './type-schema/actions'
|
|
6
6
|
|
|
7
7
|
export * from './definition-generator'
|
|
8
|
+
export * from './SchemaDatabaseMetadata'
|
|
8
9
|
export * from './lax'
|
|
9
10
|
export * from './model'
|
|
10
11
|
export * from './acl'
|
|
@@ -192,8 +192,8 @@ class LaxSchemaBuilder {
|
|
|
192
192
|
private getEntity(entityName: string): Model.Entity {
|
|
193
193
|
return this.entities[entityName] ??= {
|
|
194
194
|
name: entityName,
|
|
195
|
-
unique:
|
|
196
|
-
indexes:
|
|
195
|
+
unique: [],
|
|
196
|
+
indexes: [],
|
|
197
197
|
primary: 'id',
|
|
198
198
|
primaryColumn: 'id',
|
|
199
199
|
tableName: this.conventions.getTableName(entityName),
|
package/src/model/index.ts
CHANGED
package/src/type-schema/model.ts
CHANGED
|
@@ -148,10 +148,22 @@ const viewSchemaCheck: Typesafe.Equals<Model.View, ReturnType<typeof viewSchemaI
|
|
|
148
148
|
const viewSchema: Typesafe.Type<Model.View> = viewSchemaInner
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
})
|
|
151
|
+
const indexLike: Typesafe.Type<{readonly fields: readonly string[]; readonly name?: string | undefined}> = Typesafe.intersection(
|
|
152
|
+
Typesafe.object({
|
|
153
|
+
fields: Typesafe.array(Typesafe.string),
|
|
154
|
+
}),
|
|
155
|
+
Typesafe.partial({
|
|
156
|
+
name: Typesafe.string,
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const indexesSchema = Typesafe.coalesce<Model.Indexes, Model.Indexes>(
|
|
161
|
+
Typesafe.preprocess(
|
|
162
|
+
Typesafe.array(indexLike),
|
|
163
|
+
it => it?.constructor === Object ? Object.values(it) : it,
|
|
164
|
+
),
|
|
165
|
+
[],
|
|
166
|
+
)
|
|
155
167
|
|
|
156
168
|
const entitySchema = Typesafe.intersection(
|
|
157
169
|
Typesafe.object({
|
|
@@ -160,10 +172,10 @@ const entitySchema = Typesafe.intersection(
|
|
|
160
172
|
primaryColumn: Typesafe.string,
|
|
161
173
|
tableName: Typesafe.string,
|
|
162
174
|
fields: Typesafe.record(Typesafe.string, fieldSchema),
|
|
163
|
-
unique: Typesafe.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
unique: Typesafe.preprocess(
|
|
176
|
+
Typesafe.array(indexLike),
|
|
177
|
+
it => it?.constructor === Object ? Object.values(it) : it,
|
|
178
|
+
),
|
|
167
179
|
indexes: indexesSchema,
|
|
168
180
|
eventLog: eventLogSchema,
|
|
169
181
|
}),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export class Literal {
|
|
2
|
+
constructor(public readonly value: string) {
|
|
3
|
+
}
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type FormatterPath = ({ type: 'array' } | { type: 'object'; key: string })[]
|
|
7
|
+
export type IndentDecider = (value: any, path: FormatterPath) => boolean
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export const printJsValue = (value: any, shouldIndentCb: IndentDecider = () => false, path: FormatterPath = []): string => {
|
|
11
|
+
if (value instanceof Literal) {
|
|
12
|
+
return value.value
|
|
13
|
+
}
|
|
14
|
+
if (value === undefined || value === null || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'function') {
|
|
15
|
+
return String(value)
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === 'bigint') {
|
|
18
|
+
return value.toString(10) + 'n'
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'string') {
|
|
21
|
+
return `'${value.replaceAll(/'/g, '\\\'')}'`
|
|
22
|
+
}
|
|
23
|
+
const shouldIndent = shouldIndentCb(value, path)
|
|
24
|
+
if (!shouldIndent) {
|
|
25
|
+
shouldIndentCb = () => false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const indent = (inc: number) => '\t'.repeat(path.length + inc)
|
|
29
|
+
const nl = '\n'
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return ''
|
|
33
|
+
+ '['
|
|
34
|
+
+ value.map((it, index, arr) =>
|
|
35
|
+
(shouldIndent ? nl + indent(1) : '')
|
|
36
|
+
+ printJsValue(it, shouldIndentCb, [...path, { type: 'array' }])
|
|
37
|
+
+ (shouldIndent ? ',' : ((index + 1) < arr.length ? ', ' : '')),
|
|
38
|
+
).join('')
|
|
39
|
+
+ (shouldIndent ? nl + indent(0) : '')
|
|
40
|
+
+ ']'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ''
|
|
44
|
+
+ '{'
|
|
45
|
+
+ (shouldIndent ? '' : ' ')
|
|
46
|
+
+ Object.entries(value).map(([key, value], index, arr) => {
|
|
47
|
+
const formattedKey = isSimpleIdentifier(key) ? key : `[${printJsValue(key)}]`
|
|
48
|
+
return ''
|
|
49
|
+
+ (shouldIndent ? nl + indent(1) : '')
|
|
50
|
+
+ formattedKey
|
|
51
|
+
+ ': '
|
|
52
|
+
+ printJsValue(value, shouldIndentCb, [...path, { type: 'object', key }])
|
|
53
|
+
+ (shouldIndent ? ',' : ((index + 1) < arr.length ? ', ' : ''))
|
|
54
|
+
}).join('')
|
|
55
|
+
+ (shouldIndent ? nl + indent(0) : ' ')
|
|
56
|
+
+ '}'
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isSimpleIdentifier = (identifier: string): boolean => {
|
|
61
|
+
return !!identifier.match(/^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/)
|
|
62
|
+
}
|