@contember/schema-utils 1.1.2 → 1.2.0-alpha.11

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 (70) hide show
  1. package/dist/src/acl/PredicateDefinitionProcessor.d.ts.map +1 -1
  2. package/dist/src/acl/PredicateDefinitionProcessor.js +3 -2
  3. package/dist/src/acl/PredicateDefinitionProcessor.js.map +1 -1
  4. package/dist/src/model/NamingHelper.d.ts +2 -0
  5. package/dist/src/model/NamingHelper.d.ts.map +1 -1
  6. package/dist/src/model/NamingHelper.js +20 -7
  7. package/dist/src/model/NamingHelper.js.map +1 -1
  8. package/dist/src/model/modelUtils.d.ts +1 -0
  9. package/dist/src/model/modelUtils.d.ts.map +1 -1
  10. package/dist/src/model/modelUtils.js +69 -46
  11. package/dist/src/model/modelUtils.js.map +1 -1
  12. package/dist/src/schemaNormalizer.d.ts.map +1 -1
  13. package/dist/src/schemaNormalizer.js +15 -4
  14. package/dist/src/schemaNormalizer.js.map +1 -1
  15. package/dist/src/tsconfig.tsbuildinfo +1 -1
  16. package/dist/src/type-schema/acl.d.ts.map +1 -1
  17. package/dist/src/type-schema/acl.js +15 -3
  18. package/dist/src/type-schema/acl.js.map +1 -1
  19. package/dist/src/type-schema/condition.d.ts +5 -0
  20. package/dist/src/type-schema/condition.d.ts.map +1 -0
  21. package/dist/src/type-schema/condition.js +95 -0
  22. package/dist/src/type-schema/condition.js.map +1 -0
  23. package/dist/src/type-schema/index.d.ts +1 -0
  24. package/dist/src/type-schema/index.d.ts.map +1 -1
  25. package/dist/src/type-schema/index.js +1 -0
  26. package/dist/src/type-schema/index.js.map +1 -1
  27. package/dist/src/type-schema/model.d.ts +1 -0
  28. package/dist/src/type-schema/model.d.ts.map +1 -1
  29. package/dist/src/type-schema/model.js +5 -0
  30. package/dist/src/type-schema/model.js.map +1 -1
  31. package/dist/src/validation/AclValidator.d.ts.map +1 -1
  32. package/dist/src/validation/AclValidator.js +13 -2
  33. package/dist/src/validation/AclValidator.js.map +1 -1
  34. package/dist/src/validation/MembershipResolver.d.ts +37 -0
  35. package/dist/src/validation/MembershipResolver.d.ts.map +1 -0
  36. package/dist/src/validation/MembershipResolver.js +104 -0
  37. package/dist/src/validation/MembershipResolver.js.map +1 -0
  38. package/dist/src/validation/ModelValidator.d.ts.map +1 -1
  39. package/dist/src/validation/ModelValidator.js +13 -6
  40. package/dist/src/validation/ModelValidator.js.map +1 -1
  41. package/dist/src/validation/index.d.ts +1 -0
  42. package/dist/src/validation/index.d.ts.map +1 -1
  43. package/dist/src/validation/index.js +1 -0
  44. package/dist/src/validation/index.js.map +1 -1
  45. package/dist/tests/cases/unit/conditionSchema.test.d.ts +2 -0
  46. package/dist/tests/cases/unit/conditionSchema.test.d.ts.map +1 -0
  47. package/dist/tests/cases/unit/conditionSchema.test.js +50 -0
  48. package/dist/tests/cases/unit/conditionSchema.test.js.map +1 -0
  49. package/dist/tests/cases/unit/membershipResolver.test.d.ts +2 -0
  50. package/dist/tests/cases/unit/membershipResolver.test.d.ts.map +1 -0
  51. package/dist/tests/cases/unit/membershipResolver.test.js +149 -0
  52. package/dist/tests/cases/unit/membershipResolver.test.js.map +1 -0
  53. package/dist/tests/tsconfig.tsbuildinfo +1 -0
  54. package/package.json +5 -5
  55. package/src/acl/PredicateDefinitionProcessor.ts +3 -2
  56. package/src/model/NamingHelper.ts +19 -8
  57. package/src/model/modelUtils.ts +76 -71
  58. package/src/schemaNormalizer.ts +15 -4
  59. package/src/type-schema/acl.ts +30 -9
  60. package/src/type-schema/condition.ts +81 -0
  61. package/src/type-schema/index.ts +1 -0
  62. package/src/type-schema/model.ts +7 -0
  63. package/src/validation/AclValidator.ts +12 -2
  64. package/src/validation/MembershipResolver.ts +133 -0
  65. package/src/validation/ModelValidator.ts +15 -6
  66. package/src/validation/index.ts +1 -0
  67. package/tests/cases/unit/conditionSchema.test.ts +55 -0
  68. package/tests/cases/unit/membershipResolver.test.ts +161 -0
  69. package/tests/tsconfig.json +12 -0
  70. package/tsconfig.json +8 -1
@@ -48,10 +48,10 @@ class PredicateDefinitionProcessor {
48
48
  const fieldWhere = acceptFieldVisitor<
49
49
  WhereValue | Input.Where | Input.Condition | [string, WhereValue | Input.Where | Input.Condition] | undefined
50
50
  >(this.schema, entity, key, {
51
- visitColumn: (entity, column) => {
51
+ visitColumn: ({ entity, column }) => {
52
52
  return handler.handleColumn({ entity, column, value: value as Input.Condition | PredicateExtension, path })
53
53
  },
54
- visitRelation: (entity, relation, targetEntity) => {
54
+ visitRelation: ({ entity, relation, targetEntity }) => {
55
55
  const processedValue = handler.handleRelation({
56
56
  relation,
57
57
  entity,
@@ -61,6 +61,7 @@ class PredicateDefinitionProcessor {
61
61
  })
62
62
  if (
63
63
  typeof processedValue === 'object' &&
64
+ processedValue !== null &&
64
65
  'constructor' in processedValue &&
65
66
  processedValue.constructor.name === 'Object'
66
67
  ) {
@@ -2,20 +2,31 @@ import crypto from 'crypto'
2
2
 
3
3
  export class NamingHelper {
4
4
  public static createForeignKeyName(fromTable: string, fromColumn: string, toTable: string, toColumn: string): string {
5
- const uniqueSuffix = crypto
6
- .createHash('sha256')
7
- .update(JSON.stringify([fromTable, fromColumn, toTable, toColumn]), 'ascii')
8
- .digest('hex')
5
+ return 'fk_'
6
+ + fromTable + '_'
7
+ + fromColumn + '_'
8
+ + this.createUniqueSuffix([fromTable, fromColumn, toTable, toColumn])
9
+ }
9
10
 
10
- return 'fk_' + fromTable + '_' + fromColumn + '_' + uniqueSuffix.slice(0, 6)
11
+ public static createIndexName = (entityName: string, fields: readonly string[]): string => {
12
+ return 'idx_'
13
+ + entityName + '_'
14
+ + fields.join('_') + '_'
15
+ + this.createUniqueSuffix([entityName, ...fields])
11
16
  }
12
17
 
13
18
  public static createUniqueConstraintName = (entityName: string, fields: readonly string[]): string => {
19
+ return 'unique_'
20
+ + entityName + '_'
21
+ + fields.join('_') + '_'
22
+ + this.createUniqueSuffix([entityName, ...fields])
23
+ }
24
+
25
+ private static createUniqueSuffix = (values: string[]): string => {
14
26
  const uniqueSuffix = crypto
15
27
  .createHash('sha256')
16
- .update(JSON.stringify([entityName, ...fields]), 'ascii')
28
+ .update(JSON.stringify(values), 'ascii')
17
29
  .digest('hex')
18
-
19
- return 'unique_' + entityName + '_' + fields.join('_') + '_' + uniqueSuffix.slice(0, 6)
30
+ return uniqueSuffix.slice(0, 6)
20
31
  }
21
32
  }
@@ -28,10 +28,18 @@ export const getEntity = (schema: Model.Schema, entityName: string): Model.Entit
28
28
  return entity
29
29
  }
30
30
 
31
+ export const getField = (entity: Model.Entity, fieldName: string): Model.AnyField => {
32
+ const field = entity.fields[fieldName]
33
+ if (!field) {
34
+ throw createFieldNotFoundError(entity.name, fieldName)
35
+ }
36
+ return field
37
+ }
38
+
31
39
  export const getColumnName = (schema: Model.Schema, entity: Model.Entity, fieldName: string) => {
32
40
  return acceptFieldVisitor(schema, entity, fieldName, {
33
- visitColumn: (entity, column) => column.columnName,
34
- visitRelation: (entity, relation) => {
41
+ visitColumn: ({ column }) => column.columnName,
42
+ visitRelation: ({ entity, relation }) => {
35
43
  if (isIt<Model.JoiningColumnRelation>(relation, 'joiningColumn')) {
36
44
  return relation.joiningColumn.columnName
37
45
  }
@@ -57,8 +65,8 @@ export const tryGetColumnName = (schema: Model.Schema, entity: Model.Entity, fie
57
65
  export const getColumnType = (schema: Model.Schema, entity: Model.Entity, fieldName: string): string => {
58
66
  return acceptFieldVisitor(schema, entity, fieldName, {
59
67
  // TODO solve enum handling properly maybe we should distinguish between domain and column type
60
- visitColumn: (entity, column) => (column.type === Model.ColumnType.Enum ? 'text' : column.columnType),
61
- visitRelation: (entity, relation, targetEntity) => {
68
+ visitColumn: ({ column }) => (column.type === Model.ColumnType.Enum ? 'text' : column.columnType),
69
+ visitRelation: ({ entity, relation, targetEntity }) => {
62
70
  if (isIt<Model.JoiningColumnRelation>(relation, 'joiningColumn')) {
63
71
  return getColumnType(schema, targetEntity, targetEntity.primary)
64
72
  }
@@ -77,7 +85,7 @@ export const getTargetEntity = (
77
85
  ): Model.Entity | null => {
78
86
  return acceptFieldVisitor(schema, entity, relationName, {
79
87
  visitColumn: () => null,
80
- visitRelation: (entity, relation, targetEntity) => targetEntity,
88
+ visitRelation: ({ targetEntity }) => targetEntity,
81
89
  })
82
90
  }
83
91
 
@@ -87,9 +95,7 @@ export const acceptEveryFieldVisitor = <T>(
87
95
  visitor: Model.FieldVisitor<T>,
88
96
  ): { [fieldName: string]: T } => {
89
97
  const entityObj: Model.Entity = typeof entity === 'string' ? getEntity(schema, entity) : entity
90
- if (!entityObj) {
91
- throw createEntityNotFoundError(typeof entity === 'string' ? entity : 'unknown')
92
- }
98
+
93
99
  const result: { [fieldName: string]: T } = {}
94
100
  for (const field in entityObj.fields) {
95
101
  result[field] = acceptFieldVisitor(schema, entityObj, field, visitor)
@@ -104,34 +110,28 @@ export const acceptFieldVisitor = <T>(
104
110
  visitor: Model.FieldVisitor<T>,
105
111
  ): T => {
106
112
  const entityObj: Model.Entity = typeof entity === 'string' ? getEntity(schema, entity) : entity
107
- if (!entityObj) {
108
- throw createEntityNotFoundError(typeof entity === 'string' ? entity : 'unknown')
109
- }
113
+
110
114
  const fieldObj: Model.AnyField = typeof field === 'string' ? entityObj.fields[field] : field
111
115
  if (!fieldObj) {
112
116
  throw createFieldNotFoundError(entityObj.name, typeof field === 'string' ? field : 'unknown')
113
117
  }
118
+
114
119
  if (isIt<Model.AnyColumn>(fieldObj, 'columnType')) {
115
- return visitor.visitColumn(entityObj, fieldObj)
116
- }
117
- if (!isIt<Model.AnyRelation>(fieldObj, 'target')) {
118
- throw new Error(`Invalid field type`)
120
+ return visitor.visitColumn({
121
+ entity: entityObj,
122
+ column: fieldObj,
123
+ })
119
124
  }
120
- const targetEntity = getEntity(schema, fieldObj.target)
121
125
 
122
126
  if (isIt<Model.ColumnVisitor<T> & Model.RelationVisitor<T>>(visitor, 'visitRelation')) {
123
- let targetRelation = null
124
- if (isOwningRelation(fieldObj)) {
125
- targetRelation = fieldObj.inversedBy ? targetEntity.fields[fieldObj.inversedBy] || null : null
126
- } else if (isInverseRelation(fieldObj)) {
127
- targetRelation = targetEntity.fields[fieldObj.ownedBy]
128
- } else {
129
- throw new Error('Invalid relaton type')
130
- }
131
- if (targetRelation && !isIt<Model.Relation>(targetRelation, 'target')) {
132
- throw new Error('Invalid target')
133
- }
134
- return visitor.visitRelation(entityObj, fieldObj, targetEntity, targetRelation)
127
+ return acceptRelationTypeVisitor(schema, entityObj, fieldObj, {
128
+ visitManyHasManyInverse: visitor.visitRelation.bind(visitor),
129
+ visitManyHasManyOwning: visitor.visitRelation.bind(visitor),
130
+ visitOneHasMany: visitor.visitRelation.bind(visitor),
131
+ visitOneHasOneInverse: visitor.visitRelation.bind(visitor),
132
+ visitOneHasOneOwning: visitor.visitRelation.bind(visitor),
133
+ visitManyHasOne: visitor.visitRelation.bind(visitor),
134
+ })
135
135
  }
136
136
 
137
137
  if (isIt<Model.ColumnVisitor<T> & Model.RelationByGenericTypeVisitor<T>>(visitor, 'visitHasMany')) {
@@ -148,7 +148,8 @@ export const acceptFieldVisitor = <T>(
148
148
  if (isIt<Model.ColumnVisitor<T> & Model.RelationByTypeVisitor<T>>(visitor, 'visitManyHasManyInverse')) {
149
149
  return acceptRelationTypeVisitor(schema, entityObj, fieldObj, visitor)
150
150
  }
151
- throw new Error('Invalid field type')
151
+
152
+ throw new Error()
152
153
  }
153
154
 
154
155
  export const acceptRelationTypeVisitor = <T>(
@@ -157,49 +158,50 @@ export const acceptRelationTypeVisitor = <T>(
157
158
  relation: string | Model.AnyRelation,
158
159
  visitor: Model.RelationByTypeVisitor<T>,
159
160
  ): T => {
160
- const entityObj: Model.Entity = typeof entity === 'string' ? getEntity(schema, entity) : entity
161
- if (!entityObj) {
162
- throw createEntityNotFoundError(typeof entity === 'string' ? entity : 'unknown')
163
- }
161
+ const entityObj = typeof entity === 'string' ? getEntity(schema, entity) : entity
162
+
164
163
  const relationObj: Model.AnyField = typeof relation === 'string' ? entityObj.fields[relation] : relation
165
164
  if (!relationObj) {
166
165
  throw createFieldNotFoundError(entityObj.name, typeof relation === 'string' ? relation : 'unknown')
167
166
  }
167
+
168
168
  if (!isRelation(relationObj)) {
169
169
  throw new ModelError(
170
170
  ModelErrorCode.NOT_RELATION,
171
171
  `Field ${relationObj.name} of entity ${entityObj.name} is not a relation`,
172
172
  )
173
173
  }
174
+
174
175
  const targetEntity = getEntity(schema, relationObj.target)
175
176
 
176
177
  if (isInverseRelation(relationObj)) {
177
178
  const targetRelation = targetEntity.fields[relationObj.ownedBy]
178
- if (!isRelation(targetRelation)) {
179
- throw new Error('Invalid target relation')
180
- }
181
179
  switch (relationObj.type) {
182
180
  case Model.RelationType.ManyHasMany:
183
- return visitor.visitManyHasManyInverse(
184
- entityObj,
185
- relationObj,
186
- targetEntity,
187
- targetRelation as Model.ManyHasManyOwningRelation,
181
+ return visitor.visitManyHasManyInverse({
182
+ type: 'manyHasManyInverse',
183
+ entity: entityObj,
184
+ relation: relationObj,
185
+ targetEntity: targetEntity,
186
+ targetRelation: targetRelation as Model.ManyHasManyOwningRelation,
187
+ },
188
188
  )
189
189
  case Model.RelationType.OneHasOne:
190
- return visitor.visitOneHasOneInverse(
191
- entityObj,
192
- relationObj,
193
- targetEntity,
194
- targetRelation as Model.OneHasOneOwningRelation,
195
- )
190
+ return visitor.visitOneHasOneInverse({
191
+ type: 'oneHasOneInverse',
192
+ entity: entityObj,
193
+ relation: relationObj,
194
+ targetEntity: targetEntity,
195
+ targetRelation: targetRelation as Model.OneHasOneOwningRelation,
196
+ })
196
197
  case Model.RelationType.OneHasMany:
197
- return visitor.visitOneHasMany(
198
- entityObj, //
199
- relationObj,
200
- targetEntity,
201
- targetRelation as Model.ManyHasOneRelation,
202
- )
198
+ return visitor.visitOneHasMany({
199
+ type: 'oneHasMany',
200
+ entity: entityObj,
201
+ relation: relationObj,
202
+ targetEntity: targetEntity,
203
+ targetRelation: targetRelation as Model.ManyHasOneRelation,
204
+ })
203
205
  default:
204
206
  return assertNever(relationObj)
205
207
  }
@@ -208,26 +210,29 @@ export const acceptRelationTypeVisitor = <T>(
208
210
 
209
211
  switch (relationObj.type) {
210
212
  case Model.RelationType.ManyHasMany:
211
- return visitor.visitManyHasManyOwning(
212
- entityObj,
213
- relationObj,
214
- targetEntity,
215
- targetRelation as Model.ManyHasManyInverseRelation,
216
- )
213
+ return visitor.visitManyHasManyOwning({
214
+ type: 'manyHasManyOwning',
215
+ entity: entityObj,
216
+ relation: relationObj,
217
+ targetEntity: targetEntity,
218
+ targetRelation: targetRelation as Model.ManyHasManyInverseRelation,
219
+ })
217
220
  case Model.RelationType.OneHasOne:
218
- return visitor.visitOneHasOneOwning(
219
- entityObj,
220
- relationObj,
221
- targetEntity,
222
- targetRelation as Model.OneHasOneInverseRelation,
223
- )
221
+ return visitor.visitOneHasOneOwning({
222
+ type: 'oneHasOneOwning',
223
+ entity: entityObj,
224
+ relation: relationObj,
225
+ targetEntity: targetEntity,
226
+ targetRelation: targetRelation as Model.OneHasOneInverseRelation,
227
+ })
224
228
  case Model.RelationType.ManyHasOne:
225
- return visitor.visitManyHasOne(
226
- entityObj, //
227
- relationObj,
228
- targetEntity,
229
- targetRelation as Model.OneHasManyRelation,
230
- )
229
+ return visitor.visitManyHasOne({
230
+ type: 'manyHasOne',
231
+ entity: entityObj, //
232
+ relation: relationObj,
233
+ targetEntity: targetEntity,
234
+ targetRelation: targetRelation as Model.OneHasManyRelation,
235
+ })
231
236
  default:
232
237
  return assertNever(relationObj)
233
238
  }
@@ -18,6 +18,14 @@ export const normalizeSchema = <S extends Schema>(schema: S): S => {
18
18
  read: true,
19
19
  },
20
20
  },
21
+ content: {
22
+ export: true,
23
+ import: true,
24
+ },
25
+ system: {
26
+ export: true,
27
+ import: true,
28
+ },
21
29
  ...((schema.acl.roles?.[ProjectRole.ADMIN] as Acl.RolePermissions | undefined) || {}),
22
30
  },
23
31
  [ProjectRole.CONTENT_ADMIN]: {
@@ -30,11 +38,14 @@ export const normalizeSchema = <S extends Schema>(schema: S): S => {
30
38
  read: true,
31
39
  },
32
40
  },
41
+ content: {
42
+ export: true,
43
+ import: true,
44
+ },
33
45
  system: {
34
- diff: Acl.SystemPermissionsLevel.any,
35
- history: Acl.SystemPermissionsLevel.any,
36
- release: Acl.SystemPermissionsLevel.any,
37
- rebase: Acl.SystemPermissionsLevel.any,
46
+ history: true,
47
+ export: true,
48
+ import: true,
38
49
  },
39
50
  ...((schema.acl.roles?.[ProjectRole.CONTENT_ADMIN] as Acl.RolePermissions | undefined) || {}),
40
51
  },
@@ -1,18 +1,26 @@
1
1
  import { Acl, Model } from '@contember/schema'
2
2
  import * as Typesafe from '@contember/typesafe'
3
3
 
4
+ const membershipMatchRuleSchema = Typesafe.record(
5
+ Typesafe.string,
6
+ Typesafe.union(
7
+ Typesafe.literal(true),
8
+ Typesafe.partial({
9
+ variables: Typesafe.union(
10
+ Typesafe.literal(true),
11
+ Typesafe.record(
12
+ Typesafe.string,
13
+ Typesafe.union(Typesafe.literal(true), Typesafe.string),
14
+ ),
15
+ ),
16
+ }),
17
+ ),
18
+ )
19
+
4
20
  const tenantPermissionsSchema = Typesafe.partial({
5
21
  invite: Typesafe.boolean,
6
22
  unmanagedInvite: Typesafe.boolean,
7
- manage: Typesafe.record(Typesafe.string, Typesafe.partial({
8
- variables: Typesafe.union(
9
- Typesafe.literal(true),
10
- Typesafe.record(
11
- Typesafe.string,
12
- Typesafe.union(Typesafe.literal(true), Typesafe.string),
13
- ),
14
- ),
15
- })),
23
+ manage: membershipMatchRuleSchema,
16
24
  })
17
25
  const tenantSchemaCheck: Typesafe.Equals<Acl.TenantPermissions, ReturnType<typeof tenantPermissionsSchema>> = true
18
26
 
@@ -20,9 +28,18 @@ const systemPermissionsSchema = Typesafe.partial({
20
28
  history: Typesafe.union(Typesafe.boolean, Typesafe.enumeration('any', 'none')),
21
29
  migrate: Typesafe.boolean,
22
30
  assumeIdentity: Typesafe.boolean,
31
+ export: Typesafe.boolean,
32
+ import: Typesafe.boolean,
23
33
  })
24
34
  const systemSchemaCheck: Typesafe.Equals<Acl.SystemPermissions, ReturnType<typeof systemPermissionsSchema>> = true
25
35
 
36
+ const contentPermissionsSchema = Typesafe.partial({
37
+ assumeMembership: membershipMatchRuleSchema,
38
+ export: Typesafe.boolean,
39
+ import: Typesafe.boolean,
40
+ })
41
+ const contentSchemaCheck: Typesafe.Equals<Acl.ContentPermissions, ReturnType<typeof contentPermissionsSchema>> = true
42
+
26
43
  const variablesSchema = Typesafe.record(
27
44
  Typesafe.string,
28
45
  Typesafe.union(
@@ -34,6 +51,9 @@ const variablesSchema = Typesafe.record(
34
51
  type: Typesafe.literal(Acl.VariableType.predefined),
35
52
  value: Typesafe.enumeration('identityID', 'personID'),
36
53
  }),
54
+ Typesafe.object({
55
+ type: Typesafe.literal(Acl.VariableType.condition),
56
+ }),
37
57
  ),
38
58
  )
39
59
 
@@ -81,6 +101,7 @@ const baseRolePermissionsSchema = Typesafe.intersection(
81
101
  ),
82
102
  tenant: tenantPermissionsSchema,
83
103
  system: systemPermissionsSchema,
104
+ content: contentPermissionsSchema,
84
105
  }),
85
106
  )
86
107
  const baseRolePermissionsCheck: Typesafe.Equals<Acl.BaseRolePermissions, ReturnType<typeof baseRolePermissionsSchema>> = true
@@ -0,0 +1,81 @@
1
+ import * as Typesafe from '@contember/typesafe'
2
+ import { Json, Type } from '@contember/typesafe'
3
+ import { Input, Model, Value } from '@contember/schema'
4
+
5
+ export const conditionSchema = <T extends Value.FieldValue>(type?: Model.ColumnType): Type<Input.Condition<T>> => {
6
+ const metadata = type ? resolveColumnMetadata(type) : { type: Typesafe.scalar }
7
+ return conditionSchemaInner(metadata)
8
+ }
9
+
10
+ interface ResolvedColumnMetadata<T extends Json> {
11
+ type: Typesafe.Type<T>
12
+ isJson?: boolean
13
+ isString?: boolean
14
+ }
15
+
16
+ const conditionSchemaInner = <T extends Json>(metadata: ResolvedColumnMetadata<T>): Type<Input.Condition<T>> => {
17
+ return (input: unknown, path: PropertyKey[] = []): Input.Condition<T> => {
18
+ const self = conditionSchemaInner(metadata)
19
+ const inner = metadata.type
20
+ const objectSchema = Typesafe.noExtraProps(Typesafe.partial({
21
+ and: Typesafe.array(self),
22
+ or: Typesafe.array(self),
23
+ not: self,
24
+ null: Typesafe.boolean,
25
+ isNull: Typesafe.boolean,
26
+ never: Typesafe.true_,
27
+ always: Typesafe.true_,
28
+ ...(metadata.isJson !== true ? {
29
+ eq: inner,
30
+ notEq: inner,
31
+ in: Typesafe.array(inner),
32
+ notIn: Typesafe.array(inner),
33
+ lt: inner,
34
+ lte: inner,
35
+ gt: inner,
36
+ gte: inner,
37
+ } : {}),
38
+ ...(metadata.isString !== false ? {
39
+ contains: Typesafe.string,
40
+ startsWith: Typesafe.string,
41
+ endsWith: Typesafe.string,
42
+ containsCI: Typesafe.string,
43
+ startsWithCI: Typesafe.string,
44
+ endsWithCI: Typesafe.string,
45
+ } : {}),
46
+ }))
47
+ return objectSchema(input, path) as Input.Condition<T>
48
+ }
49
+ }
50
+
51
+ const resolveColumnMetadata = (type: Model.ColumnType): ResolvedColumnMetadata<any> => {
52
+ const resolvedType = Typesafe.nullable(resolveColumnTypeSchema(type))
53
+ return {
54
+ type: resolvedType,
55
+ isJson: type === Model.ColumnType.Json,
56
+ isString: type === Model.ColumnType.String,
57
+
58
+ }
59
+ }
60
+
61
+
62
+ const resolveColumnTypeSchema = (type: Model.ColumnType) => {
63
+ switch (type) {
64
+ case Model.ColumnType.Bool:
65
+ return Typesafe.boolean
66
+
67
+ // todo: validate format of string-like inputs uuid, date, enum
68
+ case Model.ColumnType.Date:
69
+ case Model.ColumnType.DateTime:
70
+ case Model.ColumnType.Uuid:
71
+ case Model.ColumnType.Enum:
72
+ case Model.ColumnType.String:
73
+ return Typesafe.string
74
+ case Model.ColumnType.Double:
75
+ return Typesafe.number
76
+ case Model.ColumnType.Int:
77
+ return Typesafe.integer
78
+ case Model.ColumnType.Json:
79
+ return Typesafe.anyJson
80
+ }
81
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './acl'
2
+ export * from './condition'
2
3
  export * from './model'
3
4
  export * from './validation'
@@ -147,6 +147,12 @@ const viewSchemaInner = Typesafe.intersection(
147
147
  const viewSchemaCheck: Typesafe.Equals<Model.View, ReturnType<typeof viewSchemaInner>> = true
148
148
  const viewSchema: Typesafe.Type<Model.View> = viewSchemaInner
149
149
 
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
+ })), { })
155
+
150
156
  const entitySchema = Typesafe.intersection(
151
157
  Typesafe.object({
152
158
  name: Typesafe.string,
@@ -158,6 +164,7 @@ const entitySchema = Typesafe.intersection(
158
164
  fields: Typesafe.array(Typesafe.string),
159
165
  name: Typesafe.string,
160
166
  })),
167
+ indexes: indexesSchema,
161
168
  eventLog: eventLogSchema,
162
169
  }),
163
170
  Typesafe.partial({
@@ -2,6 +2,8 @@ import { Acl, Model } from '@contember/schema'
2
2
  import { PredicateDefinitionProcessor } from '../acl'
3
3
  import { getEntity } from '../model'
4
4
  import { ErrorBuilder, ValidationError } from './errors'
5
+ import { conditionSchema } from '../type-schema/condition'
6
+ import { anyJson } from '@contember/typesafe'
5
7
 
6
8
 
7
9
  export class AclValidator {
@@ -128,8 +130,16 @@ export class AclValidator {
128
130
  const processor = new PredicateDefinitionProcessor(this.model)
129
131
  processor.process(entity, predicate, {
130
132
  handleColumn: ctx => {
131
- if (typeof ctx.value === 'string' && !variables[ctx.value]) {
132
- errorBuilder.for(...ctx.path).add(`Undefined variable ${ctx.value}`)
133
+ if (typeof ctx.value === 'string') {
134
+ if (!variables[ctx.value]) {
135
+ errorBuilder.for(...ctx.path).add(`Undefined variable ${ctx.value}`)
136
+ }
137
+ } else {
138
+ try {
139
+ conditionSchema(ctx.column.type)(ctx.value)
140
+ } catch (e: any) {
141
+ errorBuilder.for(...ctx.path).add(`Invalid condition (${e.message}): ${JSON.stringify(ctx.value)}`)
142
+ }
133
143
  }
134
144
  return ctx.value
135
145
  },
@@ -0,0 +1,133 @@
1
+ import { Acl, Input } from '@contember/schema'
2
+ import { getRoleVariables } from '../acl'
3
+ import { conditionSchema } from '../type-schema'
4
+
5
+
6
+ export class MembershipResolver {
7
+ public static UnknownIdentity = { identityId: '00000000-0000-0000-0000-000000000000' }
8
+
9
+ resolve(
10
+ acl: Acl.Schema,
11
+ memberships: readonly Acl.Membership[],
12
+ identity: {
13
+ identityId: string
14
+ personId?: string
15
+ },
16
+ ): MembershipReadResult {
17
+ const errors: MembershipValidationError[] = []
18
+ const parsed: ParsedMembership[] = []
19
+ const roles = acl.roles
20
+ for (const membership of memberships) {
21
+ if (!roles[membership.role]) {
22
+ errors.push(new MembershipValidationError(membership.role, MembershipValidationErrorType.ROLE_NOT_FOUND))
23
+ continue
24
+ }
25
+ const roleVariables = getRoleVariables(membership.role, acl)
26
+ for (const variable of membership.variables) {
27
+ if (!roleVariables[variable.name] || roleVariables[variable.name].type === Acl.VariableType.predefined) {
28
+ errors.push(
29
+ new MembershipValidationError(
30
+ membership.role,
31
+ MembershipValidationErrorType.VARIABLE_NOT_FOUND,
32
+ variable.name,
33
+ ),
34
+ )
35
+ }
36
+ }
37
+ const [membershipErrors, parsedVariables] = this.readMembershipVariables(roleVariables, membership, identity)
38
+ errors.push(...membershipErrors)
39
+ parsed.push({
40
+ role: membership.role,
41
+ variables: parsedVariables,
42
+ })
43
+ }
44
+ return new MembershipReadResult(errors, parsed)
45
+ }
46
+
47
+ private readMembershipVariables(roleVariables: Acl.Variables, membership: Acl.Membership, identity: {
48
+ identityId: string
49
+ personId?: string
50
+ }): [MembershipValidationError[], ParsedMembershipVariable[]] {
51
+ const errors: MembershipValidationError[] = []
52
+ const parsedVariables: ParsedMembershipVariable[] = []
53
+ for (const [name, variable] of Object.entries(roleVariables)) {
54
+ const inputVariable = membership.variables.find(it => it.name === name)
55
+ if (variable.type === Acl.VariableType.predefined) {
56
+ switch (variable.value) {
57
+ case 'identityID':
58
+ parsedVariables.push({ name, condition: { in: [identity.identityId] } })
59
+ break
60
+ case 'personID':
61
+ parsedVariables.push({
62
+ name,
63
+ condition: { in: identity.personId ? [identity.personId] : [] },
64
+ })
65
+ break
66
+ default:
67
+ parsedVariables.push({ name, condition: { never: true } })
68
+ errors.push(new MembershipValidationError(membership.role, MembershipValidationErrorType.VARIABLE_INVALID, name))
69
+ break
70
+ }
71
+ continue
72
+ }
73
+
74
+ if (!inputVariable) {
75
+ errors.push(new MembershipValidationError(membership.role, MembershipValidationErrorType.VARIABLE_EMPTY, name))
76
+ parsedVariables.push({ name, condition: { never: true } })
77
+ continue
78
+ }
79
+
80
+ if (variable.type === Acl.VariableType.entity) {
81
+ parsedVariables.push({ name, condition: { in: inputVariable.values } }) // todo: cast to int where primary is int
82
+
83
+ } else if (variable.type === Acl.VariableType.condition) {
84
+ try {
85
+ const conditions = inputVariable.values.map(it => conditionSchema()(JSON.parse(it)))
86
+ parsedVariables.push({
87
+ name,
88
+ condition: conditions.length === 1 ? conditions[0] : { or: conditions },
89
+ })
90
+ } catch {
91
+ errors.push(new MembershipValidationError(membership.role, MembershipValidationErrorType.VARIABLE_INVALID, name))
92
+ parsedVariables.push({ name, condition: { never: true } })
93
+ }
94
+ }
95
+ }
96
+ return [errors, parsedVariables]
97
+ }
98
+ }
99
+
100
+ export class MembershipReadResult {
101
+ constructor(
102
+ public readonly errors: MembershipValidationError[],
103
+ public readonly memberships: ParsedMembership[],
104
+ ) {
105
+ }
106
+ }
107
+
108
+ export class MembershipValidationError {
109
+ constructor(
110
+ public readonly role: string,
111
+ public readonly error: MembershipValidationErrorType,
112
+ public readonly variable?: string,
113
+ ) {
114
+ }
115
+ }
116
+
117
+ export enum MembershipValidationErrorType {
118
+ ROLE_NOT_FOUND = 'roleNotFound',
119
+ VARIABLE_NOT_FOUND = 'variableNotFound',
120
+ VARIABLE_EMPTY = 'variableEmpty',
121
+ VARIABLE_INVALID = 'variableInvalid',
122
+ }
123
+
124
+
125
+ export interface ParsedMembershipVariable {
126
+ readonly name: string
127
+ readonly condition: Input.Condition
128
+ }
129
+
130
+ export interface ParsedMembership {
131
+ readonly role: string
132
+ readonly variables: readonly ParsedMembershipVariable[]
133
+ }