@contember/schema-utils 1.2.1 → 1.3.0-alpha.13

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 (140) hide show
  1. package/dist/src/SchemaDatabaseMetadata.d.ts +40 -0
  2. package/dist/src/SchemaDatabaseMetadata.d.ts.map +1 -0
  3. package/dist/src/SchemaDatabaseMetadata.js +20 -0
  4. package/dist/src/SchemaDatabaseMetadata.js.map +1 -0
  5. package/dist/src/acl/PredicateDefinitionProcessor.d.ts.map +1 -1
  6. package/dist/src/acl/PredicateDefinitionProcessor.js +49 -44
  7. package/dist/src/acl/PredicateDefinitionProcessor.js.map +1 -1
  8. package/dist/src/definition-generator/AclDefinitionCodeGenerator.d.ts +19 -0
  9. package/dist/src/definition-generator/AclDefinitionCodeGenerator.d.ts.map +1 -0
  10. package/dist/src/definition-generator/AclDefinitionCodeGenerator.js +116 -0
  11. package/dist/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -0
  12. package/dist/src/definition-generator/DefinitionCodeGenerator.d.ts +27 -0
  13. package/dist/src/definition-generator/DefinitionCodeGenerator.d.ts.map +1 -0
  14. package/dist/src/definition-generator/DefinitionCodeGenerator.js +216 -0
  15. package/dist/src/definition-generator/DefinitionCodeGenerator.js.map +1 -0
  16. package/dist/src/definition-generator/DefinitionNamingConventions.d.ts +7 -0
  17. package/dist/src/definition-generator/DefinitionNamingConventions.d.ts.map +1 -0
  18. package/dist/src/definition-generator/DefinitionNamingConventions.js +18 -0
  19. package/dist/src/definition-generator/DefinitionNamingConventions.js.map +1 -0
  20. package/dist/src/definition-generator/TsDefinitionGenerator.d.ts +4 -18
  21. package/dist/src/definition-generator/TsDefinitionGenerator.d.ts.map +1 -1
  22. package/dist/src/definition-generator/TsDefinitionGenerator.js +6 -232
  23. package/dist/src/definition-generator/TsDefinitionGenerator.js.map +1 -1
  24. package/dist/src/definition-generator/index.d.ts +3 -0
  25. package/dist/src/definition-generator/index.d.ts.map +1 -1
  26. package/dist/src/definition-generator/index.js +3 -0
  27. package/dist/src/definition-generator/index.js.map +1 -1
  28. package/dist/src/index.d.ts +2 -1
  29. package/dist/src/index.d.ts.map +1 -1
  30. package/dist/src/index.js +6 -1
  31. package/dist/src/index.js.map +1 -1
  32. package/dist/src/lax/LaxSchemaConverter.d.ts.map +1 -1
  33. package/dist/src/lax/LaxSchemaConverter.js +4 -5
  34. package/dist/src/lax/LaxSchemaConverter.js.map +1 -1
  35. package/dist/src/model/index.d.ts +0 -1
  36. package/dist/src/model/index.d.ts.map +1 -1
  37. package/dist/src/model/index.js +0 -1
  38. package/dist/src/model/index.js.map +1 -1
  39. package/dist/src/tsconfig.tsbuildinfo +1 -1
  40. package/dist/src/type-schema/actions.d.ts +31 -0
  41. package/dist/src/type-schema/actions.d.ts.map +1 -0
  42. package/dist/src/type-schema/actions.js +75 -0
  43. package/dist/src/type-schema/actions.js.map +1 -0
  44. package/dist/src/type-schema/model.d.ts +8 -6
  45. package/dist/src/type-schema/model.d.ts.map +1 -1
  46. package/dist/src/type-schema/model.js +6 -6
  47. package/dist/src/type-schema/model.js.map +1 -1
  48. package/dist/src/type-schema/where.d.ts +7 -0
  49. package/dist/src/type-schema/where.d.ts.map +1 -0
  50. package/dist/src/type-schema/where.js +50 -0
  51. package/dist/src/type-schema/where.js.map +1 -0
  52. package/dist/src/utils/deepCompare.d.ts +8 -2
  53. package/dist/src/utils/deepCompare.d.ts.map +1 -1
  54. package/dist/src/utils/deepCompare.js +32 -4
  55. package/dist/src/utils/deepCompare.js.map +1 -1
  56. package/dist/src/utils/printJsValue.d.ts +13 -0
  57. package/dist/src/utils/printJsValue.d.ts.map +1 -0
  58. package/dist/src/utils/printJsValue.js +57 -0
  59. package/dist/src/utils/printJsValue.js.map +1 -0
  60. package/dist/src/validation/ActionsValidator.d.ts +13 -0
  61. package/dist/src/validation/ActionsValidator.d.ts.map +1 -0
  62. package/dist/src/validation/ActionsValidator.js +134 -0
  63. package/dist/src/validation/ActionsValidator.js.map +1 -0
  64. package/dist/src/validation/ModelValidator.d.ts.map +1 -1
  65. package/dist/src/validation/ModelValidator.js +5 -31
  66. package/dist/src/validation/ModelValidator.js.map +1 -1
  67. package/dist/src/validation/SchemaValidator.d.ts.map +1 -1
  68. package/dist/src/validation/SchemaValidator.js +4 -1
  69. package/dist/src/validation/SchemaValidator.js.map +1 -1
  70. package/dist/src/validation/errors.d.ts +1 -1
  71. package/dist/src/validation/errors.d.ts.map +1 -1
  72. package/dist/src/validation/errors.js.map +1 -1
  73. package/dist/tests/cases/unit/deepCompare.test.d.ts +2 -0
  74. package/dist/tests/cases/unit/deepCompare.test.d.ts.map +1 -0
  75. package/dist/tests/cases/unit/deepCompare.test.js +43 -0
  76. package/dist/tests/cases/unit/deepCompare.test.js.map +1 -0
  77. package/dist/tests/cases/unit/modelValidator.test.js +6 -72
  78. package/dist/tests/cases/unit/modelValidator.test.js.map +1 -1
  79. package/dist/tests/cases/unit/printJsvalue.test.d.ts +2 -0
  80. package/dist/tests/cases/unit/printJsvalue.test.d.ts.map +1 -0
  81. package/dist/tests/cases/unit/printJsvalue.test.js +90 -0
  82. package/dist/tests/cases/unit/printJsvalue.test.js.map +1 -0
  83. package/dist/tests/cases/unit/schemas/acl.d.ts +13 -0
  84. package/dist/tests/cases/unit/schemas/acl.d.ts.map +1 -0
  85. package/dist/tests/cases/unit/schemas/acl.js +47 -0
  86. package/dist/tests/cases/unit/schemas/acl.js.map +1 -0
  87. package/dist/tests/cases/unit/schemas/basic.d.ts.map +1 -1
  88. package/dist/tests/cases/unit/schemas/basic.js.map +1 -1
  89. package/dist/tests/cases/unit/schemas/enum.d.ts.map +1 -1
  90. package/dist/tests/cases/unit/schemas/enum.js.map +1 -1
  91. package/dist/tests/cases/unit/schemas/relations.d.ts.map +1 -1
  92. package/dist/tests/cases/unit/schemas/relations.js.map +1 -1
  93. package/dist/tests/cases/unit/schemas/unique.d.ts.map +1 -1
  94. package/dist/tests/cases/unit/schemas/unique.js.map +1 -1
  95. package/dist/tests/cases/unit/tsDefinitionGenerator.test.js +5 -3
  96. package/dist/tests/cases/unit/tsDefinitionGenerator.test.js.map +1 -1
  97. package/dist/tests/cases/unit/whereSchema.test.d.ts +2 -0
  98. package/dist/tests/cases/unit/whereSchema.test.d.ts.map +1 -0
  99. package/dist/tests/cases/unit/whereSchema.test.js +85 -0
  100. package/dist/tests/cases/unit/whereSchema.test.js.map +1 -0
  101. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  102. package/package.json +9 -4
  103. package/src/SchemaDatabaseMetadata.ts +66 -0
  104. package/src/acl/PredicateDefinitionProcessor.ts +59 -60
  105. package/src/definition-generator/AclDefinitionCodeGenerator.ts +126 -0
  106. package/src/definition-generator/DefinitionCodeGenerator.ts +250 -0
  107. package/src/definition-generator/DefinitionNamingConventions.ts +16 -0
  108. package/src/definition-generator/TsDefinitionGenerator.ts +8 -264
  109. package/src/definition-generator/index.ts +3 -0
  110. package/src/index.ts +5 -1
  111. package/src/lax/LaxSchemaConverter.ts +4 -5
  112. package/src/model/index.ts +0 -1
  113. package/src/tsconfig.json +2 -6
  114. package/src/type-schema/actions.ts +79 -0
  115. package/src/type-schema/model.ts +21 -8
  116. package/src/type-schema/where.ts +28 -0
  117. package/src/utils/deepCompare.ts +37 -4
  118. package/src/utils/printJsValue.ts +62 -0
  119. package/src/validation/ActionsValidator.ts +119 -0
  120. package/src/validation/ModelValidator.ts +12 -36
  121. package/src/validation/SchemaValidator.ts +6 -1
  122. package/src/validation/errors.ts +7 -0
  123. package/tests/cases/unit/__snapshots__/laxSchemaConverter.test.ts.snap +30 -14
  124. package/tests/cases/unit/deepCompare.test.ts +44 -0
  125. package/tests/cases/unit/modelValidator.test.ts +6 -73
  126. package/tests/cases/unit/printJsvalue.test.ts +100 -0
  127. package/tests/cases/unit/schemas/acl.ts +34 -0
  128. package/tests/cases/unit/schemas/basic.ts +1 -1
  129. package/tests/cases/unit/schemas/enum.ts +1 -1
  130. package/tests/cases/unit/schemas/relations.ts +1 -1
  131. package/tests/cases/unit/schemas/unique.ts +1 -1
  132. package/tests/cases/unit/tsDefinitionGenerator.test.ts +6 -4
  133. package/tests/cases/unit/whereSchema.test.ts +89 -0
  134. package/tests/tsconfig.json +1 -3
  135. package/tsconfig.json +2 -6
  136. package/dist/src/model/NamingHelper.d.ts +0 -9
  137. package/dist/src/model/NamingHelper.d.ts.map +0 -1
  138. package/dist/src/model/NamingHelper.js +0 -46
  139. package/dist/src/model/NamingHelper.js.map +0 -1
  140. package/src/model/NamingHelper.ts +0 -42
@@ -0,0 +1,126 @@
1
+ import { PredicateDefinitionProcessor } from '../acl'
2
+ import { IndentDecider, Literal, printJsValue } from '../utils/printJsValue'
3
+ import { Acl, Model, Schema } from '@contember/schema'
4
+ import { DefinitionNamingConventions } from './DefinitionNamingConventions'
5
+
6
+
7
+ const indentFirstLevel: IndentDecider = (val, path) => path.length === 0
8
+
9
+ export class AclDefinitionCodeGenerator {
10
+
11
+ constructor(
12
+ private readonly definitionNamingConventions = new DefinitionNamingConventions(),
13
+ ) {
14
+ }
15
+
16
+ public generateRoles({ acl }: { acl: Acl.Schema }): string {
17
+ return Object.entries(acl.roles).map(([name, values]) => this.generateRole({
18
+ name,
19
+ values,
20
+ })).join('')
21
+ }
22
+
23
+ private generateRole({ name, values }: { name: string; values: Acl.RolePermissions }): string {
24
+ const { variables, entities, inherits, ...other } = values
25
+ const roleVarName = this.definitionNamingConventions.roleVarName(name)
26
+ return `\nexport const ${roleVarName} = acl.createRole(${printJsValue(name)}, ${printJsValue(other)})\n`
27
+ }
28
+
29
+ public generateAclVariables({ acl }: { acl: Acl.Schema }): string {
30
+ const variablesOutput: string[] = []
31
+
32
+ Object.entries(acl.roles).map(([roleName, roleValues]) => {
33
+ Object.entries(roleValues.variables).map(([variableName, variableValues]) => {
34
+ let variableDefinition = `\nexport const ${this.definitionNamingConventions.variableVarName(roleName, variableName)} = acl.`
35
+ const varFormatted = printJsValue(variableName)
36
+ const roleVarName = this.definitionNamingConventions.roleVarName(roleName)
37
+
38
+ if (variableValues.type === Acl.VariableType.predefined) {
39
+ const varDefinition = printJsValue(variableValues.value)
40
+ variableDefinition += `createPredefinedVariable(${varFormatted}, ${varDefinition}, ${roleVarName})\n`
41
+ } else if (variableValues.type === Acl.VariableType.entity) {
42
+ const varDefinition = printJsValue(variableValues.entityName)
43
+ variableDefinition += `createEntityVariable(${varFormatted}, ${varDefinition}, ${roleVarName})\n`
44
+ } else {
45
+ throw new Error(`Variable type ${variableValues.type} not yet supported`)
46
+ }
47
+
48
+ variablesOutput.push(variableDefinition)
49
+ })
50
+ })
51
+
52
+ return variablesOutput.join('')
53
+ }
54
+
55
+ public generateEntityAcl({ entity, schema }: { entity: Model.Entity; schema: Schema }): string {
56
+ const aclOutput: string[] = []
57
+ const numberOfEntityFieldsWithoutId = Object.keys(entity.fields).length - 1
58
+ for (const [roleName, roleDefinition] of Object.entries(schema.acl.roles)) {
59
+ const entityPermission = roleDefinition.entities[entity.name]
60
+ if (!entityPermission) {
61
+ continue
62
+ }
63
+ const roleVarName = this.definitionNamingConventions.roleVarName(roleName)
64
+ for (const [predicateName, predicateDefinition] of Object.entries(entityPermission.predicates)) {
65
+ const operations = this.getMatchingOperations({
66
+ predicate: predicateName,
67
+ operations: entityPermission.operations,
68
+ numberOfEntityFieldsWithoutId,
69
+ })
70
+ if (Object.keys(operations).length > 0) {
71
+ const processor = new PredicateDefinitionProcessor(schema.model)
72
+ const when = processor.process(entity, predicateDefinition, {
73
+ handleColumn: ctx => {
74
+ if (typeof ctx.value === 'string' && ctx.value in roleDefinition.variables) {
75
+ return new Literal(this.definitionNamingConventions.variableVarName(roleName, ctx.value))
76
+ }
77
+ return ctx.value
78
+ },
79
+ handleRelation: ctx => {
80
+ return ctx.value
81
+ },
82
+ })
83
+ const aclDefinition = printJsValue({ when, ...operations }, indentFirstLevel)
84
+ aclOutput.push(`@acl.allow(${roleVarName}, ${aclDefinition})\n`)
85
+ }
86
+ }
87
+ const trueOperations = this.getMatchingOperations({
88
+ predicate: true,
89
+ operations: entityPermission.operations,
90
+ numberOfEntityFieldsWithoutId,
91
+ })
92
+ if (Object.keys(trueOperations).length > 0) {
93
+ const aclDefinition = printJsValue({ ...trueOperations }, indentFirstLevel)
94
+ aclOutput.push(`@acl.allow(${roleVarName}, ${aclDefinition})\n`)
95
+ }
96
+ }
97
+
98
+ if (!aclOutput.length) {
99
+ return ''
100
+ }
101
+ return `\n${aclOutput.join('')}`
102
+ }
103
+
104
+ private getMatchingOperations({ operations, predicate, numberOfEntityFieldsWithoutId }: {
105
+ operations: Acl.EntityOperations
106
+ predicate: Acl.Predicate
107
+ numberOfEntityFieldsWithoutId: number
108
+ }): { read?: string[] | true; create?: string[] | boolean; update?: string[] | boolean; delete?: true } {
109
+ const result: ReturnType<AclDefinitionCodeGenerator['getMatchingOperations']> = {}
110
+ for (const op of ['read', 'create', 'update'] as const) {
111
+ const fields = Object.entries(operations[op] ?? {}).filter(([, it]) => it === predicate).map(([it]) => it)
112
+ if (fields.length === 0) {
113
+ continue
114
+ } else if (fields.length === numberOfEntityFieldsWithoutId) {
115
+ result[op] = true
116
+ } else {
117
+ result[op] = fields
118
+ }
119
+ }
120
+ if (operations.delete === predicate) {
121
+ result.delete = true
122
+ }
123
+
124
+ return result
125
+ }
126
+ }
@@ -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 { 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`)
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'