@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.
Files changed (55) hide show
  1. package/dist/src/acl/PredicateDefinitionProcessor.d.ts.map +1 -1
  2. package/dist/src/acl/PredicateDefinitionProcessor.js +49 -44
  3. package/dist/src/acl/PredicateDefinitionProcessor.js.map +1 -1
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +3 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/lax/LaxSchemaConverter.d.ts.map +1 -1
  8. package/dist/src/lax/LaxSchemaConverter.js +2 -3
  9. package/dist/src/lax/LaxSchemaConverter.js.map +1 -1
  10. package/dist/src/tsconfig.tsbuildinfo +1 -1
  11. package/dist/src/type-schema/actions.d.ts +31 -0
  12. package/dist/src/type-schema/actions.d.ts.map +1 -0
  13. package/dist/src/type-schema/actions.js +75 -0
  14. package/dist/src/type-schema/actions.js.map +1 -0
  15. package/dist/src/type-schema/model.d.ts +4 -0
  16. package/dist/src/type-schema/model.d.ts.map +1 -1
  17. package/dist/src/type-schema/model.js +1 -0
  18. package/dist/src/type-schema/model.js.map +1 -1
  19. package/dist/src/type-schema/where.d.ts +7 -0
  20. package/dist/src/type-schema/where.d.ts.map +1 -0
  21. package/dist/src/type-schema/where.js +50 -0
  22. package/dist/src/type-schema/where.js.map +1 -0
  23. package/dist/src/validation/ActionsValidator.d.ts +13 -0
  24. package/dist/src/validation/ActionsValidator.d.ts.map +1 -0
  25. package/dist/src/validation/ActionsValidator.js +134 -0
  26. package/dist/src/validation/ActionsValidator.js.map +1 -0
  27. package/dist/src/validation/ModelValidator.js +1 -1
  28. package/dist/src/validation/ModelValidator.js.map +1 -1
  29. package/dist/src/validation/SchemaValidator.d.ts.map +1 -1
  30. package/dist/src/validation/SchemaValidator.js +4 -1
  31. package/dist/src/validation/SchemaValidator.js.map +1 -1
  32. package/dist/src/validation/errors.d.ts +1 -1
  33. package/dist/src/validation/errors.d.ts.map +1 -1
  34. package/dist/src/validation/errors.js.map +1 -1
  35. package/dist/tests/cases/unit/whereSchema.test.d.ts +2 -0
  36. package/dist/tests/cases/unit/whereSchema.test.d.ts.map +1 -0
  37. package/dist/tests/cases/unit/whereSchema.test.js +85 -0
  38. package/dist/tests/cases/unit/whereSchema.test.js.map +1 -0
  39. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +4 -3
  41. package/src/acl/PredicateDefinitionProcessor.ts +59 -60
  42. package/src/index.ts +3 -0
  43. package/src/lax/LaxSchemaConverter.ts +2 -3
  44. package/src/tsconfig.json +2 -6
  45. package/src/type-schema/actions.ts +79 -0
  46. package/src/type-schema/model.ts +1 -0
  47. package/src/type-schema/where.ts +28 -0
  48. package/src/validation/ActionsValidator.ts +119 -0
  49. package/src/validation/ModelValidator.ts +1 -1
  50. package/src/validation/SchemaValidator.ts +6 -1
  51. package/src/validation/errors.ts +7 -0
  52. package/tests/cases/unit/__snapshots__/laxSchemaConverter.test.ts.snap +16 -0
  53. package/tests/cases/unit/whereSchema.test.ts +89 -0
  54. package/tests/tsconfig.json +1 -3
  55. package/tsconfig.json +2 -6
@@ -169,6 +169,7 @@ const entitySchema = Typesafe.intersection(
169
169
  }),
170
170
  Typesafe.partial({
171
171
  view: viewSchema,
172
+ orderBy: orderBySchema,
172
173
  }),
173
174
  )
174
175
 
@@ -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('ACL_UNDEFINED_FIELD', `Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not defined`)
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
- 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'
@@ -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
+ })
@@ -5,8 +5,6 @@
5
5
  "esModuleInterop": true
6
6
  },
7
7
  "references": [
8
- {
9
- "path": "../src"
10
- }
8
+ { "path": "../src" }
11
9
  ]
12
10
  }
package/tsconfig.json CHANGED
@@ -2,11 +2,7 @@
2
2
  "files": [],
3
3
  "include": [],
4
4
  "references": [
5
- {
6
- "path": "./src"
7
- },
8
- {
9
- "path": "./tests"
10
- }
5
+ { "path": "./src" },
6
+ { "path": "./tests" }
11
7
  ]
12
8
  }