@contember/schema-utils 1.2.0 → 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
package/src/index.ts CHANGED
@@ -2,8 +2,10 @@ import { Schema } from '@contember/schema'
2
2
  import { emptyModelSchema } from './model'
3
3
  import * as Typesafe from '@contember/typesafe'
4
4
  import { aclSchema, modelSchema, validationSchema, settingsSchema } from './type-schema'
5
+ import { actionsSchema } from './type-schema/actions'
5
6
 
6
7
  export * from './definition-generator'
8
+ export * from './SchemaDatabaseMetadata'
7
9
  export * from './lax'
8
10
  export * from './model'
9
11
  export * from './acl'
@@ -11,12 +13,13 @@ export * from './dataUtils'
11
13
  export * from './validation'
12
14
  export * from './schemaNormalizer'
13
15
  export * from './schemaFilter'
14
- export { deepCompare } from './utils'
16
+ export { deepCompare, compareArraysIgnoreOrder } from './utils'
15
17
 
16
18
  export const emptySchema: Schema = {
17
19
  model: emptyModelSchema,
18
20
  acl: { roles: {} },
19
21
  validation: {},
22
+ actions: { triggers: {}, targets: {} },
20
23
  settings: {},
21
24
  }
22
25
 
@@ -24,5 +27,6 @@ export const schemaType: Typesafe.Type<Schema> = Typesafe.object({
24
27
  model: modelSchema,
25
28
  acl: aclSchema,
26
29
  validation: validationSchema,
30
+ actions: Typesafe.coalesce(actionsSchema, { triggers: {}, targets: {} }),
27
31
  settings: Typesafe.coalesce(settingsSchema, {}),
28
32
  })
@@ -1,6 +1,7 @@
1
1
  import { Model, Schema, Writable } from '@contember/schema'
2
2
  import { LaxSchema, LaxSchemaColumn, LaxSchemaEnum, LaxSchemaField, LaxSchemaRelation } from './schema'
3
3
  import { DefaultNamingConventions, NamingConventions, resolveDefaultColumnType } from '../model'
4
+ import { emptySchema } from '../index'
4
5
 
5
6
  const simpleColumnTypes: { [K in LaxSchemaColumn['type']]: { type: Model.ColumnType; columnType: string } } = {
6
7
  boolean: { type: Model.ColumnType.Bool, columnType: 'boolean' },
@@ -53,9 +54,7 @@ class LaxSchemaBuilder {
53
54
  }
54
55
  }
55
56
  return {
56
- acl: { roles: {} },
57
- settings: {},
58
- validation: {},
57
+ ...emptySchema,
59
58
  model: {
60
59
  enums: this.enums,
61
60
  entities: this.entities,
@@ -193,8 +192,8 @@ class LaxSchemaBuilder {
193
192
  private getEntity(entityName: string): Model.Entity {
194
193
  return this.entities[entityName] ??= {
195
194
  name: entityName,
196
- unique: {},
197
- indexes: {},
195
+ unique: [],
196
+ indexes: [],
198
197
  primary: 'id',
199
198
  primaryColumn: 'id',
200
199
  tableName: this.conventions.getTableName(entityName),
@@ -1,4 +1,3 @@
1
1
  export * from './modelUtils'
2
- export * from './NamingHelper'
3
2
  export * from './resolveDefaultColumnType'
4
3
  export * from './NamingConventions'
package/src/tsconfig.json CHANGED
@@ -5,11 +5,7 @@
5
5
  "esModuleInterop": true
6
6
  },
7
7
  "references": [
8
- {
9
- "path": "../../schema/src"
10
- },
11
- {
12
- "path": "../../typesafe/src"
13
- }
8
+ { "path": "../../schema/src" },
9
+ { "path": "../../typesafe/src" }
14
10
  ]
15
11
  }
@@ -0,0 +1,79 @@
1
+ import * as Typesafe from '@contember/typesafe'
2
+ import { Actions } from '@contember/schema'
3
+
4
+ const selectionNodeSchemaFactory = (): Typesafe.Type<Actions.SelectionNode> => {
5
+ return (input: unknown, path: PropertyKey[] = []): Actions.SelectionNode => {
6
+ return Typesafe.array(Typesafe.union(
7
+ Typesafe.string,
8
+ Typesafe.tuple(
9
+ Typesafe.string,
10
+ Typesafe.anyJsonObject,
11
+ selectionNodeSchemaFactory(),
12
+ ),
13
+ ))(input, path)
14
+ }
15
+ }
16
+
17
+ const webhookTargetSchema = Typesafe.intersection(
18
+ Typesafe.object({
19
+ type: Typesafe.literal('webhook'),
20
+ name: Typesafe.string,
21
+ url: Typesafe.string,
22
+ }),
23
+ Typesafe.partial({
24
+ headers: Typesafe.record(Typesafe.string, Typesafe.string),
25
+ timeoutMs: Typesafe.integer,
26
+ maxAttempts: Typesafe.integer,
27
+ initialRepeatIntervalMs: Typesafe.integer,
28
+ batchSize: Typesafe.integer,
29
+ }),
30
+ )
31
+ const webhookTargetSchemaCheck: Typesafe.Equals<Actions.WebhookTarget, ReturnType<typeof webhookTargetSchema>> = true
32
+
33
+ const anyTargetSchema: Typesafe.Type<Actions.AnyTarget> = Typesafe.union(webhookTargetSchema)
34
+
35
+ const watchTriggerSchema = Typesafe.intersection(
36
+ Typesafe.object({
37
+ type: Typesafe.literal('watch'),
38
+ name: Typesafe.string,
39
+ entity: Typesafe.string,
40
+ watch: selectionNodeSchemaFactory(),
41
+ target: Typesafe.string,
42
+ }),
43
+ Typesafe.partial({
44
+ selection: selectionNodeSchemaFactory(),
45
+ priority: Typesafe.integer,
46
+ }),
47
+ )
48
+
49
+ const watchTriggerSchemaCheck: Typesafe.Equals<Actions.WatchTrigger, ReturnType<typeof watchTriggerSchema>> = true
50
+
51
+
52
+ const basicTriggerSchema = Typesafe.intersection(
53
+ Typesafe.object({
54
+ type: Typesafe.literal('basic'),
55
+ name: Typesafe.string,
56
+ entity: Typesafe.string,
57
+ create: Typesafe.boolean,
58
+ delete: Typesafe.boolean,
59
+ update: Typesafe.union(Typesafe.boolean, Typesafe.array(Typesafe.string)),
60
+ target: Typesafe.string,
61
+ }),
62
+ Typesafe.partial({
63
+ selection: selectionNodeSchemaFactory(),
64
+ priority: Typesafe.integer,
65
+ }),
66
+ )
67
+
68
+ const basicTriggerSchemaCheck: Typesafe.Equals<Actions.BasicTrigger, ReturnType<typeof basicTriggerSchema>> = true
69
+
70
+
71
+ const anyTriggerSchema = Typesafe.union(
72
+ watchTriggerSchema,
73
+ basicTriggerSchema,
74
+ )
75
+
76
+ export const actionsSchema = Typesafe.object({
77
+ triggers: Typesafe.record(Typesafe.string, anyTriggerSchema),
78
+ targets: Typesafe.record(Typesafe.string, anyTargetSchema),
79
+ })
@@ -148,10 +148,22 @@ const viewSchemaCheck: Typesafe.Equals<Model.View, ReturnType<typeof viewSchemaI
148
148
  const viewSchema: Typesafe.Type<Model.View> = viewSchemaInner
149
149
 
150
150
 
151
- const indexesSchema = Typesafe.coalesce<Model.Indexes, Model.Indexes>(Typesafe.record(Typesafe.string, Typesafe.object({
152
- fields: Typesafe.array(Typesafe.string),
153
- name: Typesafe.string,
154
- })), { })
151
+ const indexLike: Typesafe.Type<{readonly fields: readonly string[]; readonly name?: string | undefined}> = Typesafe.intersection(
152
+ Typesafe.object({
153
+ fields: Typesafe.array(Typesafe.string),
154
+ }),
155
+ Typesafe.partial({
156
+ name: Typesafe.string,
157
+ }),
158
+ )
159
+
160
+ const indexesSchema = Typesafe.coalesce<Model.Indexes, Model.Indexes>(
161
+ Typesafe.preprocess(
162
+ Typesafe.array(indexLike),
163
+ it => it?.constructor === Object ? Object.values(it) : it,
164
+ ),
165
+ [],
166
+ )
155
167
 
156
168
  const entitySchema = Typesafe.intersection(
157
169
  Typesafe.object({
@@ -160,15 +172,16 @@ const entitySchema = Typesafe.intersection(
160
172
  primaryColumn: Typesafe.string,
161
173
  tableName: Typesafe.string,
162
174
  fields: Typesafe.record(Typesafe.string, fieldSchema),
163
- unique: Typesafe.record(Typesafe.string, Typesafe.object({
164
- fields: Typesafe.array(Typesafe.string),
165
- name: Typesafe.string,
166
- })),
175
+ unique: Typesafe.preprocess(
176
+ Typesafe.array(indexLike),
177
+ it => it?.constructor === Object ? Object.values(it) : it,
178
+ ),
167
179
  indexes: indexesSchema,
168
180
  eventLog: eventLogSchema,
169
181
  }),
170
182
  Typesafe.partial({
171
183
  view: viewSchema,
184
+ orderBy: orderBySchema,
172
185
  }),
173
186
  )
174
187
 
@@ -0,0 +1,28 @@
1
+ import { Input, Model } from '@contember/schema'
2
+ import { acceptFieldVisitor } from '../model'
3
+ import { conditionSchema } from './condition'
4
+ import * as Typesafe from '@contember/typesafe'
5
+
6
+ export const whereSchema = ({ schema, entity }: {
7
+ schema: Model.Schema
8
+ entity: Model.Entity
9
+ }): Typesafe.Type<Input.Where> => {
10
+ return (input: unknown, path: PropertyKey[] = []): Input.Where => {
11
+ const whereSchemaInner = Typesafe.noExtraProps(Typesafe.partial({
12
+ and: Typesafe.array(whereSchema({ schema, entity })),
13
+ or: Typesafe.array(whereSchema({ schema, entity })),
14
+ not: whereSchema({ schema, entity }),
15
+ ...Object.fromEntries(Object.values(entity.fields).map((it): [string, Typesafe.Type<Input.Where[any]>] => {
16
+ return [
17
+ it.name,
18
+ acceptFieldVisitor(schema, entity, it.name, {
19
+ visitColumn: ({ column }) => conditionSchema(column.type),
20
+ visitRelation: ({ targetEntity }) => whereSchema({ schema, entity: targetEntity }),
21
+ }),
22
+ ]
23
+ })),
24
+ }))
25
+
26
+ return whereSchemaInner(input, path)
27
+ }
28
+ }
@@ -1,5 +1,7 @@
1
+ type Path = (string | number)[]
2
+
1
3
  type CompareError = {
2
- path: (string | number)[]
4
+ path: Path
3
5
  message: string
4
6
  }
5
7
 
@@ -26,10 +28,41 @@ const getType = (val: any): ExtendedType => {
26
28
  return 'object'
27
29
  }
28
30
 
29
- export function deepCompare(a: any, b: any, path: (string | number)[]): CompareError[] {
31
+ type Comparator = (a: unknown, b: unknown) => CompareError[]
32
+
33
+ export const compareArraysIgnoreOrder = (a: unknown, b: unknown, path: Path) => {
34
+ if (!Array.isArray(a) || !Array.isArray(b)) {
35
+ return [{ path, message: 'Invalid type, expected array' }]
36
+ }
37
+ if (a.length !== b.length) {
38
+ return [{ path, message: `Array length: ${a.length} != ${b.length}` }]
39
+ }
40
+ const haystack = [...b]
41
+ for (const needle of a) {
42
+ const index = haystack.findIndex(it => {
43
+ const errors = deepCompare(needle, it, path)
44
+ return errors.length === 0
45
+ })
46
+ if (index < 0) {
47
+ return [{ path, message: `Array item: ${JSON.stringify(needle)} not found in ${JSON.stringify(haystack)}` }]
48
+ }
49
+ haystack.splice(index, 1)
50
+ }
51
+ if (haystack.length > 0) {
52
+ return [{ path, message: `Array items: ${JSON.stringify(haystack)} not found in ${JSON.stringify(a)}` }]
53
+ }
54
+ return []
55
+ }
56
+
57
+ export function deepCompare(a: any, b: any, path: Path = [], getCustomComparator?: (path: Path) => Comparator | null): CompareError[] {
30
58
  if (a === b) {
31
59
  return []
32
60
  }
61
+ const comparator = getCustomComparator?.(path)
62
+ if (comparator) {
63
+ return comparator(a, b)
64
+ }
65
+
33
66
  const aType = getType(a)
34
67
  const bType = getType(b)
35
68
  if (aType !== bType) {
@@ -53,7 +86,7 @@ export function deepCompare(a: any, b: any, path: (string | number)[]): CompareE
53
86
  }
54
87
  const errors: CompareError[] = []
55
88
  for (let i = 0; i < aLength; i++) {
56
- errors.push(...deepCompare(a[i], b[i], [...path, i]))
89
+ errors.push(...deepCompare(a[i], b[i], [...path, i], getCustomComparator))
57
90
  }
58
91
  return errors
59
92
  }
@@ -91,7 +124,7 @@ export function deepCompare(a: any, b: any, path: (string | number)[]): CompareE
91
124
  message: `Missing property: ${key} not in right`,
92
125
  })
93
126
  } else {
94
- errors.push(...deepCompare(a[key], b[key], [...path, key]))
127
+ errors.push(...deepCompare(a[key], b[key], [...path, key], getCustomComparator))
95
128
  }
96
129
  }
97
130
  for (const key of bKeys) {
@@ -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
+ }
@@ -0,0 +1,119 @@
1
+ import { Actions, Model } from '@contember/schema'
2
+ import { acceptFieldVisitor } from '../model'
3
+ import { ErrorBuilder, ValidationError } from './errors'
4
+ import { assertNever } from '../utils'
5
+ import * as Typesafe from '@contember/typesafe'
6
+ import { whereSchema } from '../type-schema/where'
7
+
8
+ const manyArgsSchema = (args: {schema: Model.Schema; entity: Model.Entity}) => Typesafe.partial({
9
+ limit: Typesafe.integer,
10
+ offset: Typesafe.integer,
11
+ orderBy: Typesafe.array(Typesafe.anyJsonObject),
12
+ filter: whereSchema(args),
13
+ })
14
+
15
+ export class ActionsValidator {
16
+ constructor(private readonly model: Model.Schema) {}
17
+
18
+ public validate(schema: Actions.Schema): ValidationError[] {
19
+ const errorBuilder = new ErrorBuilder([], ['actions'])
20
+ this.validateTriggers(schema, errorBuilder.for('triggers'))
21
+ return errorBuilder.errors
22
+ }
23
+
24
+ private validateTriggers(schema: Actions.Schema, errorBuilder: ErrorBuilder) {
25
+ for (const [name, def] of Object.entries(schema.triggers)) {
26
+ const triggerErrorBuilder = errorBuilder.for(name)
27
+ if (name !== def.name) {
28
+ triggerErrorBuilder.add('ACTIONS_NAME_MISMATCH', `Trigger name ${def.name} does not match the name in a map ${name}`)
29
+ }
30
+ if (!schema.targets[def.target]) {
31
+ triggerErrorBuilder.add('ACTIONS_UNDEFINED_TRIGGER_TARGET', `Trigger target ${def.target} not found`)
32
+ }
33
+ this.validateTrigger(def, triggerErrorBuilder)
34
+ }
35
+ }
36
+
37
+
38
+ private validateTrigger(trigger: Actions.AnyTrigger, errorBuilder: ErrorBuilder): void {
39
+ const entity = this.model.entities[trigger.entity]
40
+ if (!entity) {
41
+ errorBuilder.add('ACTIONS_UNDEFINED_ENTITY', `Entity ${trigger.entity} not found.`)
42
+ return
43
+ }
44
+
45
+ switch (trigger.type) {
46
+ case 'watch':
47
+ this.validateWatchTrigger(trigger, entity, errorBuilder)
48
+ break
49
+ case 'basic':
50
+ this.validateBasicTrigger(trigger, entity, errorBuilder)
51
+ break
52
+ default:
53
+ assertNever(trigger)
54
+ }
55
+ }
56
+
57
+ private validateWatchTrigger(trigger: Actions.WatchTrigger, entity: Model.Entity, errorBuilder: ErrorBuilder): void {
58
+ this.validateSelection(trigger.watch, entity, errorBuilder.for('watch'))
59
+ if (trigger.selection) {
60
+ this.validateSelection(trigger.selection, entity, errorBuilder.for('selection'))
61
+ }
62
+ }
63
+
64
+ private validateBasicTrigger(trigger: Actions.BasicTrigger, entity: Model.Entity, errorBuilder: ErrorBuilder): void {
65
+ if (trigger.selection) {
66
+ this.validateSelection(trigger.selection, entity, errorBuilder.for('selection'))
67
+ }
68
+
69
+ if (Array.isArray(trigger.update)) {
70
+ for (const field of trigger.update) {
71
+ if (!entity.fields[field]) {
72
+ errorBuilder.for('update').add('ACTIONS_UNDEFINED_FIELD', `Field ${field} of entity ${entity.name} not found.`)
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ private validateSelection(
79
+ selectionNode: Actions.SelectionNode,
80
+ entity: Model.Entity,
81
+ errorBuilder: ErrorBuilder,
82
+ ): void {
83
+ for (const part of selectionNode) {
84
+ const [field, args, selection] = Array.isArray(part) ? part : [part, {}, []]
85
+ const fieldErrorBuilder = errorBuilder.for(field)
86
+ if (!entity.fields[field]) {
87
+ fieldErrorBuilder.add('ACTIONS_UNDEFINED_FIELD', `Undefined field ${field} on entity ${entity.name}`)
88
+ continue
89
+ }
90
+ acceptFieldVisitor(this.model, entity, field, {
91
+ visitColumn: () => {
92
+ if (selection.length) {
93
+ fieldErrorBuilder.add('ACTIONS_INVALID_SELECTION', `Unexpected sub selection on a column.`)
94
+ }
95
+ if (Object.keys(args).length !== 0) {
96
+ fieldErrorBuilder.add('ACTIONS_INVALID_SELECTION', `Unexpected args on a column.`)
97
+ }
98
+ },
99
+ visitHasMany: ({ targetEntity }) => {
100
+ if (selection.length === 0) {
101
+ fieldErrorBuilder.add('ACTIONS_INVALID_SELECTION', `Sub-selection expected on a relation.`)
102
+ }
103
+ this.validateSelection(selection, targetEntity, fieldErrorBuilder)
104
+ try {
105
+ manyArgsSchema({ schema: this.model, entity: targetEntity })(args)
106
+ } catch (e: any) {
107
+ fieldErrorBuilder.add('ACTIONS_INVALID_SELECTION', `Selection args are not valid: ` + e.message)
108
+ }
109
+ },
110
+ visitHasOne: ({ targetEntity }) => {
111
+ if (selection.length === 0) {
112
+ fieldErrorBuilder.add('ACTIONS_INVALID_SELECTION', `Sub-selection expected on a relation.`)
113
+ }
114
+ this.validateSelection(selection, targetEntity, fieldErrorBuilder)
115
+ },
116
+ })
117
+ }
118
+ }
119
+ }
@@ -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([], [])
@@ -54,15 +54,10 @@ export class ModelValidator {
54
54
  }
55
55
 
56
56
  private validateUniqueConstraints(uniqueConstraints: Model.Entity['unique'], fields: Set<string>, errors: ErrorBuilder): void {
57
- for (const [constraintName, constraint] of Object.entries(uniqueConstraints)) {
58
- const uniqueErrors = errors.for(constraintName)
59
- if (constraint.name !== constraintName) {
60
- uniqueErrors.add('MODEL_NAME_MISMATCH', `Constraint name ${constraint.name} does not match the name in a map "${constraintName}"`)
61
- continue
62
- }
57
+ for (const constraint of uniqueConstraints) {
63
58
  for (const field of constraint.fields) {
64
59
  if (!fields.has(field)) {
65
- uniqueErrors.add('MODEL_UNDEFINED_FIELD', `Referenced field ${field} in a constraint does not exists`)
60
+ errors.add('MODEL_UNDEFINED_FIELD', `Referenced field ${field} in a constraint does not exists`)
66
61
  }
67
62
  }
68
63
  }
@@ -79,7 +74,7 @@ export class ModelValidator {
79
74
  }
80
75
  }
81
76
 
82
- private validateRelation(partialEntity: Model.Entity, field: Model.AnyRelation, errors: ErrorBuilder): void {
77
+ private validateRelation(partialEntity: Model.Entity, field: Model.AnyRelation, errors: ErrorBuilder): void {
83
78
  const entityName = partialEntity.name
84
79
  const targetEntityName = field.target
85
80
  const targetEntity = this.model.entities[targetEntityName] || undefined
@@ -94,7 +89,7 @@ export class ModelValidator {
94
89
  const orderByField = entity.fields[it.path[i]]
95
90
  const pathStr = it.path.slice(0, i + 1).join('.')
96
91
  if (!orderByField) {
97
- errors.add('ACL_UNDEFINED_FIELD', `Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not defined`)
92
+ errors.add('MODEL_UNDEFINED_FIELD', `Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not defined`)
98
93
  return
99
94
  }
100
95
  if (i + 1 < it.path.length) {
@@ -148,7 +143,7 @@ export class ModelValidator {
148
143
  return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} inverse relation is not set`)
149
144
  }
150
145
  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`)
146
+ return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} expected, ${targetField.target}::${targetField.inversedBy} given`)
152
147
  }
153
148
  if (field.type === Model.RelationType.OneHasOne && targetField.type !== Model.RelationType.OneHasOne) {
154
149
  return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
@@ -185,7 +180,7 @@ export class ModelValidator {
185
180
  return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} owning relation is not set`)
186
181
  }
187
182
  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`)
183
+ return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} expected, ${targetField.target}::${targetField.ownedBy} given`)
189
184
  }
190
185
  if (field.type === Model.RelationType.OneHasOne && targetField.type !== Model.RelationType.OneHasOne) {
191
186
  return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
@@ -259,32 +254,13 @@ export class ModelValidator {
259
254
  }
260
255
  aliasedTypes.set(column.typeAlias, column.type)
261
256
  },
262
- visitManyHasManyInverse: () => {},
263
- visitOneHasMany: () => {},
264
- visitOneHasOneInverse: () => {},
265
- visitOneHasOneOwning: () => {},
266
- visitManyHasOne: () => {},
257
+ visitManyHasManyInverse: () => { },
258
+ visitOneHasMany: () => { },
259
+ visitOneHasOneInverse: () => { },
260
+ visitOneHasOneOwning: () => { },
261
+ visitManyHasOne: () => { },
267
262
  })
268
263
  }
269
- for (const entity of entities) {
270
- const entityErrorBuilder = errorBuilder.for(entity.name)
271
- for (const index of Object.values(entity.indexes)) {
272
- const description = `index name ${index.name} of entity ${entity.name}`
273
- if (relationNames[index.name]) {
274
- entityErrorBuilder.add('MODEL_NAME_COLLISION', `${description} collides with ${relationNames[index.name]}`)
275
- } else {
276
- relationNames[index.name] = description
277
- }
278
- }
279
- for (const unique of Object.values(entity.unique)) {
280
- const description = `unique index name ${unique.name} of entity ${entity.name}`
281
- if (relationNames[unique.name]) {
282
- entityErrorBuilder.add('MODEL_NAME_COLLISION', `${description} collides with ${relationNames[unique.name]}`)
283
- } else {
284
- relationNames[unique.name] = description
285
- }
286
- }
287
- }
288
264
  }
289
265
  }
290
266
 
@@ -3,6 +3,7 @@ import { ValidationError, ValidationErrorCode } from './errors'
3
3
  import { AclValidator } from './AclValidator'
4
4
  import { ModelValidator } from './ModelValidator'
5
5
  import { ValidationValidator } from './ValidationValidator'
6
+ import { ActionsValidator } from './ActionsValidator'
6
7
 
7
8
  export interface SchemaValidatorSkippedErrors {
8
9
  code: ValidationErrorCode
@@ -20,7 +21,11 @@ export class SchemaValidator {
20
21
 
21
22
  const validationValidator = new ValidationValidator(schema.model)
22
23
  const validationErrors = validationValidator.validate(schema.validation)
23
- const allErrors = [...aclErrors, ...modelErrors, ...validationErrors]
24
+
25
+ const actionsValidator = new ActionsValidator(schema.model)
26
+ const actionsErrors = actionsValidator.validate(schema.actions)
27
+
28
+ const allErrors = [...aclErrors, ...modelErrors, ...validationErrors, ...actionsErrors]
24
29
  if (skippedErrors.length === 0) {
25
30
  return allErrors
26
31
  }
@@ -16,6 +16,13 @@ export type ValidationErrorCode =
16
16
  | 'ACL_UNDEFINED_ROLE'
17
17
  | 'ACL_UNDEFINED_ENTITY'
18
18
 
19
+ | 'ACTIONS_NAME_MISMATCH'
20
+ | 'ACTIONS_INVALID_CONDITION'
21
+ | 'ACTIONS_UNDEFINED_FIELD'
22
+ | 'ACTIONS_UNDEFINED_ENTITY'
23
+ | 'ACTIONS_UNDEFINED_TRIGGER_TARGET'
24
+ | 'ACTIONS_INVALID_SELECTION'
25
+
19
26
  | 'VALIDATION_UNDEFINED_ENTITY'
20
27
  | 'VALIDATION_UNDEFINED_FIELD'
21
28
  | 'VALIDATION_NOT_IMPLEMENTED'