@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.
- package/dist/src/SchemaDatabaseMetadata.d.ts +40 -0
- package/dist/src/SchemaDatabaseMetadata.d.ts.map +1 -0
- package/dist/src/SchemaDatabaseMetadata.js +20 -0
- package/dist/src/SchemaDatabaseMetadata.js.map +1 -0
- package/dist/src/acl/PredicateDefinitionProcessor.d.ts.map +1 -1
- package/dist/src/acl/PredicateDefinitionProcessor.js +49 -44
- package/dist/src/acl/PredicateDefinitionProcessor.js.map +1 -1
- package/dist/src/definition-generator/AclDefinitionCodeGenerator.d.ts +19 -0
- package/dist/src/definition-generator/AclDefinitionCodeGenerator.d.ts.map +1 -0
- package/dist/src/definition-generator/AclDefinitionCodeGenerator.js +116 -0
- package/dist/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -0
- package/dist/src/definition-generator/DefinitionCodeGenerator.d.ts +27 -0
- package/dist/src/definition-generator/DefinitionCodeGenerator.d.ts.map +1 -0
- package/dist/src/definition-generator/DefinitionCodeGenerator.js +216 -0
- package/dist/src/definition-generator/DefinitionCodeGenerator.js.map +1 -0
- package/dist/src/definition-generator/DefinitionNamingConventions.d.ts +7 -0
- package/dist/src/definition-generator/DefinitionNamingConventions.d.ts.map +1 -0
- package/dist/src/definition-generator/DefinitionNamingConventions.js +18 -0
- package/dist/src/definition-generator/DefinitionNamingConventions.js.map +1 -0
- package/dist/src/definition-generator/TsDefinitionGenerator.d.ts +4 -18
- package/dist/src/definition-generator/TsDefinitionGenerator.d.ts.map +1 -1
- package/dist/src/definition-generator/TsDefinitionGenerator.js +6 -232
- package/dist/src/definition-generator/TsDefinitionGenerator.js.map +1 -1
- package/dist/src/definition-generator/index.d.ts +3 -0
- package/dist/src/definition-generator/index.d.ts.map +1 -1
- package/dist/src/definition-generator/index.js +3 -0
- package/dist/src/definition-generator/index.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lax/LaxSchemaConverter.d.ts.map +1 -1
- package/dist/src/lax/LaxSchemaConverter.js +4 -5
- package/dist/src/lax/LaxSchemaConverter.js.map +1 -1
- package/dist/src/model/index.d.ts +0 -1
- package/dist/src/model/index.d.ts.map +1 -1
- package/dist/src/model/index.js +0 -1
- package/dist/src/model/index.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/type-schema/actions.d.ts +31 -0
- package/dist/src/type-schema/actions.d.ts.map +1 -0
- package/dist/src/type-schema/actions.js +75 -0
- package/dist/src/type-schema/actions.js.map +1 -0
- package/dist/src/type-schema/model.d.ts +8 -6
- package/dist/src/type-schema/model.d.ts.map +1 -1
- package/dist/src/type-schema/model.js +6 -6
- package/dist/src/type-schema/model.js.map +1 -1
- package/dist/src/type-schema/where.d.ts +7 -0
- package/dist/src/type-schema/where.d.ts.map +1 -0
- package/dist/src/type-schema/where.js +50 -0
- package/dist/src/type-schema/where.js.map +1 -0
- package/dist/src/utils/deepCompare.d.ts +8 -2
- package/dist/src/utils/deepCompare.d.ts.map +1 -1
- package/dist/src/utils/deepCompare.js +32 -4
- package/dist/src/utils/deepCompare.js.map +1 -1
- package/dist/src/utils/printJsValue.d.ts +13 -0
- package/dist/src/utils/printJsValue.d.ts.map +1 -0
- package/dist/src/utils/printJsValue.js +57 -0
- package/dist/src/utils/printJsValue.js.map +1 -0
- package/dist/src/validation/ActionsValidator.d.ts +13 -0
- package/dist/src/validation/ActionsValidator.d.ts.map +1 -0
- package/dist/src/validation/ActionsValidator.js +134 -0
- package/dist/src/validation/ActionsValidator.js.map +1 -0
- package/dist/src/validation/ModelValidator.d.ts.map +1 -1
- package/dist/src/validation/ModelValidator.js +5 -31
- package/dist/src/validation/ModelValidator.js.map +1 -1
- package/dist/src/validation/SchemaValidator.d.ts.map +1 -1
- package/dist/src/validation/SchemaValidator.js +4 -1
- package/dist/src/validation/SchemaValidator.js.map +1 -1
- package/dist/src/validation/errors.d.ts +1 -1
- package/dist/src/validation/errors.d.ts.map +1 -1
- package/dist/src/validation/errors.js.map +1 -1
- package/dist/tests/cases/unit/deepCompare.test.d.ts +2 -0
- package/dist/tests/cases/unit/deepCompare.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/deepCompare.test.js +43 -0
- package/dist/tests/cases/unit/deepCompare.test.js.map +1 -0
- package/dist/tests/cases/unit/modelValidator.test.js +6 -72
- package/dist/tests/cases/unit/modelValidator.test.js.map +1 -1
- package/dist/tests/cases/unit/printJsvalue.test.d.ts +2 -0
- package/dist/tests/cases/unit/printJsvalue.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/printJsvalue.test.js +90 -0
- package/dist/tests/cases/unit/printJsvalue.test.js.map +1 -0
- package/dist/tests/cases/unit/schemas/acl.d.ts +13 -0
- package/dist/tests/cases/unit/schemas/acl.d.ts.map +1 -0
- package/dist/tests/cases/unit/schemas/acl.js +47 -0
- package/dist/tests/cases/unit/schemas/acl.js.map +1 -0
- package/dist/tests/cases/unit/schemas/basic.d.ts.map +1 -1
- package/dist/tests/cases/unit/schemas/basic.js.map +1 -1
- package/dist/tests/cases/unit/schemas/enum.d.ts.map +1 -1
- package/dist/tests/cases/unit/schemas/enum.js.map +1 -1
- package/dist/tests/cases/unit/schemas/relations.d.ts.map +1 -1
- package/dist/tests/cases/unit/schemas/relations.js.map +1 -1
- package/dist/tests/cases/unit/schemas/unique.d.ts.map +1 -1
- package/dist/tests/cases/unit/schemas/unique.js.map +1 -1
- package/dist/tests/cases/unit/tsDefinitionGenerator.test.js +5 -3
- package/dist/tests/cases/unit/tsDefinitionGenerator.test.js.map +1 -1
- package/dist/tests/cases/unit/whereSchema.test.d.ts +2 -0
- package/dist/tests/cases/unit/whereSchema.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/whereSchema.test.js +85 -0
- package/dist/tests/cases/unit/whereSchema.test.js.map +1 -0
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -4
- package/src/SchemaDatabaseMetadata.ts +66 -0
- package/src/acl/PredicateDefinitionProcessor.ts +59 -60
- package/src/definition-generator/AclDefinitionCodeGenerator.ts +126 -0
- package/src/definition-generator/DefinitionCodeGenerator.ts +250 -0
- package/src/definition-generator/DefinitionNamingConventions.ts +16 -0
- package/src/definition-generator/TsDefinitionGenerator.ts +8 -264
- package/src/definition-generator/index.ts +3 -0
- package/src/index.ts +5 -1
- package/src/lax/LaxSchemaConverter.ts +4 -5
- package/src/model/index.ts +0 -1
- package/src/tsconfig.json +2 -6
- package/src/type-schema/actions.ts +79 -0
- package/src/type-schema/model.ts +21 -8
- package/src/type-schema/where.ts +28 -0
- package/src/utils/deepCompare.ts +37 -4
- package/src/utils/printJsValue.ts +62 -0
- package/src/validation/ActionsValidator.ts +119 -0
- package/src/validation/ModelValidator.ts +12 -36
- package/src/validation/SchemaValidator.ts +6 -1
- package/src/validation/errors.ts +7 -0
- package/tests/cases/unit/__snapshots__/laxSchemaConverter.test.ts.snap +30 -14
- package/tests/cases/unit/deepCompare.test.ts +44 -0
- package/tests/cases/unit/modelValidator.test.ts +6 -73
- package/tests/cases/unit/printJsvalue.test.ts +100 -0
- package/tests/cases/unit/schemas/acl.ts +34 -0
- package/tests/cases/unit/schemas/basic.ts +1 -1
- package/tests/cases/unit/schemas/enum.ts +1 -1
- package/tests/cases/unit/schemas/relations.ts +1 -1
- package/tests/cases/unit/schemas/unique.ts +1 -1
- package/tests/cases/unit/tsDefinitionGenerator.test.ts +6 -4
- package/tests/cases/unit/whereSchema.test.ts +89 -0
- package/tests/tsconfig.json +1 -3
- package/tsconfig.json +2 -6
- package/dist/src/model/NamingHelper.d.ts +0 -9
- package/dist/src/model/NamingHelper.d.ts.map +0 -1
- package/dist/src/model/NamingHelper.js +0 -46
- package/dist/src/model/NamingHelper.js.map +0 -1
- 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
|
-
|
|
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),
|
package/src/model/index.ts
CHANGED
package/src/tsconfig.json
CHANGED
|
@@ -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
|
+
})
|
package/src/type-schema/model.ts
CHANGED
|
@@ -148,10 +148,22 @@ const viewSchemaCheck: Typesafe.Equals<Model.View, ReturnType<typeof viewSchemaI
|
|
|
148
148
|
const viewSchema: Typesafe.Type<Model.View> = viewSchemaInner
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
})
|
|
151
|
+
const indexLike: Typesafe.Type<{readonly fields: readonly string[]; readonly name?: string | undefined}> = Typesafe.intersection(
|
|
152
|
+
Typesafe.object({
|
|
153
|
+
fields: Typesafe.array(Typesafe.string),
|
|
154
|
+
}),
|
|
155
|
+
Typesafe.partial({
|
|
156
|
+
name: Typesafe.string,
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const indexesSchema = Typesafe.coalesce<Model.Indexes, Model.Indexes>(
|
|
161
|
+
Typesafe.preprocess(
|
|
162
|
+
Typesafe.array(indexLike),
|
|
163
|
+
it => it?.constructor === Object ? Object.values(it) : it,
|
|
164
|
+
),
|
|
165
|
+
[],
|
|
166
|
+
)
|
|
155
167
|
|
|
156
168
|
const entitySchema = Typesafe.intersection(
|
|
157
169
|
Typesafe.object({
|
|
@@ -160,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.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
unique: Typesafe.preprocess(
|
|
176
|
+
Typesafe.array(indexLike),
|
|
177
|
+
it => it?.constructor === Object ? Object.values(it) : it,
|
|
178
|
+
),
|
|
167
179
|
indexes: indexesSchema,
|
|
168
180
|
eventLog: eventLogSchema,
|
|
169
181
|
}),
|
|
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
|
+
}
|
package/src/utils/deepCompare.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
type Path = (string | number)[]
|
|
2
|
+
|
|
1
3
|
type CompareError = {
|
|
2
|
-
path:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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):
|
|
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('
|
|
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}
|
|
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}
|
|
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
|
-
|
|
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
|
}
|
package/src/validation/errors.ts
CHANGED
|
@@ -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'
|