@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.
- package/dist/src/acl/PredicateDefinitionProcessor.d.ts.map +1 -1
- package/dist/src/acl/PredicateDefinitionProcessor.js +3 -2
- package/dist/src/acl/PredicateDefinitionProcessor.js.map +1 -1
- package/dist/src/model/NamingHelper.d.ts +2 -0
- package/dist/src/model/NamingHelper.d.ts.map +1 -1
- package/dist/src/model/NamingHelper.js +20 -7
- package/dist/src/model/NamingHelper.js.map +1 -1
- package/dist/src/model/modelUtils.d.ts +1 -0
- package/dist/src/model/modelUtils.d.ts.map +1 -1
- package/dist/src/model/modelUtils.js +69 -46
- package/dist/src/model/modelUtils.js.map +1 -1
- package/dist/src/schemaNormalizer.d.ts.map +1 -1
- package/dist/src/schemaNormalizer.js +15 -4
- package/dist/src/schemaNormalizer.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/type-schema/acl.d.ts.map +1 -1
- package/dist/src/type-schema/acl.js +15 -3
- package/dist/src/type-schema/acl.js.map +1 -1
- package/dist/src/type-schema/condition.d.ts +5 -0
- package/dist/src/type-schema/condition.d.ts.map +1 -0
- package/dist/src/type-schema/condition.js +95 -0
- package/dist/src/type-schema/condition.js.map +1 -0
- package/dist/src/type-schema/index.d.ts +1 -0
- package/dist/src/type-schema/index.d.ts.map +1 -1
- package/dist/src/type-schema/index.js +1 -0
- package/dist/src/type-schema/index.js.map +1 -1
- package/dist/src/type-schema/model.d.ts +1 -0
- package/dist/src/type-schema/model.d.ts.map +1 -1
- package/dist/src/type-schema/model.js +5 -0
- package/dist/src/type-schema/model.js.map +1 -1
- package/dist/src/validation/AclValidator.d.ts.map +1 -1
- package/dist/src/validation/AclValidator.js +13 -2
- package/dist/src/validation/AclValidator.js.map +1 -1
- package/dist/src/validation/MembershipResolver.d.ts +37 -0
- package/dist/src/validation/MembershipResolver.d.ts.map +1 -0
- package/dist/src/validation/MembershipResolver.js +104 -0
- package/dist/src/validation/MembershipResolver.js.map +1 -0
- package/dist/src/validation/ModelValidator.d.ts.map +1 -1
- package/dist/src/validation/ModelValidator.js +13 -6
- package/dist/src/validation/ModelValidator.js.map +1 -1
- package/dist/src/validation/index.d.ts +1 -0
- package/dist/src/validation/index.d.ts.map +1 -1
- package/dist/src/validation/index.js +1 -0
- package/dist/src/validation/index.js.map +1 -1
- package/dist/tests/cases/unit/conditionSchema.test.d.ts +2 -0
- package/dist/tests/cases/unit/conditionSchema.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/conditionSchema.test.js +50 -0
- package/dist/tests/cases/unit/conditionSchema.test.js.map +1 -0
- package/dist/tests/cases/unit/membershipResolver.test.d.ts +2 -0
- package/dist/tests/cases/unit/membershipResolver.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/membershipResolver.test.js +149 -0
- package/dist/tests/cases/unit/membershipResolver.test.js.map +1 -0
- package/dist/tests/tsconfig.tsbuildinfo +1 -0
- package/package.json +5 -5
- package/src/acl/PredicateDefinitionProcessor.ts +3 -2
- package/src/model/NamingHelper.ts +19 -8
- package/src/model/modelUtils.ts +76 -71
- package/src/schemaNormalizer.ts +15 -4
- package/src/type-schema/acl.ts +30 -9
- package/src/type-schema/condition.ts +81 -0
- package/src/type-schema/index.ts +1 -0
- package/src/type-schema/model.ts +7 -0
- package/src/validation/AclValidator.ts +12 -2
- package/src/validation/MembershipResolver.ts +133 -0
- package/src/validation/ModelValidator.ts +15 -6
- package/src/validation/index.ts +1 -0
- package/tests/cases/unit/conditionSchema.test.ts +55 -0
- package/tests/cases/unit/membershipResolver.test.ts +161 -0
- package/tests/tsconfig.json +12 -0
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.
|
|
5
|
+
return 'fk_'
|
|
6
|
+
+ fromTable + '_'
|
|
7
|
+
+ fromColumn + '_'
|
|
8
|
+
+ this.createUniqueSuffix([fromTable, fromColumn, toTable, toColumn])
|
|
9
|
+
}
|
|
9
10
|
|
|
10
|
-
|
|
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(
|
|
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
|
}
|
package/src/model/modelUtils.ts
CHANGED
|
@@ -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: (
|
|
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: (
|
|
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: (
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
161
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
}
|
package/src/schemaNormalizer.ts
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
},
|
package/src/type-schema/acl.ts
CHANGED
|
@@ -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:
|
|
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
|
+
}
|
package/src/type-schema/index.ts
CHANGED
package/src/type-schema/model.ts
CHANGED
|
@@ -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'
|
|
132
|
-
|
|
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
|
+
}
|