@contember/schema-utils 1.2.1 → 1.3.0-alpha.4
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/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/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lax/LaxSchemaConverter.d.ts.map +1 -1
- package/dist/src/lax/LaxSchemaConverter.js +2 -3
- package/dist/src/lax/LaxSchemaConverter.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 +4 -0
- package/dist/src/type-schema/model.d.ts.map +1 -1
- package/dist/src/type-schema/model.js +1 -0
- 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/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.js +1 -1
- 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/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 +4 -3
- package/src/acl/PredicateDefinitionProcessor.ts +59 -60
- package/src/index.ts +3 -0
- package/src/lax/LaxSchemaConverter.ts +2 -3
- package/src/tsconfig.json +2 -6
- package/src/type-schema/actions.ts +79 -0
- package/src/type-schema/model.ts +1 -0
- package/src/type-schema/where.ts +28 -0
- package/src/validation/ActionsValidator.ts +119 -0
- package/src/validation/ModelValidator.ts +1 -1
- package/src/validation/SchemaValidator.ts +6 -1
- package/src/validation/errors.ts +7 -0
- package/tests/cases/unit/__snapshots__/laxSchemaConverter.test.ts.snap +16 -0
- package/tests/cases/unit/whereSchema.test.ts +89 -0
- package/tests/tsconfig.json +1 -3
- package/tsconfig.json +2 -6
package/src/type-schema/model.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -94,7 +94,7 @@ export class ModelValidator {
|
|
|
94
94
|
const orderByField = entity.fields[it.path[i]]
|
|
95
95
|
const pathStr = it.path.slice(0, i + 1).join('.')
|
|
96
96
|
if (!orderByField) {
|
|
97
|
-
errors.add('
|
|
97
|
+
errors.add('MODEL_UNDEFINED_FIELD', `Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not defined`)
|
|
98
98
|
return
|
|
99
99
|
}
|
|
100
100
|
if (i + 1 < it.path.length) {
|
|
@@ -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'
|
|
@@ -5,6 +5,10 @@ exports[`lax schema converter basic 1`] = `
|
|
|
5
5
|
"acl": {
|
|
6
6
|
"roles": {},
|
|
7
7
|
},
|
|
8
|
+
"actions": {
|
|
9
|
+
"targets": {},
|
|
10
|
+
"triggers": {},
|
|
11
|
+
},
|
|
8
12
|
"model": {
|
|
9
13
|
"entities": {
|
|
10
14
|
"Article": {
|
|
@@ -67,6 +71,10 @@ exports[`lax schema converter many-has-many 1`] = `
|
|
|
67
71
|
"acl": {
|
|
68
72
|
"roles": {},
|
|
69
73
|
},
|
|
74
|
+
"actions": {
|
|
75
|
+
"targets": {},
|
|
76
|
+
"triggers": {},
|
|
77
|
+
},
|
|
70
78
|
"model": {
|
|
71
79
|
"entities": {
|
|
72
80
|
"Article": {
|
|
@@ -155,6 +163,10 @@ exports[`lax schema converter one-has-many 1`] = `
|
|
|
155
163
|
"acl": {
|
|
156
164
|
"roles": {},
|
|
157
165
|
},
|
|
166
|
+
"actions": {
|
|
167
|
+
"targets": {},
|
|
168
|
+
"triggers": {},
|
|
169
|
+
},
|
|
158
170
|
"model": {
|
|
159
171
|
"entities": {
|
|
160
172
|
"Article": {
|
|
@@ -234,6 +246,10 @@ exports[`lax schema converter one-has-one 1`] = `
|
|
|
234
246
|
"acl": {
|
|
235
247
|
"roles": {},
|
|
236
248
|
},
|
|
249
|
+
"actions": {
|
|
250
|
+
"targets": {},
|
|
251
|
+
"triggers": {},
|
|
252
|
+
},
|
|
237
253
|
"model": {
|
|
238
254
|
"entities": {
|
|
239
255
|
"Article": {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { assert, test, describe } from 'vitest'
|
|
2
|
+
import { conditionSchema } from '../../../src/type-schema'
|
|
3
|
+
import { Model } from '@contember/schema'
|
|
4
|
+
import { createSchema, SchemaDefinition as def } from '@contember/schema-definition'
|
|
5
|
+
import { whereSchema } from '../../../src/type-schema/where'
|
|
6
|
+
|
|
7
|
+
namespace MyModel {
|
|
8
|
+
export class Article {
|
|
9
|
+
title = def.stringColumn()
|
|
10
|
+
category = def.manyHasOne(Category)
|
|
11
|
+
tags = def.manyHasMany(Tag)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Category {
|
|
15
|
+
name = def.stringColumn()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Tag {
|
|
19
|
+
name = def.stringColumn()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const schema = createSchema(MyModel).model
|
|
24
|
+
|
|
25
|
+
describe('where schema', () => {
|
|
26
|
+
test('simple scalar where', () => {
|
|
27
|
+
const where = {
|
|
28
|
+
title: { eq: 'foo' },
|
|
29
|
+
}
|
|
30
|
+
assert.deepStrictEqual(whereSchema({ schema, entity: schema.entities.Article })(where), where)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('where with and / or /not', () => {
|
|
34
|
+
const where = {
|
|
35
|
+
and: [
|
|
36
|
+
{
|
|
37
|
+
or: [
|
|
38
|
+
{ not: { title: { eq: 'bar' } } },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
assert.deepStrictEqual(whereSchema({ schema, entity: schema.entities.Article })(where), where)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
test('where with relation', () => {
|
|
48
|
+
const where = {
|
|
49
|
+
tags: { name: { eq: 'xx' } },
|
|
50
|
+
}
|
|
51
|
+
assert.deepStrictEqual(whereSchema({ schema, entity: schema.entities.Article })(where), where)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('undefined field', () => {
|
|
55
|
+
const where = {
|
|
56
|
+
name: { eq: 'foo' },
|
|
57
|
+
}
|
|
58
|
+
assert.throw(() => whereSchema({
|
|
59
|
+
schema,
|
|
60
|
+
entity: schema.entities.Article,
|
|
61
|
+
})(where), 'value at root: extra property name found')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
test('undefined field in relation', () => {
|
|
66
|
+
const where = {
|
|
67
|
+
tags: {
|
|
68
|
+
caption: { eq: 'foo' },
|
|
69
|
+
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
assert.throw(() => whereSchema({
|
|
73
|
+
schema,
|
|
74
|
+
entity: schema.entities.Article,
|
|
75
|
+
})(where), 'value at path /tags: extra property caption found')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
test('invalid condition', () => {
|
|
80
|
+
const where = {
|
|
81
|
+
title: { xx: 'foo' },
|
|
82
|
+
}
|
|
83
|
+
assert.throw(() => whereSchema({
|
|
84
|
+
schema,
|
|
85
|
+
entity: schema.entities.Article,
|
|
86
|
+
})(where), 'value at path /title: extra property xx found')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
})
|
package/tests/tsconfig.json
CHANGED