@contember/schema-utils 1.3.0-alpha.5 → 1.3.0-alpha.7

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.
Files changed (61) hide show
  1. package/dist/src/definition-generator/AclDefinitionCodeGenerator.d.ts +19 -0
  2. package/dist/src/definition-generator/AclDefinitionCodeGenerator.d.ts.map +1 -0
  3. package/dist/src/definition-generator/AclDefinitionCodeGenerator.js +116 -0
  4. package/dist/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -0
  5. package/dist/src/definition-generator/DefinitionCodeGenerator.d.ts +27 -0
  6. package/dist/src/definition-generator/DefinitionCodeGenerator.d.ts.map +1 -0
  7. package/dist/src/definition-generator/DefinitionCodeGenerator.js +224 -0
  8. package/dist/src/definition-generator/DefinitionCodeGenerator.js.map +1 -0
  9. package/dist/src/definition-generator/DefinitionNamingConventions.d.ts +7 -0
  10. package/dist/src/definition-generator/DefinitionNamingConventions.d.ts.map +1 -0
  11. package/dist/src/definition-generator/DefinitionNamingConventions.js +18 -0
  12. package/dist/src/definition-generator/DefinitionNamingConventions.js.map +1 -0
  13. package/dist/src/definition-generator/TsDefinitionGenerator.d.ts +4 -18
  14. package/dist/src/definition-generator/TsDefinitionGenerator.d.ts.map +1 -1
  15. package/dist/src/definition-generator/TsDefinitionGenerator.js +6 -232
  16. package/dist/src/definition-generator/TsDefinitionGenerator.js.map +1 -1
  17. package/dist/src/definition-generator/index.d.ts +3 -0
  18. package/dist/src/definition-generator/index.d.ts.map +1 -1
  19. package/dist/src/definition-generator/index.js +3 -0
  20. package/dist/src/definition-generator/index.js.map +1 -1
  21. package/dist/src/tsconfig.tsbuildinfo +1 -1
  22. package/dist/src/utils/printJsValue.d.ts +13 -0
  23. package/dist/src/utils/printJsValue.d.ts.map +1 -0
  24. package/dist/src/utils/printJsValue.js +57 -0
  25. package/dist/src/utils/printJsValue.js.map +1 -0
  26. package/dist/src/validation/ModelValidator.js +2 -2
  27. package/dist/src/validation/ModelValidator.js.map +1 -1
  28. package/dist/tests/cases/unit/printJsvalue.test.d.ts +2 -0
  29. package/dist/tests/cases/unit/printJsvalue.test.d.ts.map +1 -0
  30. package/dist/tests/cases/unit/printJsvalue.test.js +90 -0
  31. package/dist/tests/cases/unit/printJsvalue.test.js.map +1 -0
  32. package/dist/tests/cases/unit/schemas/acl.d.ts +13 -0
  33. package/dist/tests/cases/unit/schemas/acl.d.ts.map +1 -0
  34. package/dist/tests/cases/unit/schemas/acl.js +47 -0
  35. package/dist/tests/cases/unit/schemas/acl.js.map +1 -0
  36. package/dist/tests/cases/unit/schemas/basic.d.ts.map +1 -1
  37. package/dist/tests/cases/unit/schemas/basic.js.map +1 -1
  38. package/dist/tests/cases/unit/schemas/enum.d.ts.map +1 -1
  39. package/dist/tests/cases/unit/schemas/enum.js.map +1 -1
  40. package/dist/tests/cases/unit/schemas/relations.d.ts.map +1 -1
  41. package/dist/tests/cases/unit/schemas/relations.js.map +1 -1
  42. package/dist/tests/cases/unit/schemas/unique.d.ts.map +1 -1
  43. package/dist/tests/cases/unit/schemas/unique.js.map +1 -1
  44. package/dist/tests/cases/unit/tsDefinitionGenerator.test.js +5 -3
  45. package/dist/tests/cases/unit/tsDefinitionGenerator.test.js.map +1 -1
  46. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +4 -4
  48. package/src/definition-generator/AclDefinitionCodeGenerator.ts +126 -0
  49. package/src/definition-generator/DefinitionCodeGenerator.ts +259 -0
  50. package/src/definition-generator/DefinitionNamingConventions.ts +16 -0
  51. package/src/definition-generator/TsDefinitionGenerator.ts +8 -264
  52. package/src/definition-generator/index.ts +3 -0
  53. package/src/utils/printJsValue.ts +62 -0
  54. package/src/validation/ModelValidator.ts +9 -9
  55. package/tests/cases/unit/printJsvalue.test.ts +100 -0
  56. package/tests/cases/unit/schemas/acl.ts +34 -0
  57. package/tests/cases/unit/schemas/basic.ts +1 -1
  58. package/tests/cases/unit/schemas/enum.ts +1 -1
  59. package/tests/cases/unit/schemas/relations.ts +1 -1
  60. package/tests/cases/unit/schemas/unique.ts +1 -1
  61. package/tests/cases/unit/tsDefinitionGenerator.test.ts +6 -4
@@ -0,0 +1,259 @@
1
+ import { Acl, Model, Schema, Writable } from '@contember/schema'
2
+ import {
3
+ acceptFieldVisitor,
4
+ DefaultNamingConventions,
5
+ isInverseRelation,
6
+ NamingConventions,
7
+ NamingHelper,
8
+ resolveDefaultColumnType,
9
+ } from '../model'
10
+ import { printJsValue } from '../utils/printJsValue'
11
+ import { DefinitionNamingConventions } from './DefinitionNamingConventions'
12
+ import { AclDefinitionCodeGenerator } from './AclDefinitionCodeGenerator'
13
+
14
+
15
+ export class DefinitionCodeGenerator {
16
+ constructor(
17
+ private readonly schemaNamingConventions: NamingConventions = new DefaultNamingConventions(),
18
+ private readonly definitionNamingConventions = new DefinitionNamingConventions(),
19
+ private readonly aclGenerator = new AclDefinitionCodeGenerator(),
20
+ ) {
21
+ }
22
+
23
+ public generate(schema: Schema) {
24
+ const roles = this.aclGenerator.generateRoles({ acl: schema.acl })
25
+ const aclVariables = this.aclGenerator.generateAclVariables({ acl: schema.acl })
26
+
27
+ const enums = Object.entries(schema.model.enums).map(([name, values]) => this.generateEnum({
28
+ name,
29
+ values,
30
+ })).join('')
31
+
32
+ const entities = Object.values(schema.model.entities).map(entity => this.generateEntity({ entity, schema })).join('')
33
+
34
+ return `import { SchemaDefinition as def, AclDefinition as acl } from '@contember/schema-definition'
35
+ ${roles}${aclVariables}${enums}${entities}`
36
+ }
37
+
38
+
39
+ private generateEnum({ name, values }: { name: string; values: readonly string[] }): string {
40
+ return `\nexport const ${this.formatIdentifier(name)} = def.createEnum(${values.map(it => printJsValue(it)).join(', ')})\n`
41
+ }
42
+
43
+ public generateEntity({ entity, schema }: { entity: Model.Entity; schema: Schema }): string {
44
+ const decorators = [
45
+ ...Object.values(entity.unique).map(constraint => this.generateUniqueConstraint({ entity, constraint })),
46
+ ...Object.values(entity.indexes).map(index => this.generateIndex({ entity, index })),
47
+ this.generateView({ entity }),
48
+ ].filter(it => !!it).map(it => `${it}\n`).join('')
49
+ const acl = this.aclGenerator.generateEntityAcl({ entity, schema })
50
+
51
+ return `\n${decorators}${acl}export class ${this.formatIdentifier(entity.name)} {
52
+ ${Object.values(entity.fields).map(field => this.generateField({ field, entity, schema })).filter(it => !!it).join('\n')}
53
+ }\n`
54
+ }
55
+
56
+ private generateUniqueConstraint({ entity, constraint }: {
57
+ entity: Model.Entity
58
+ constraint: Model.UniqueConstraint
59
+ }): string {
60
+ const defaultName = NamingHelper.createUniqueConstraintName(entity.name, constraint.fields)
61
+ if (defaultName === constraint.name) {
62
+ const fieldsList = `${constraint.fields.map(it => printJsValue(it)).join(', ')}`
63
+ return `@def.Unique(${fieldsList})`
64
+ }
65
+ return `@def.Unique(${printJsValue(constraint)})`
66
+ }
67
+
68
+ private generateIndex({ entity, index }: { entity: Model.Entity; index: Model.Index }): string {
69
+ const defaultName = NamingHelper.createIndexName(entity.name, index.fields)
70
+ if (defaultName === index.name) {
71
+ const fieldsList = `${index.fields.map(it => printJsValue(it)).join(', ')}`
72
+ return `@def.Index(${fieldsList})`
73
+ }
74
+ return `@def.Index(${printJsValue(index)})`
75
+ }
76
+
77
+
78
+ private generateView({ entity }: { entity: Model.Entity }): string | undefined {
79
+ if (!entity.view) {
80
+ return undefined
81
+ }
82
+
83
+ const dependenciesExpr = (entity.view.dependencies?.length ?? 0) > 0
84
+ ? `, {\n\tdependencies: () => [${entity.view.dependencies?.map(it => this.formatIdentifier(it))}]\n}`
85
+ : ''
86
+
87
+ return `@def.View(\`${entity.view.sql}\`${dependenciesExpr})`
88
+ }
89
+
90
+ public generateField({ entity, field, schema }: { entity: Model.Entity; field: Model.AnyField; schema: Schema }): string | undefined {
91
+ const formatRelationFactory = (method: string, relation: Model.AnyRelation) => {
92
+ const otherSide = isInverseRelation(relation) ? relation.ownedBy : relation.inversedBy
93
+ const otherSideFormatted = otherSide ? `, ${printJsValue(otherSide)}` : ''
94
+ return `${method}(${this.formatIdentifier(relation.target)}${otherSideFormatted})`
95
+ }
96
+ const formatEnumRef = (enumName: string, enumValues: Record<string, string>, providedValue?: string, defaultValue?: string): string | undefined => {
97
+ if (!providedValue || providedValue === defaultValue) {
98
+ return undefined
99
+ }
100
+ const enumValueKey = Object.entries(Model.OrderDirection).find(dir => dir[1] === providedValue)?.[0]
101
+ if (!enumValueKey) {
102
+ throw new Error(`Value ${providedValue} is not defined in enum ${enumName}`)
103
+ }
104
+ return `${enumName}.${enumValueKey}`
105
+
106
+ }
107
+ const formatOrderBy = (orderBy?: readonly Model.OrderBy[]) => {
108
+ return orderBy?.map(it => {
109
+ const enumExpr = formatEnumRef(`Model.OrderDirection`, Model.OrderDirection, it.direction, Model.OrderDirection.asc)
110
+ return `orderBy(${printJsValue(it.path)}${enumExpr ? `, ${enumExpr}` : ''})`
111
+ }) ?? []
112
+ }
113
+ const formatOnDelete = (onDelete?: Model.OnDelete): string | undefined => {
114
+ if (onDelete === Model.OnDelete.cascade) {
115
+ return 'cascadeOnDelete()'
116
+ }
117
+ if (onDelete === Model.OnDelete.setNull) {
118
+ return 'setNullOnDelete()'
119
+ }
120
+ return undefined
121
+ }
122
+ const formatJoiningColumn = (joiningColumnName: string, fieldName: string): string | undefined => {
123
+ const defaultJoiningColumn = this.schemaNamingConventions.getJoiningColumnName(fieldName)
124
+ if (defaultJoiningColumn === joiningColumnName) {
125
+ return undefined
126
+ }
127
+ return `joiningColumn(${printJsValue(joiningColumnName)})`
128
+ }
129
+ const definition = acceptFieldVisitor<(string | undefined)[]>(schema.model, entity, field, {
130
+ visitColumn: ctx => {
131
+ return this.generateColumn(ctx)
132
+ },
133
+ visitOneHasMany: ({ relation }) => {
134
+ return [
135
+ formatRelationFactory('oneHasMany', relation),
136
+ ...formatOrderBy(relation.orderBy),
137
+ ]
138
+ },
139
+ visitManyHasOne: ({ relation }) => {
140
+ return [
141
+ formatRelationFactory('manyHasOne', relation),
142
+ !relation.nullable ? 'notNull()' : undefined,
143
+ formatOnDelete(relation.joiningColumn.onDelete),
144
+ formatJoiningColumn(relation.joiningColumn.columnName, relation.name),
145
+ ]
146
+ },
147
+ visitOneHasOneInverse: ({ relation }) => {
148
+ return [
149
+ formatRelationFactory('oneHasOneInverse', relation),
150
+ !relation.nullable ? 'notNull()' : undefined,
151
+ ]
152
+ },
153
+ visitOneHasOneOwning: ({ relation }) => {
154
+ return [
155
+ formatRelationFactory('oneHasOne', relation),
156
+ !relation.nullable ? 'notNull()' : undefined,
157
+ formatOnDelete(relation.joiningColumn.onDelete),
158
+ formatJoiningColumn(relation.joiningColumn.columnName, relation.name),
159
+ relation.orphanRemoval ? 'removeOrphan()' : undefined,
160
+ ]
161
+ },
162
+ visitManyHasManyOwning: ({ entity, relation }) => {
163
+ const columnNames = this.schemaNamingConventions.getJoiningTableColumnNames(
164
+ entity.name,
165
+ relation.name,
166
+ relation.target,
167
+ relation.inversedBy,
168
+ )
169
+ const defaultJoiningTable = this.schemaNamingConventions.getJoiningTableName(entity.name, relation.name)
170
+ const joiningTable: Writable<Partial<Model.JoiningTable>> = {}
171
+ if (relation.joiningTable.tableName !== defaultJoiningTable) {
172
+ joiningTable.tableName = relation.joiningTable.tableName
173
+ }
174
+ if (!relation.joiningTable.eventLog.enabled) {
175
+ joiningTable.eventLog = { enabled: false }
176
+ }
177
+ if (columnNames[0] !== relation.joiningTable.joiningColumn.columnName || relation.joiningTable.joiningColumn.onDelete !== Model.OnDelete.cascade) {
178
+ joiningTable.joiningColumn = {
179
+ columnName: relation.joiningTable.joiningColumn.columnName,
180
+ onDelete: relation.joiningTable.joiningColumn.onDelete,
181
+ }
182
+ }
183
+ if (columnNames[1] !== relation.joiningTable.inverseJoiningColumn.columnName || relation.joiningTable.inverseJoiningColumn.onDelete !== Model.OnDelete.cascade) {
184
+ joiningTable.inverseJoiningColumn = {
185
+ columnName: relation.joiningTable.inverseJoiningColumn.columnName,
186
+ onDelete: relation.joiningTable.inverseJoiningColumn.onDelete,
187
+ }
188
+ }
189
+
190
+ return [
191
+ formatRelationFactory('manyHasMany', relation),
192
+ ...formatOrderBy(relation.orderBy),
193
+ Object.keys(joiningTable).length > 0 ? `joiningTable(${printJsValue(joiningTable)})` : undefined,
194
+ ]
195
+ },
196
+ visitManyHasManyInverse: ({ relation }) => {
197
+ return [
198
+ formatRelationFactory('manyHasManyInverse', relation),
199
+ ...formatOrderBy(relation.orderBy),
200
+ ]
201
+ },
202
+ })
203
+ const definitionCode = definition.filter(it => !!it).join('.')
204
+ if (field.name === 'id' && definitionCode === 'uuidColumn().notNull()') {
205
+ return undefined
206
+ }
207
+ return `\t${this.formatIdentifier(field.name)} = def.${definitionCode}`
208
+ }
209
+
210
+ private generateColumn({ entity, column }: { entity: Model.Entity; column: Model.AnyColumn }): string[] {
211
+ let parts: string[] = []
212
+ if (column.type === Model.ColumnType.Enum) {
213
+ parts.push(`enumColumn(${column.columnType})`)
214
+ } else {
215
+ parts.push(`${ColumnToMethodMapping[column.type]}()`)
216
+ const defaultColumnType = resolveDefaultColumnType(column.type)
217
+ if (defaultColumnType !== column.columnType) {
218
+ parts.push(`columnType(${printJsValue(column.columnType)})`)
219
+ }
220
+ }
221
+ const defaultColumnName = this.schemaNamingConventions.getColumnName(column.name)
222
+ if (defaultColumnName !== column.columnName) {
223
+ parts.push(`columnName(${printJsValue(column.columnName)})`)
224
+ }
225
+ if (!column.nullable) {
226
+ parts.push('notNull()')
227
+ }
228
+ if (column.default !== undefined) {
229
+ parts.push(`default(${printJsValue(column.default)})`)
230
+ }
231
+ if (column.typeAlias) {
232
+ parts.push(`typeAlias(${printJsValue(column.typeAlias)})`)
233
+ }
234
+
235
+
236
+ // todo: sequence
237
+ // todo: maybe single column unique()
238
+
239
+ return parts
240
+ }
241
+
242
+ private formatIdentifier(id: string): string {
243
+ return this.definitionNamingConventions.formatIdentifier(id)
244
+ }
245
+ }
246
+
247
+
248
+ const ColumnToMethodMapping: {
249
+ [K in Exclude<Model.ColumnType, Model.ColumnType.Enum>]: string
250
+ } = {
251
+ [Model.ColumnType.Bool]: 'boolColumn',
252
+ [Model.ColumnType.Date]: 'dateColumn',
253
+ [Model.ColumnType.DateTime]: 'dateTimeColumn',
254
+ [Model.ColumnType.Json]: 'jsonColumn',
255
+ [Model.ColumnType.Double]: 'doubleColumn',
256
+ [Model.ColumnType.Uuid]: 'uuidColumn',
257
+ [Model.ColumnType.Int]: 'intColumn',
258
+ [Model.ColumnType.String]: 'stringColumn',
259
+ }
@@ -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 { Model, Schema, Writable } from '@contember/schema'
2
- import {
3
- acceptFieldVisitor,
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 enums = Object.entries(this.schema.model.enums).map(([name, values]) => this.generateEnum({
22
- name,
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
  }
@@ -1 +1,4 @@
1
1
  export * from './TsDefinitionGenerator'
2
+ export * from './AclDefinitionCodeGenerator'
3
+ export * from './DefinitionCodeGenerator'
4
+ export * from './DefinitionNamingConventions'
@@ -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
+ }
@@ -7,7 +7,7 @@ const RESERVED_WORDS = ['and', 'or', 'not']
7
7
 
8
8
 
9
9
  export class ModelValidator {
10
- constructor(private readonly model: Model.Schema) {}
10
+ constructor(private readonly model: Model.Schema) { }
11
11
 
12
12
  public validate(): ValidationError[] {
13
13
  const errorBuilder = new ErrorBuilder([], [])
@@ -79,7 +79,7 @@ export class ModelValidator {
79
79
  }
80
80
  }
81
81
 
82
- private validateRelation(partialEntity: Model.Entity, field: Model.AnyRelation, errors: ErrorBuilder): void {
82
+ private validateRelation(partialEntity: Model.Entity, field: Model.AnyRelation, errors: ErrorBuilder): void {
83
83
  const entityName = partialEntity.name
84
84
  const targetEntityName = field.target
85
85
  const targetEntity = this.model.entities[targetEntityName] || undefined
@@ -148,7 +148,7 @@ export class ModelValidator {
148
148
  return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} inverse relation is not set`)
149
149
  }
150
150
  if (targetField.inversedBy !== field.name) {
151
- return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.inversedBy} given`)
151
+ return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} expected, ${targetField.target}::${targetField.inversedBy} given`)
152
152
  }
153
153
  if (field.type === Model.RelationType.OneHasOne && targetField.type !== Model.RelationType.OneHasOne) {
154
154
  return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
@@ -185,7 +185,7 @@ export class ModelValidator {
185
185
  return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} owning relation is not set`)
186
186
  }
187
187
  if (targetField.ownedBy !== field.name) {
188
- return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.ownedBy} given`)
188
+ return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} expected, ${targetField.target}::${targetField.ownedBy} given`)
189
189
  }
190
190
  if (field.type === Model.RelationType.OneHasOne && targetField.type !== Model.RelationType.OneHasOne) {
191
191
  return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
@@ -259,11 +259,11 @@ export class ModelValidator {
259
259
  }
260
260
  aliasedTypes.set(column.typeAlias, column.type)
261
261
  },
262
- visitManyHasManyInverse: () => {},
263
- visitOneHasMany: () => {},
264
- visitOneHasOneInverse: () => {},
265
- visitOneHasOneOwning: () => {},
266
- visitManyHasOne: () => {},
262
+ visitManyHasManyInverse: () => { },
263
+ visitOneHasMany: () => { },
264
+ visitOneHasOneInverse: () => { },
265
+ visitOneHasOneOwning: () => { },
266
+ visitManyHasOne: () => { },
267
267
  })
268
268
  }
269
269
  for (const entity of entities) {