@contember/schema-utils 1.2.0-beta.1 → 1.2.0-beta.3

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 (83) hide show
  1. package/dist/src/definition-generator/TsDefinitionGenerator.d.ts +26 -0
  2. package/dist/src/definition-generator/TsDefinitionGenerator.d.ts.map +1 -0
  3. package/dist/src/definition-generator/TsDefinitionGenerator.js +246 -0
  4. package/dist/src/definition-generator/TsDefinitionGenerator.js.map +1 -0
  5. package/dist/src/definition-generator/index.d.ts +2 -0
  6. package/dist/src/definition-generator/index.d.ts.map +1 -0
  7. package/dist/src/definition-generator/index.js +18 -0
  8. package/dist/src/definition-generator/index.js.map +1 -0
  9. package/dist/src/index.d.ts +2 -0
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +2 -0
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/lax/LaxSchemaConverter.d.ts +10 -0
  14. package/dist/src/lax/LaxSchemaConverter.d.ts.map +1 -0
  15. package/dist/src/lax/LaxSchemaConverter.js +204 -0
  16. package/dist/src/lax/LaxSchemaConverter.js.map +1 -0
  17. package/dist/src/lax/index.d.ts +3 -0
  18. package/dist/src/lax/index.d.ts.map +1 -0
  19. package/dist/src/lax/index.js +21 -0
  20. package/dist/src/lax/index.js.map +1 -0
  21. package/dist/src/lax/schema.d.ts +285 -0
  22. package/dist/src/lax/schema.d.ts.map +1 -0
  23. package/dist/src/lax/schema.js +67 -0
  24. package/dist/src/lax/schema.js.map +1 -0
  25. package/dist/src/model/NamingConventions.d.ts +17 -0
  26. package/dist/src/model/NamingConventions.d.ts.map +1 -0
  27. package/dist/src/model/NamingConventions.js +29 -0
  28. package/dist/src/model/NamingConventions.js.map +1 -0
  29. package/dist/src/model/index.d.ts +2 -0
  30. package/dist/src/model/index.d.ts.map +1 -1
  31. package/dist/src/model/index.js +2 -0
  32. package/dist/src/model/index.js.map +1 -1
  33. package/dist/src/model/resolveDefaultColumnType.d.ts +3 -0
  34. package/dist/src/model/resolveDefaultColumnType.d.ts.map +1 -0
  35. package/dist/src/model/resolveDefaultColumnType.js +29 -0
  36. package/dist/src/model/resolveDefaultColumnType.js.map +1 -0
  37. package/dist/src/tsconfig.tsbuildinfo +1 -1
  38. package/dist/src/type-schema/model.js +1 -1
  39. package/dist/src/type-schema/model.js.map +1 -1
  40. package/dist/tests/cases/unit/laxSchemaConverter.test.d.ts +2 -0
  41. package/dist/tests/cases/unit/laxSchemaConverter.test.d.ts.map +1 -0
  42. package/dist/tests/cases/unit/laxSchemaConverter.test.js +118 -0
  43. package/dist/tests/cases/unit/laxSchemaConverter.test.js.map +1 -0
  44. package/dist/tests/cases/unit/schemas/basic.d.ts +5 -0
  45. package/dist/tests/cases/unit/schemas/basic.d.ts.map +1 -0
  46. package/dist/tests/cases/unit/schemas/basic.js +11 -0
  47. package/dist/tests/cases/unit/schemas/basic.js.map +1 -0
  48. package/dist/tests/cases/unit/schemas/enum.d.ts +7 -0
  49. package/dist/tests/cases/unit/schemas/enum.d.ts.map +1 -0
  50. package/dist/tests/cases/unit/schemas/enum.js +13 -0
  51. package/dist/tests/cases/unit/schemas/enum.js.map +1 -0
  52. package/dist/tests/cases/unit/schemas/relations.d.ts +15 -0
  53. package/dist/tests/cases/unit/schemas/relations.d.ts.map +1 -0
  54. package/dist/tests/cases/unit/schemas/relations.js +27 -0
  55. package/dist/tests/cases/unit/schemas/relations.js.map +1 -0
  56. package/dist/tests/cases/unit/schemas/unique.d.ts +5 -0
  57. package/dist/tests/cases/unit/schemas/unique.d.ts.map +1 -0
  58. package/dist/tests/cases/unit/schemas/unique.js +20 -0
  59. package/dist/tests/cases/unit/schemas/unique.js.map +1 -0
  60. package/dist/tests/cases/unit/tsDefinitionGenerator.test.d.ts +2 -0
  61. package/dist/tests/cases/unit/tsDefinitionGenerator.test.d.ts.map +1 -0
  62. package/dist/tests/cases/unit/tsDefinitionGenerator.test.js +56 -0
  63. package/dist/tests/cases/unit/tsDefinitionGenerator.test.js.map +1 -0
  64. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +3 -3
  66. package/src/definition-generator/TsDefinitionGenerator.ts +275 -0
  67. package/src/definition-generator/index.ts +1 -0
  68. package/src/index.ts +2 -0
  69. package/src/lax/LaxSchemaConverter.ts +213 -0
  70. package/src/lax/index.ts +2 -0
  71. package/src/lax/schema.ts +62 -0
  72. package/src/model/NamingConventions.ts +53 -0
  73. package/src/model/index.ts +2 -0
  74. package/src/model/resolveDefaultColumnType.ts +25 -0
  75. package/src/type-schema/model.ts +1 -1
  76. package/tests/cases/unit/__snapshots__/laxSchemaConverter.test.ts.snap +310 -0
  77. package/tests/cases/unit/laxSchemaConverter.test.ts +137 -0
  78. package/tests/cases/unit/schemas/.eslintignore +1 -0
  79. package/tests/cases/unit/schemas/basic.ts +5 -0
  80. package/tests/cases/unit/schemas/enum.ts +8 -0
  81. package/tests/cases/unit/schemas/relations.ts +17 -0
  82. package/tests/cases/unit/schemas/unique.ts +6 -0
  83. package/tests/cases/unit/tsDefinitionGenerator.test.ts +31 -0
@@ -0,0 +1,275 @@
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'
10
+
11
+ 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
+ constructor(
15
+ private readonly schema: Schema,
16
+ private readonly conventions: NamingConventions = new DefaultNamingConventions(),
17
+ ) {
18
+ }
19
+
20
+ 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`)
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)})`
54
+ }
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
+ }
@@ -0,0 +1 @@
1
+ export * from './TsDefinitionGenerator'
package/src/index.ts CHANGED
@@ -3,6 +3,8 @@ import { emptyModelSchema } from './model'
3
3
  import * as Typesafe from '@contember/typesafe'
4
4
  import { aclSchema, modelSchema, validationSchema, settingsSchema } from './type-schema'
5
5
 
6
+ export * from './definition-generator'
7
+ export * from './lax'
6
8
  export * from './model'
7
9
  export * from './acl'
8
10
  export * from './dataUtils'
@@ -0,0 +1,213 @@
1
+ import { Model, Schema, Writable } from '@contember/schema'
2
+ import { LaxSchema, LaxSchemaColumn, LaxSchemaEnum, LaxSchemaField, LaxSchemaRelation } from './schema'
3
+ import { DefaultNamingConventions, NamingConventions, resolveDefaultColumnType } from '../model'
4
+
5
+ const simpleColumnTypes: { [K in LaxSchemaColumn['type']]: { type: Model.ColumnType; columnType: string } } = {
6
+ boolean: { type: Model.ColumnType.Bool, columnType: 'boolean' },
7
+ int: { type: Model.ColumnType.Int, columnType: 'integer' },
8
+ date: { type: Model.ColumnType.Date, columnType: 'date' },
9
+ datetime: { type: Model.ColumnType.DateTime, columnType: 'timestamptz' },
10
+ float: { type: Model.ColumnType.Double, columnType: 'double precision' },
11
+ json: { type: Model.ColumnType.Json, columnType: 'jsonb' },
12
+ string: { type: Model.ColumnType.String, columnType: 'text' },
13
+ }
14
+
15
+ const isColumnDefinition = (field: LaxSchemaField): field is LaxSchemaColumn => field.type in simpleColumnTypes
16
+
17
+ export class LaxSchemaConverter {
18
+ constructor(
19
+ private readonly schema: LaxSchema,
20
+ private readonly conventions: NamingConventions = new DefaultNamingConventions(),
21
+ ) {
22
+ }
23
+
24
+ public convert(): Schema {
25
+ const builder = new LaxSchemaBuilder(this.schema, this.conventions)
26
+ return builder.build()
27
+ }
28
+
29
+ }
30
+
31
+ class LaxSchemaBuilder {
32
+ private readonly entities: Record<string, Model.Entity> = {}
33
+ private readonly enums: Record<string, string[]> = {}
34
+
35
+ constructor(
36
+ private readonly schema: LaxSchema,
37
+ private readonly conventions: NamingConventions = new DefaultNamingConventions(),
38
+ ) {
39
+ }
40
+
41
+ public build(): Schema {
42
+ for (const [entityName, entityDefinition] of Object.entries(this.schema.entities)) {
43
+ for (const [fieldName, fieldDefinition] of Object.entries(entityDefinition.fields)) {
44
+ if (isColumnDefinition(fieldDefinition)) {
45
+ this.processColumn(entityName, fieldName, fieldDefinition)
46
+
47
+ } else if (fieldDefinition.type === 'enum') {
48
+ this.processEnum(entityName, fieldName, fieldDefinition)
49
+
50
+ } else {
51
+ this.processRelation(entityName, fieldName, fieldDefinition)
52
+ }
53
+ }
54
+ }
55
+ return {
56
+ acl: { roles: {} },
57
+ settings: {},
58
+ validation: {},
59
+ model: {
60
+ enums: this.enums,
61
+ entities: this.entities,
62
+ },
63
+ }
64
+ }
65
+
66
+ private processColumn(entityName: string, fieldName: string, columnDef: LaxSchemaColumn) {
67
+ const columnName = this.conventions.getColumnName(fieldName)
68
+ this.registerField(entityName, {
69
+ name: fieldName,
70
+ columnName: columnName,
71
+ type: simpleColumnTypes[columnDef.type].type,
72
+ nullable: !columnDef.notNull,
73
+ columnType: simpleColumnTypes[columnDef.type].columnType,
74
+ })
75
+ }
76
+
77
+ private processEnum(entityName: string, fieldName: string, enumDef: LaxSchemaEnum) {
78
+ this.enums[enumDef.enumName] = Array.from(new Set([...this.enums[enumDef.enumName] ?? [], ...enumDef.values]))
79
+ const columnName = this.conventions.getColumnName(fieldName)
80
+ this.registerField(entityName, {
81
+ name: fieldName,
82
+ columnName: columnName,
83
+ type: Model.ColumnType.Enum,
84
+ nullable: !enumDef.notNull,
85
+ columnType: enumDef.enumName,
86
+ })
87
+ }
88
+
89
+ private processRelation(entityName: string, fieldName: string, relDef: LaxSchemaRelation) {
90
+ switch (relDef.type) {
91
+ case 'manyHasMany':
92
+ const joiningColumns = this.conventions.getJoiningTableColumnNames(entityName, fieldName, relDef.targetEntity, relDef.targetField)
93
+ this.registerField(entityName, {
94
+ type: Model.RelationType.ManyHasMany,
95
+ name: fieldName,
96
+ target: relDef.targetEntity,
97
+ inversedBy: relDef.targetField,
98
+ joiningTable: {
99
+ tableName: this.conventions.getJoiningTableName(entityName, fieldName),
100
+ joiningColumn: {
101
+ columnName: joiningColumns[0],
102
+ onDelete: Model.OnDelete.cascade,
103
+ },
104
+ inverseJoiningColumn: {
105
+ columnName: joiningColumns[1],
106
+ onDelete: Model.OnDelete.cascade,
107
+ },
108
+ eventLog: { enabled: true },
109
+ },
110
+ })
111
+ if (!this.schema.entities[relDef.targetEntity].fields[relDef.targetField]) {
112
+ this.processRelation(relDef.targetEntity, relDef.targetField, {
113
+ type: 'manyHasManyInverse',
114
+ targetEntity: entityName,
115
+ targetField: fieldName,
116
+ })
117
+ }
118
+ break
119
+ case 'manyHasManyInverse':
120
+ this.registerField(entityName, {
121
+ type: Model.RelationType.ManyHasMany,
122
+ name: fieldName,
123
+ target: relDef.targetEntity,
124
+ ownedBy: relDef.targetField,
125
+ })
126
+ break
127
+ case 'manyHasOne':
128
+ this.registerField(entityName, {
129
+ type: Model.RelationType.ManyHasOne,
130
+ name: fieldName,
131
+ target: relDef.targetEntity,
132
+ inversedBy: relDef.targetField,
133
+ joiningColumn: {
134
+ columnName: this.conventions.getJoiningColumnName(fieldName),
135
+ onDelete: Model.OnDelete.restrict,
136
+ },
137
+ nullable: !relDef.notNull,
138
+ })
139
+ if (!this.schema.entities[relDef.targetEntity].fields[relDef.targetField]) {
140
+ this.processRelation(relDef.targetEntity, relDef.targetField, {
141
+ type: 'oneHasMany',
142
+ targetEntity: entityName,
143
+ targetField: fieldName,
144
+ })
145
+ }
146
+ break
147
+ case 'oneHasMany':
148
+ this.registerField(entityName, {
149
+ type: Model.RelationType.OneHasMany,
150
+ name: fieldName,
151
+ target: relDef.targetEntity,
152
+ ownedBy: relDef.targetField,
153
+ })
154
+ break
155
+ case 'oneHasOne':
156
+ this.registerField(entityName, {
157
+ type: Model.RelationType.OneHasOne,
158
+ name: fieldName,
159
+ target: relDef.targetEntity,
160
+ inversedBy: relDef.targetField,
161
+ joiningColumn: {
162
+ columnName: this.conventions.getJoiningColumnName(fieldName),
163
+ onDelete: Model.OnDelete.restrict,
164
+ },
165
+ nullable: !relDef.notNull,
166
+ })
167
+ if (!this.schema.entities[relDef.targetEntity].fields[relDef.targetField]) {
168
+ this.processRelation(relDef.targetEntity, relDef.targetField, {
169
+ type: 'oneHasOneInverse',
170
+ targetEntity: entityName,
171
+ targetField: fieldName,
172
+ })
173
+ }
174
+ break
175
+ case 'oneHasOneInverse':
176
+ this.registerField(entityName, {
177
+ type: Model.RelationType.OneHasOne,
178
+ name: fieldName,
179
+ target: relDef.targetEntity,
180
+ ownedBy: relDef.targetField,
181
+ nullable: !relDef.notNull,
182
+ })
183
+ break
184
+ }
185
+ }
186
+
187
+ private registerField(entityName: string, field: Model.AnyField) {
188
+ const entity = this.getEntity(entityName)
189
+ ;(entity.fields as Writable<Model.Entity['fields']>)[field.name] = field
190
+ }
191
+
192
+
193
+ private getEntity(entityName: string): Model.Entity {
194
+ return this.entities[entityName] ??= {
195
+ name: entityName,
196
+ unique: {},
197
+ indexes: {},
198
+ primary: 'id',
199
+ primaryColumn: 'id',
200
+ tableName: this.conventions.getTableName(entityName),
201
+ eventLog: { enabled: true },
202
+ fields: {
203
+ id: {
204
+ name: 'id',
205
+ columnName: 'id',
206
+ type: Model.ColumnType.Uuid,
207
+ columnType: resolveDefaultColumnType(Model.ColumnType.Uuid),
208
+ nullable: false,
209
+ },
210
+ },
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,2 @@
1
+ export * from './schema'
2
+ export { LaxSchemaConverter } from './LaxSchemaConverter'
@@ -0,0 +1,62 @@
1
+ import * as Typesafe from '@contember/typesafe'
2
+
3
+ const LaxSchemaEnumInner = Typesafe.intersection(
4
+ Typesafe.object({
5
+ values: Typesafe.array(Typesafe.string),
6
+ enumName: Typesafe.string,
7
+ }),
8
+ Typesafe.partial({
9
+ notNull: Typesafe.boolean,
10
+ unique: Typesafe.boolean,
11
+ }),
12
+ )
13
+
14
+ const LaxSchemaColumnInner = Typesafe.partial({
15
+ notNull: Typesafe.boolean,
16
+ unique: Typesafe.boolean,
17
+ })
18
+
19
+ const LaxSchemaRelationInner = Typesafe.intersection(
20
+ Typesafe.object({
21
+ targetEntity: Typesafe.string,
22
+ targetField: Typesafe.string,
23
+ }),
24
+ Typesafe.partial({
25
+ notNull: Typesafe.boolean,
26
+ }),
27
+ )
28
+
29
+ const LaxSchemaField = Typesafe.discriminatedUnion('type', {
30
+ oneHasOne: LaxSchemaRelationInner,
31
+ oneHasOneInverse: LaxSchemaRelationInner,
32
+ manyHasMany: LaxSchemaRelationInner,
33
+ manyHasManyInverse: LaxSchemaRelationInner,
34
+ oneHasMany: LaxSchemaRelationInner,
35
+ manyHasOne: LaxSchemaRelationInner,
36
+ int: LaxSchemaColumnInner,
37
+ float: LaxSchemaColumnInner,
38
+ boolean: LaxSchemaColumnInner,
39
+ string: LaxSchemaColumnInner,
40
+ json: LaxSchemaColumnInner,
41
+ date: LaxSchemaColumnInner,
42
+ datetime: LaxSchemaColumnInner,
43
+ enum: LaxSchemaEnumInner,
44
+ })
45
+
46
+ export type LaxSchemaField = ReturnType<typeof LaxSchemaField>
47
+ export type LaxSchemaEnum = Extract<LaxSchemaField, { type: 'enum' }>
48
+ export type LaxSchemaColumn = Extract<LaxSchemaField, { type: 'int' | 'float' | 'boolean' | 'string' | 'json' | 'datetime' | 'date' | 'uuid' }>
49
+ export type LaxSchemaRelation = Extract<LaxSchemaField, {type: 'oneHasOne' | 'oneHasOneInverse' | 'manyHasMany' | 'manyHasManyInverse' | 'oneHasMany' | 'manyHasOne' }>
50
+
51
+ const LaxSchemaEntity = Typesafe.object({
52
+ fields: Typesafe.record(Typesafe.string, LaxSchemaField),
53
+ })
54
+ export type LaxSchemaEntity = ReturnType<typeof LaxSchemaEntity>
55
+
56
+ export const LaxSchema = Typesafe.object({
57
+ entities: Typesafe.record(
58
+ Typesafe.string,
59
+ LaxSchemaEntity,
60
+ ),
61
+ })
62
+ export type LaxSchema = ReturnType<typeof LaxSchema>
@@ -0,0 +1,53 @@
1
+ export interface NamingConventions {
2
+ getPrimaryField(): string
3
+
4
+ getColumnName(fieldName: string): string
5
+
6
+ getTableName(entityName: string): string
7
+
8
+ getJoiningColumnName(relationName: string): string
9
+
10
+ getJoiningTableName(entity: string, relation: string): string
11
+
12
+ getJoiningTableColumnNames(
13
+ owningEntity: string,
14
+ owningRelation: string,
15
+ inverseEntity: string,
16
+ inverseRelation?: string,
17
+ ): [string, string]
18
+ }
19
+
20
+ const toUnderscore = (s: string) => s.replace(/([A-Z]+)/g, (x, y) => '_' + y.toLowerCase()).replace(/^_/, '')
21
+ export class DefaultNamingConventions implements NamingConventions {
22
+ getPrimaryField(): string {
23
+ return 'id'
24
+ }
25
+
26
+ getColumnName(fieldName: string): string {
27
+ return toUnderscore(fieldName)
28
+ }
29
+
30
+ getTableName(entityName: string): string {
31
+ return toUnderscore(entityName)
32
+ }
33
+
34
+ getJoiningColumnName(relationName: string): string {
35
+ return toUnderscore(relationName) + '_id'
36
+ }
37
+
38
+ getJoiningTableName(entity: string, relation: string): string {
39
+ return toUnderscore(entity) + '_' + toUnderscore(relation)
40
+ }
41
+
42
+ getJoiningTableColumnNames(
43
+ owningEntity: string,
44
+ owningRelation: string,
45
+ inverseEntity: string,
46
+ inverseRelation?: string,
47
+ ): [string, string] {
48
+ return [
49
+ toUnderscore(owningEntity) + '_id',
50
+ toUnderscore(inverseEntity === owningEntity ? owningRelation : inverseEntity) + '_id',
51
+ ]
52
+ }
53
+ }
@@ -1,2 +1,4 @@
1
1
  export * from './modelUtils'
2
2
  export * from './NamingHelper'
3
+ export * from './resolveDefaultColumnType'
4
+ export * from './NamingConventions'
@@ -0,0 +1,25 @@
1
+ import { Model } from '@contember/schema'
2
+ import { assertNever } from '../utils'
3
+
4
+ export const resolveDefaultColumnType = (type: Exclude<Model.ColumnType, Model.ColumnType.Enum>): string => {
5
+ switch (type) {
6
+ case Model.ColumnType.Int:
7
+ return 'integer'
8
+ case Model.ColumnType.Double:
9
+ return 'double precision'
10
+ case Model.ColumnType.String:
11
+ return 'text'
12
+ case Model.ColumnType.Uuid:
13
+ return 'uuid'
14
+ case Model.ColumnType.Bool:
15
+ return 'boolean'
16
+ case Model.ColumnType.DateTime:
17
+ return 'timestamptz'
18
+ case Model.ColumnType.Date:
19
+ return 'date'
20
+ case Model.ColumnType.Json:
21
+ return 'jsonb'
22
+ default:
23
+ return assertNever(type)
24
+ }
25
+ }
@@ -125,7 +125,7 @@ const columnSchema = Typesafe.intersection(
125
125
  )
126
126
  const columnSchemaCheck: Typesafe.Equals<Model.AnyColumn, ReturnType<typeof columnSchema>> = true
127
127
 
128
- const fieldSchema: Typesafe.Type<Model.AnyField> = Typesafe.discriminatedUnion(
128
+ const fieldSchema: Typesafe.Type<Model.AnyField> = Typesafe.partiallyDiscriminatedUnion(
129
129
  'type',
130
130
  oneHasOneInverseRelationSchema,
131
131
  oneHasOneOwningRelationSchema,