@contember/engine-content-api 2.0.0-beta.2 → 2.0.0-beta.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 (47) hide show
  1. package/dist/development/src/acl/Authorizator.cjs +3 -0
  2. package/dist/development/src/acl/Authorizator.cjs.map +1 -1
  3. package/dist/development/src/acl/Authorizator.js +3 -0
  4. package/dist/development/src/acl/Authorizator.js.map +1 -1
  5. package/dist/development/src/acl/PermissionFactory.cjs +11 -2
  6. package/dist/development/src/acl/PermissionFactory.cjs.map +1 -1
  7. package/dist/development/src/acl/PermissionFactory.js +11 -2
  8. package/dist/development/src/acl/PermissionFactory.js.map +1 -1
  9. package/dist/development/src/schema/MutationProvider.cjs +12 -0
  10. package/dist/development/src/schema/MutationProvider.cjs.map +1 -1
  11. package/dist/development/src/schema/MutationProvider.js +12 -0
  12. package/dist/development/src/schema/MutationProvider.js.map +1 -1
  13. package/dist/development/src/schema/QueryProvider.cjs +3 -0
  14. package/dist/development/src/schema/QueryProvider.cjs.map +1 -1
  15. package/dist/development/src/schema/QueryProvider.js +3 -0
  16. package/dist/development/src/schema/QueryProvider.js.map +1 -1
  17. package/dist/production/src/acl/Authorizator.cjs +3 -0
  18. package/dist/production/src/acl/Authorizator.cjs.map +1 -1
  19. package/dist/production/src/acl/Authorizator.js +3 -0
  20. package/dist/production/src/acl/Authorizator.js.map +1 -1
  21. package/dist/production/src/acl/PermissionFactory.cjs +11 -2
  22. package/dist/production/src/acl/PermissionFactory.cjs.map +1 -1
  23. package/dist/production/src/acl/PermissionFactory.js +11 -2
  24. package/dist/production/src/acl/PermissionFactory.js.map +1 -1
  25. package/dist/production/src/schema/MutationProvider.cjs +12 -0
  26. package/dist/production/src/schema/MutationProvider.cjs.map +1 -1
  27. package/dist/production/src/schema/MutationProvider.js +12 -0
  28. package/dist/production/src/schema/MutationProvider.js.map +1 -1
  29. package/dist/production/src/schema/QueryProvider.cjs +3 -0
  30. package/dist/production/src/schema/QueryProvider.cjs.map +1 -1
  31. package/dist/production/src/schema/QueryProvider.js +3 -0
  32. package/dist/production/src/schema/QueryProvider.js.map +1 -1
  33. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  34. package/dist/types/acl/Authorizator.d.ts +1 -0
  35. package/dist/types/acl/Authorizator.d.ts.map +1 -1
  36. package/dist/types/acl/PermissionFactory.d.ts.map +1 -1
  37. package/dist/types/schema/MutationProvider.d.ts.map +1 -1
  38. package/dist/types/schema/QueryProvider.d.ts.map +1 -1
  39. package/dist/types/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +10 -10
  41. package/src/acl/Authorizator.ts +4 -0
  42. package/src/acl/PermissionFactory.ts +12 -2
  43. package/src/schema/MutationProvider.ts +13 -0
  44. package/src/schema/QueryProvider.ts +3 -0
  45. package/tests/cases/integration/graphQlSchema/graphQlSchemaBuilder.test.ts +43 -1
  46. package/tests/cases/integration/graphQlSchema/schema-no-root-ops.gql +308 -0
  47. package/tests/cases/unit/permissionMerger.test.ts +120 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contember/engine-content-api",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.4",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/production/index.js",
6
6
  "typings": "./dist/types/index.d.ts",
@@ -26,18 +26,18 @@
26
26
  "generate": "gql-gen --config graphql.codegen.yml"
27
27
  },
28
28
  "dependencies": {
29
- "@contember/database": "2.0.0-beta.2",
30
- "@contember/dic": "2.0.0-beta.2",
31
- "@contember/graphql-utils": "2.0.0-beta.2",
32
- "@contember/logger": "2.0.0-beta.2",
33
- "@contember/schema": "2.0.0-beta.2",
34
- "@contember/schema-definition": "2.0.0-beta.2",
35
- "@contember/schema-utils": "2.0.0-beta.2",
29
+ "@contember/database": "2.0.0-beta.4",
30
+ "@contember/dic": "2.0.0-beta.4",
31
+ "@contember/graphql-utils": "2.0.0-beta.4",
32
+ "@contember/logger": "2.0.0-beta.4",
33
+ "@contember/schema": "2.0.0-beta.4",
34
+ "@contember/schema-definition": "2.0.0-beta.4",
35
+ "@contember/schema-utils": "2.0.0-beta.4",
36
36
  "fast-deep-equal": "^3.1.3"
37
37
  },
38
38
  "devDependencies": {
39
- "@contember/database-tester": "2.0.0-beta.2",
40
- "@contember/schema-definition": "2.0.0-beta.2",
39
+ "@contember/database-tester": "2.0.0-beta.4",
40
+ "@contember/schema-definition": "2.0.0-beta.4",
41
41
  "@types/node": "^20.16.11",
42
42
  "graphql": "^16.9.0",
43
43
  "pg": "^8.12.0"
@@ -52,4 +52,8 @@ export class Authorizator {
52
52
  isCustomPrimaryAllowed(entity: string): boolean {
53
53
  return this.permissions?.[entity]?.operations?.customPrimary ?? this.defaultCustomPrimary
54
54
  }
55
+
56
+ isRootOperationDisallowed(entity: string, operation: Acl.Operation): boolean {
57
+ return this.permissions?.[entity]?.operations?.noRoot?.includes(operation) ?? false
58
+ }
55
59
  }
@@ -116,6 +116,7 @@ export class PermissionFactory {
116
116
  if (left.operations.customPrimary || right.operations.customPrimary) {
117
117
  operations.customPrimary = true
118
118
  }
119
+ const noRoot: `${Acl.Operation}`[] = []
119
120
 
120
121
  const operationNames: (keyof Pick<Acl.EntityOperations, 'create' | 'read' | 'update'>)[] = [
121
122
  'create',
@@ -124,8 +125,14 @@ export class PermissionFactory {
124
125
  ]
125
126
 
126
127
  for (let operation of operationNames) {
127
- const leftFieldPermissions: Acl.FieldPermissions = left.operations[operation] || {}
128
- const rightFieldPermissions: Acl.FieldPermissions = right.operations[operation] || {}
128
+ const leftNoRoot = left.operations.noRoot?.includes(operation) || false
129
+ const rightNoRoot = right.operations.noRoot?.includes(operation) || false
130
+ if (leftNoRoot && rightNoRoot) {
131
+ noRoot.push(operation)
132
+ }
133
+
134
+ const leftFieldPermissions: Acl.FieldPermissions = leftNoRoot && !rightNoRoot ? {} : (left.operations[operation] || {})
135
+ const rightFieldPermissions: Acl.FieldPermissions = rightNoRoot && !leftNoRoot ? {} : (right.operations[operation] || {})
129
136
  const [operationPredicates, fieldPermissions] = this.mergeFieldPermissions(
130
137
  left.predicates,
131
138
  leftFieldPermissions,
@@ -150,6 +157,9 @@ export class PermissionFactory {
150
157
  predicates[predicate] = predicateDefinition
151
158
  operations.delete = predicate
152
159
  }
160
+ if (noRoot.length > 0) {
161
+ operations.noRoot = noRoot
162
+ }
153
163
 
154
164
  return {
155
165
  predicates: predicates,
@@ -41,6 +41,9 @@ export class MutationProvider {
41
41
  }
42
42
 
43
43
  protected getCreateMutation(entity: Model.Entity): FieldConfig<Input.CreateInput> | undefined {
44
+ if (this.authorizator.isRootOperationDisallowed(entity.name, Acl.Operation.create)) {
45
+ return undefined
46
+ }
44
47
  const entityName = entity.name
45
48
  const dataType = this.createEntityInputProvider.getInput(entityName)
46
49
  if (dataType === undefined) {
@@ -66,6 +69,9 @@ export class MutationProvider {
66
69
 
67
70
  protected getDeleteMutation(entity: Model.Entity): FieldConfig<Input.DeleteInput> | undefined {
68
71
  const entityName = entity.name
72
+ if (this.authorizator.isRootOperationDisallowed(entityName, Acl.Operation.delete)) {
73
+ return undefined
74
+ }
69
75
  if (entity.view) {
70
76
  return undefined
71
77
  }
@@ -99,6 +105,9 @@ export class MutationProvider {
99
105
  }
100
106
 
101
107
  protected getUpdateMutation(entity: Model.Entity): FieldConfig<Input.UpdateInput> | undefined {
108
+ if (this.authorizator.isRootOperationDisallowed(entity.name, Acl.Operation.update)) {
109
+ return undefined
110
+ }
102
111
  const entityName = entity.name
103
112
  const dataType = this.updateEntityInputProvider.getInput(entityName)
104
113
  if (dataType === undefined) {
@@ -133,6 +142,10 @@ export class MutationProvider {
133
142
  }
134
143
 
135
144
  private getUpsertMutation(entity: Model.Entity): FieldConfig<Input.UpsertInput> | undefined {
145
+ if (this.authorizator.isRootOperationDisallowed(entity.name, Acl.Operation.update)
146
+ || this.authorizator.isRootOperationDisallowed(entity.name, Acl.Operation.create)) {
147
+ return undefined
148
+ }
136
149
  const entityName = entity.name
137
150
  const createInput = this.createEntityInputProvider.getInput(entityName)
138
151
  if (createInput === undefined) {
@@ -22,6 +22,9 @@ export class QueryProvider {
22
22
  if (this.authorizator.getEntityPermission(Acl.Operation.read, entity.name) === 'no') {
23
23
  return {}
24
24
  }
25
+ if (this.authorizator.isRootOperationDisallowed(entity.name, Acl.Operation.read)) {
26
+ return {}
27
+ }
25
28
  return {
26
29
  ['get' + entity.name]: this.getByUniqueQuery(entity),
27
30
  ['list' + entity.name]: this.getListQuery(entity),
@@ -6,9 +6,11 @@ import {
6
6
  AllowAllPermissionFactory,
7
7
  SchemaBuilder,
8
8
  SchemaDefinition,
9
+ c,
10
+ createSchema,
9
11
  SchemaDefinition as def,
10
12
  } from '@contember/schema-definition'
11
- import { Authorizator, GraphQlSchemaBuilderFactory } from '../../../../src'
13
+ import { Authorizator, GraphQlSchemaBuilderFactory, PermissionFactory } from '../../../../src'
12
14
  import * as model from './model'
13
15
  import { expect, describe, it } from 'bun:test'
14
16
  import { dirname } from 'node:path'
@@ -387,6 +389,20 @@ describe('GraphQL schema builder', () => {
387
389
  graphQlSchemaFile: 'schema-view-entity.gql',
388
390
  })
389
391
  })
392
+
393
+ it.only('no root ops', async () => {
394
+ const schema = createSchema(NoRootOperation)
395
+ console.log(schema.acl.roles.editor.entities.Image)
396
+
397
+ await testSchema({
398
+ schema: () => schema.model,
399
+ permissions: () => {
400
+ const factory = new PermissionFactory()
401
+ return factory.create(schema, ['editor'])
402
+ },
403
+ graphQlSchemaFile: 'schema-no-root-ops.gql',
404
+ })
405
+ })
390
406
  })
391
407
 
392
408
  namespace ViewEntity {
@@ -395,3 +411,29 @@ namespace ViewEntity {
395
411
  name = def.stringColumn()
396
412
  }
397
413
  }
414
+
415
+ namespace NoRootOperation {
416
+ export const editorRole = c.createRole('editor')
417
+
418
+ @c.Allow(editorRole, {
419
+ read: true,
420
+ create: true,
421
+ delete: true,
422
+ update: true,
423
+ })
424
+ export class Article {
425
+ title = c.stringColumn()
426
+ coverImage = c.oneHasOne(Image)
427
+ }
428
+
429
+ @c.Allow(editorRole, {
430
+ through: true,
431
+ read: true,
432
+ create: true,
433
+ delete: true,
434
+ update: true,
435
+ })
436
+ export class Image {
437
+ url = c.stringColumn()
438
+ }
439
+ }
@@ -0,0 +1,308 @@
1
+ type Query {
2
+ getArticle(by: ArticleUniqueWhere!, filter: ArticleWhere): Article
3
+ listArticle(filter: ArticleWhere, orderBy: [ArticleOrderBy!], offset: Int, limit: Int): [Article!]!
4
+ paginateArticle(filter: ArticleWhere, orderBy: [ArticleOrderBy!], skip: Int, first: Int): ArticleConnection!
5
+ validateCreateArticle(data: ArticleCreateInput!): _ValidationResult!
6
+ validateUpdateArticle(by: ArticleUniqueWhere!, data: ArticleUpdateInput!): _ValidationResult!
7
+ validateCreateImage(data: ImageCreateInput!): _ValidationResult!
8
+ validateUpdateImage(by: ImageUniqueWhere!, data: ImageUpdateInput!): _ValidationResult!
9
+ transaction: QueryTransaction
10
+ _info: Info
11
+ }
12
+
13
+ type Article {
14
+ _meta: ArticleMeta
15
+ id: UUID!
16
+ title: String
17
+ coverImage(filter: ImageWhere): Image
18
+ }
19
+
20
+ type ArticleMeta {
21
+ id: FieldMeta
22
+ title: FieldMeta
23
+ coverImage: FieldMeta
24
+ }
25
+
26
+ type FieldMeta {
27
+ readable: Boolean
28
+ updatable: Boolean
29
+ }
30
+
31
+ scalar UUID
32
+
33
+ type Image {
34
+ _meta: ImageMeta
35
+ id: UUID!
36
+ url: String
37
+ }
38
+
39
+ type ImageMeta {
40
+ id: FieldMeta
41
+ url: FieldMeta
42
+ }
43
+
44
+ input ImageWhere {
45
+ id: UUIDCondition
46
+ url: StringCondition
47
+ and: [ImageWhere]
48
+ or: [ImageWhere]
49
+ not: ImageWhere
50
+ }
51
+
52
+ input UUIDCondition {
53
+ and: [UUIDCondition!]
54
+ or: [UUIDCondition!]
55
+ not: UUIDCondition
56
+ null: Boolean
57
+ isNull: Boolean
58
+ eq: UUID
59
+ notEq: UUID
60
+ in: [UUID!]
61
+ notIn: [UUID!]
62
+ lt: UUID
63
+ lte: UUID
64
+ gt: UUID
65
+ gte: UUID
66
+ }
67
+
68
+ input StringCondition {
69
+ and: [StringCondition!]
70
+ or: [StringCondition!]
71
+ not: StringCondition
72
+ null: Boolean
73
+ isNull: Boolean
74
+ eq: String
75
+ notEq: String
76
+ in: [String!]
77
+ notIn: [String!]
78
+ lt: String
79
+ lte: String
80
+ gt: String
81
+ gte: String
82
+ contains: String
83
+ startsWith: String
84
+ endsWith: String
85
+ containsCI: String
86
+ startsWithCI: String
87
+ endsWithCI: String
88
+ }
89
+
90
+ input ArticleUniqueWhere {
91
+ id: UUID
92
+ coverImage: ImageUniqueWhere
93
+ }
94
+
95
+ input ImageUniqueWhere {
96
+ id: UUID
97
+ }
98
+
99
+ input ArticleWhere {
100
+ id: UUIDCondition
101
+ title: StringCondition
102
+ coverImage: ImageWhere
103
+ and: [ArticleWhere]
104
+ or: [ArticleWhere]
105
+ not: ArticleWhere
106
+ }
107
+
108
+ input ArticleOrderBy {
109
+ _random: Boolean
110
+ _randomSeeded: Int
111
+ id: OrderDirection
112
+ title: OrderDirection
113
+ coverImage: ImageOrderBy
114
+ }
115
+
116
+ enum OrderDirection {
117
+ asc
118
+ desc
119
+ ascNullsFirst
120
+ descNullsLast
121
+ }
122
+
123
+ input ImageOrderBy {
124
+ _random: Boolean
125
+ _randomSeeded: Int
126
+ id: OrderDirection
127
+ url: OrderDirection
128
+ }
129
+
130
+ type ArticleConnection {
131
+ pageInfo: PageInfo!
132
+ edges: [ArticleEdge!]!
133
+ }
134
+
135
+ type PageInfo {
136
+ totalCount: Int!
137
+ }
138
+
139
+ type ArticleEdge {
140
+ node: Article!
141
+ }
142
+
143
+ type _ValidationResult {
144
+ valid: Boolean!
145
+ errors: [_ValidationError!]!
146
+ }
147
+
148
+ type _ValidationError {
149
+ path: [_PathFragment!]!
150
+ message: _ValidationMessage!
151
+ }
152
+
153
+ union _PathFragment = _FieldPathFragment | _IndexPathFragment
154
+
155
+ type _FieldPathFragment {
156
+ field: String!
157
+ }
158
+
159
+ type _IndexPathFragment {
160
+ index: Int!
161
+ alias: String
162
+ }
163
+
164
+ type _ValidationMessage {
165
+ text: String!
166
+ }
167
+
168
+ input ArticleCreateInput {
169
+ title: String
170
+ coverImage: ArticleCreateCoverImageEntityRelationInput
171
+ _dummy_field_: Boolean
172
+ }
173
+
174
+ input ArticleCreateCoverImageEntityRelationInput {
175
+ connect: ImageUniqueWhere
176
+ create: ImageCreateInput
177
+ connectOrCreate: ArticleConnectOrCreateCoverImageRelationInput
178
+ }
179
+
180
+ input ImageCreateInput {
181
+ url: String
182
+ _dummy_field_: Boolean
183
+ }
184
+
185
+ input ArticleConnectOrCreateCoverImageRelationInput {
186
+ connect: ImageUniqueWhere!
187
+ create: ImageCreateInput!
188
+ }
189
+
190
+ input ArticleUpdateInput {
191
+ title: String
192
+ coverImage: ArticleUpdateCoverImageEntityRelationInput
193
+ _dummy_field_: Boolean
194
+ }
195
+
196
+ input ArticleUpdateCoverImageEntityRelationInput {
197
+ connect: ImageUniqueWhere
198
+ create: ImageCreateInput
199
+ connectOrCreate: ArticleConnectOrCreateCoverImageRelationInput
200
+ update: ImageUpdateInput
201
+ upsert: ArticleUpsertCoverImageRelationInput
202
+ disconnect: Boolean
203
+ delete: Boolean
204
+ }
205
+
206
+ input ImageUpdateInput {
207
+ url: String
208
+ _dummy_field_: Boolean
209
+ }
210
+
211
+ input ArticleUpsertCoverImageRelationInput {
212
+ update: ImageUpdateInput!
213
+ create: ImageCreateInput!
214
+ }
215
+
216
+ type QueryTransaction {
217
+ getArticle(by: ArticleUniqueWhere!, filter: ArticleWhere): Article
218
+ listArticle(filter: ArticleWhere, orderBy: [ArticleOrderBy!], offset: Int, limit: Int): [Article!]!
219
+ paginateArticle(filter: ArticleWhere, orderBy: [ArticleOrderBy!], skip: Int, first: Int): ArticleConnection!
220
+ validateCreateArticle(data: ArticleCreateInput!): _ValidationResult!
221
+ validateUpdateArticle(by: ArticleUniqueWhere!, data: ArticleUpdateInput!): _ValidationResult!
222
+ validateCreateImage(data: ImageCreateInput!): _ValidationResult!
223
+ validateUpdateImage(by: ImageUniqueWhere!, data: ImageUpdateInput!): _ValidationResult!
224
+ }
225
+
226
+ type Info {
227
+ description: String
228
+ }
229
+
230
+ type Mutation {
231
+ createArticle(data: ArticleCreateInput!): ArticleCreateResult!
232
+ deleteArticle(by: ArticleUniqueWhere!, filter: ArticleWhere): ArticleDeleteResult!
233
+ updateArticle(by: ArticleUniqueWhere!, filter: ArticleWhere, data: ArticleUpdateInput!): ArticleUpdateResult!
234
+ upsertArticle(by: ArticleUniqueWhere!, filter: ArticleWhere, update: ArticleUpdateInput!, create: ArticleCreateInput!): ArticleUpsertResult!
235
+ transaction(options: MutationTransactionOptions): MutationTransaction!
236
+ query: Query!
237
+ }
238
+
239
+ type ArticleCreateResult implements MutationResult {
240
+ ok: Boolean!
241
+ errorMessage: String
242
+ errors: [_MutationError!]!
243
+ node: Article
244
+ validation: _ValidationResult!
245
+ }
246
+
247
+ interface MutationResult {
248
+ ok: Boolean!
249
+ errorMessage: String
250
+ errors: [_MutationError!]!
251
+ }
252
+
253
+ type _MutationError {
254
+ path: [_PathFragment!]! @deprecated(reason: "Use `paths`.")
255
+ paths: [[_PathFragment!]!]!
256
+ type: _MutationErrorType!
257
+ message: String
258
+ }
259
+
260
+ enum _MutationErrorType {
261
+ NotNullConstraintViolation
262
+ UniqueConstraintViolation
263
+ ForeignKeyConstraintViolation
264
+ NotFoundOrDenied
265
+ NonUniqueWhereInput
266
+ InvalidDataInput
267
+ SqlError
268
+ }
269
+
270
+ type ArticleDeleteResult implements MutationResult {
271
+ ok: Boolean!
272
+ errorMessage: String
273
+ errors: [_MutationError!]!
274
+ node: Article
275
+ }
276
+
277
+ type ArticleUpdateResult implements MutationResult {
278
+ ok: Boolean!
279
+ errorMessage: String
280
+ errors: [_MutationError!]!
281
+ node: Article
282
+ validation: _ValidationResult!
283
+ }
284
+
285
+ type ArticleUpsertResult implements MutationResult {
286
+ ok: Boolean!
287
+ errorMessage: String
288
+ errors: [_MutationError!]!
289
+ node: Article
290
+ validation: _ValidationResult!
291
+ }
292
+
293
+ type MutationTransaction {
294
+ ok: Boolean!
295
+ errorMessage: String
296
+ errors: [_MutationError!]!
297
+ validation: _ValidationResult!
298
+ createArticle(data: ArticleCreateInput!): ArticleCreateResult!
299
+ deleteArticle(by: ArticleUniqueWhere!, filter: ArticleWhere): ArticleDeleteResult!
300
+ updateArticle(by: ArticleUniqueWhere!, filter: ArticleWhere, data: ArticleUpdateInput!): ArticleUpdateResult!
301
+ upsertArticle(by: ArticleUniqueWhere!, filter: ArticleWhere, update: ArticleUpdateInput!, create: ArticleCreateInput!): ArticleUpsertResult!
302
+ query: Query
303
+ }
304
+
305
+ input MutationTransactionOptions {
306
+ deferForeignKeyConstraints: Boolean
307
+ deferUniqueConstraints: Boolean
308
+ }
@@ -550,4 +550,124 @@ describe('Permission merger', () => {
550
550
  })
551
551
 
552
552
 
553
+
554
+ it('prefer permissions with root allowed', () => {
555
+ execute({
556
+ acl: {
557
+ roles: {
558
+ role1: {
559
+ variables: {},
560
+ entities: {
561
+ Entity2: {
562
+ predicates: {
563
+ foo: { xyz: { lorem: { eq: 'foo' } } },
564
+ },
565
+ operations: {
566
+ read: {
567
+ title: 'foo',
568
+ },
569
+ noRoot: ['read'],
570
+ },
571
+ },
572
+ },
573
+ },
574
+ role2: {
575
+ variables: {},
576
+ entities: {
577
+ Entity2: {
578
+ predicates: {
579
+ foo: { xyz: { lorem: { eq: 'bar' } } },
580
+ },
581
+ operations: {
582
+ read: {
583
+ title: 'foo',
584
+ },
585
+ },
586
+ },
587
+ },
588
+ },
589
+ },
590
+ },
591
+ roles: ['role1', 'role2'],
592
+ result: {
593
+ Entity2: {
594
+ operations: {
595
+ read: {
596
+ title: 'foo_',
597
+ id: 'foo_',
598
+ },
599
+ },
600
+ predicates: {
601
+ foo_: {
602
+ xyz: {
603
+ lorem: { eq: 'bar' },
604
+ },
605
+ },
606
+ },
607
+ },
608
+ },
609
+ })
610
+ })
611
+
612
+
613
+ it('merge permissions with no root', () => {
614
+ execute({
615
+ acl: {
616
+ roles: {
617
+ role1: {
618
+ variables: {},
619
+ entities: {
620
+ Entity2: {
621
+ predicates: {
622
+ foo: { xyz: { lorem: { eq: 'foo' } } },
623
+ },
624
+ operations: {
625
+ read: {
626
+ title: 'foo',
627
+ },
628
+ noRoot: ['read'],
629
+ },
630
+ },
631
+ },
632
+ },
633
+ role2: {
634
+ variables: {},
635
+ entities: {
636
+ Entity2: {
637
+ predicates: {
638
+ foo: { xyz: { lorem: { eq: 'bar' } } },
639
+ },
640
+ operations: {
641
+ read: {
642
+ title: 'foo',
643
+ },
644
+ noRoot: ['read'],
645
+ },
646
+ },
647
+ },
648
+ },
649
+ },
650
+ },
651
+ roles: ['role1', 'role2'],
652
+ result: {
653
+ Entity2: {
654
+ operations: {
655
+ noRoot: ['read'],
656
+ read: {
657
+ title: '__merge__foo__foo',
658
+ id: '__merge__foo__foo',
659
+ },
660
+ },
661
+ predicates: {
662
+ __merge__foo__foo: {
663
+ or: [
664
+ { xyz: { lorem: { eq: 'foo' } } },
665
+ { xyz: { lorem: { eq: 'bar' } } },
666
+ ],
667
+ },
668
+ },
669
+ },
670
+ },
671
+ })
672
+ })
553
673
  })