@contember/engine-content-api 2.2.0-alpha.4 → 2.2.0-alpha.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contember/engine-content-api",
3
- "version": "2.2.0-alpha.4",
3
+ "version": "2.2.0-alpha.6",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/production/index.js",
6
6
  "typings": "./dist/types/index.d.ts",
@@ -26,17 +26,17 @@
26
26
  "generate": "gql-gen --config graphql.codegen.yml"
27
27
  },
28
28
  "dependencies": {
29
- "@contember/database": "2.2.0-alpha.4",
30
- "@contember/dic": "2.2.0-alpha.4",
31
- "@contember/graphql-utils": "2.2.0-alpha.4",
32
- "@contember/logger": "2.2.0-alpha.4",
33
- "@contember/schema": "2.2.0-alpha.4",
34
- "@contember/schema-definition": "2.2.0-alpha.4",
35
- "@contember/schema-utils": "2.2.0-alpha.4",
29
+ "@contember/database": "2.2.0-alpha.6",
30
+ "@contember/dic": "2.2.0-alpha.6",
31
+ "@contember/graphql-utils": "2.2.0-alpha.6",
32
+ "@contember/logger": "2.2.0-alpha.6",
33
+ "@contember/schema": "2.2.0-alpha.6",
34
+ "@contember/schema-definition": "2.2.0-alpha.6",
35
+ "@contember/schema-utils": "2.2.0-alpha.6",
36
36
  "fast-deep-equal": "^3.1.3"
37
37
  },
38
38
  "devDependencies": {
39
- "@contember/database-tester": "2.2.0-alpha.4",
39
+ "@contember/database-tester": "2.2.0-alpha.6",
40
40
  "@types/node": "^24.12.0",
41
41
  "graphql": "^16.13.1",
42
42
  "pg": "^8.20.0"
@@ -57,15 +57,7 @@ export class PredicatesInjector {
57
57
  if (shouldSimplify) {
58
58
  predicatesWhere = { [entity.primary]: { always: true } }
59
59
  } else {
60
- const rawPredicate = this.predicateFactory.create(entity, Acl.Operation.read, fieldNames, relationContext, isQueryRoot)
61
- // Process the predicate to inject nested entity predicates
62
- predicatesWhere = this.injectPredicatesToPredicate(
63
- rawPredicate,
64
- entity,
65
- isBackReferenceContext ?? false,
66
- ancestorPath ?? [],
67
- isQueryRoot,
68
- )
60
+ predicatesWhere = this.predicateFactory.create(entity, Acl.Operation.read, fieldNames, relationContext, isQueryRoot)
69
61
  }
70
62
 
71
63
  const and = [where, predicatesWhere].filter(it => Object.keys(it).length > 0)
@@ -78,101 +70,6 @@ export class PredicatesInjector {
78
70
  return { and: and }
79
71
  }
80
72
 
81
- /**
82
- * Processes a predicate and injects nested entity predicates for any relation traversals.
83
- * This ensures that when a predicate like { department: { company: { name: 'Acme' } } }
84
- * is used, the predicates of Department and Company are also applied.
85
- */
86
- private injectPredicatesToPredicate(
87
- where: Input.OptionalWhere,
88
- entity: Model.Entity,
89
- isBackReferenceContext: boolean,
90
- ancestorPath: readonly Model.AnyRelationContext[],
91
- isQueryRoot?: boolean,
92
- ): Input.OptionalWhere {
93
- const resultWhere: Writable<Input.OptionalWhere> = {}
94
-
95
- if (where.and) {
96
- resultWhere.and = where.and
97
- .filter((it): it is Input.Where => !!it)
98
- .map(it => this.injectPredicatesToPredicate(it, entity, isBackReferenceContext, ancestorPath, isQueryRoot))
99
- }
100
- if (where.or) {
101
- resultWhere.or = where.or
102
- .filter((it): it is Input.Where => !!it)
103
- .map(it => this.injectPredicatesToPredicate(it, entity, isBackReferenceContext, ancestorPath, isQueryRoot))
104
- }
105
- if (where.not) {
106
- resultWhere.not = this.injectPredicatesToPredicate(where.not, entity, isBackReferenceContext, ancestorPath, isQueryRoot)
107
- }
108
-
109
- const fields = Object.keys(where).filter(it => !['and', 'or', 'not'].includes(it))
110
-
111
- for (const field of fields) {
112
- resultWhere[field] = acceptFieldVisitor(this.schema, entity, field, {
113
- visitColumn: () => where[field],
114
- visitRelation: context => {
115
- const relationWhere = where[field] as Input.OptionalWhere | null
116
- if (relationWhere === null) {
117
- return null
118
- }
119
-
120
- // Check if this relation is a back-reference to somewhere in our ancestor path
121
- const isBackReference = this.findBackReferencedAncestor(
122
- ancestorPath,
123
- context.relation.name,
124
- context.entity.name,
125
- ) !== undefined
126
- const nestedIsBackReferenceContext = isBackReference || isBackReferenceContext
127
- const nestedAncestorPath: readonly Model.AnyRelationContext[] = [...ancestorPath, context]
128
-
129
- // Recursively process the nested where (always do this to handle deeper nesting)
130
- const processedNestedWhere = this.injectPredicatesToPredicate(
131
- relationWhere,
132
- context.targetEntity,
133
- nestedIsBackReferenceContext,
134
- nestedAncestorPath,
135
- isQueryRoot,
136
- )
137
-
138
- // Check if we should simplify the target entity's predicate
139
- const shouldSimplifyNested = nestedIsBackReferenceContext
140
- && this.findBackReferencedAncestor(nestedAncestorPath, context.relation.name, context.entity.name) !== undefined
141
-
142
- // Get target entity's predicate (simplified if back-reference)
143
- const targetPredicate = shouldSimplifyNested
144
- ? { [context.targetEntity.primary]: { always: true } }
145
- : this.predicateFactory.create(context.targetEntity, Acl.Operation.read, undefined, context, isQueryRoot)
146
-
147
- // Optimization: avoid duplicate { id: always } when both are simplified
148
- const primaryKey = context.targetEntity.primary
149
- const nestedIsAlwaysTrue = Object.keys(processedNestedWhere).length === 1
150
- && (processedNestedWhere as Record<string, unknown>)[primaryKey] !== undefined
151
- && (processedNestedWhere as Record<string, Record<string, unknown>>)[primaryKey]?.always === true
152
- const targetIsAlwaysTrue = Object.keys(targetPredicate).length === 1
153
- && (targetPredicate as Record<string, unknown>)[primaryKey] !== undefined
154
- && (targetPredicate as Record<string, Record<string, unknown>>)[primaryKey]?.always === true
155
-
156
- if (nestedIsAlwaysTrue && targetIsAlwaysTrue) {
157
- return processedNestedWhere
158
- }
159
-
160
- // Combine the processed nested where with target entity's predicate
161
- const parts = [processedNestedWhere, targetPredicate].filter(it => Object.keys(it).length > 0)
162
- if (parts.length === 0) {
163
- return {}
164
- }
165
- if (parts.length === 1) {
166
- return parts[0]
167
- }
168
- return { and: parts }
169
- },
170
- })
171
- }
172
-
173
- return resultWhere
174
- }
175
-
176
73
  private injectToWhere(
177
74
  where: Input.OptionalWhere,
178
75
  entity: Model.Entity,
@@ -700,27 +700,13 @@ describe('predicates injector - multi-level back-reference', () => {
700
700
 
701
701
  it('should NOT simplify when filtering via non-ancestor relation', () => {
702
702
  // If we query Employee directly (not through Company->Department chain)
703
- // the predicate should NOT be simplified AND nested predicates should be applied
703
+ // the predicate should NOT be simplified it is applied verbatim.
704
+ // Relation hops inside the predicate are the author's rule (as-definer semantics),
705
+ // so Department's/Company's own read predicates are NOT re-applied inside it.
704
706
  const injected = injector.inject(schema.model.entities.Employee, {})
705
707
 
706
- // Full predicate with nested entity predicates applied:
707
- // - Employee predicate: { department: { company: { isActive: true } } }
708
- // - Department predicate: { company: { isActive: true } } is added
709
- // - Company predicate: { isActive: true } is added inside company traversal
710
708
  assert.deepStrictEqual(injected, {
711
- department: {
712
- and: [
713
- {
714
- company: {
715
- and: [
716
- { isActive: { eq: true } }, // from Employee predicate
717
- { isActive: { eq: true } }, // Company's own predicate
718
- ],
719
- },
720
- },
721
- { company: { isActive: { eq: true } } }, // Department's predicate
722
- ],
723
- },
709
+ department: { company: { isActive: { eq: true } } },
724
710
  })
725
711
  })
726
712
 
@@ -864,21 +850,13 @@ describe('predicates injector - subsidiary edge case', () => {
864
850
  it('should correctly handle predicate when Company in predicate is NOT in path', () => {
865
851
  // Query Department directly (not through Company chain)
866
852
  // Department's predicate: { company: { isActive: true } }
867
- // Since we didn't traverse through Company, the predicate MUST be fully applied
868
- // AND Company's own predicate should also be applied
853
+ // Since we didn't traverse through Company, the predicate MUST be fully applied
854
+ // verbatim, without re-applying Company's own predicate inside it (as-definer semantics)
869
855
 
870
856
  const injected = injector.inject(schema.model.entities.Department, {})
871
857
 
872
- // Full predicate with nested Company predicate:
873
- // - Department predicate: { company: { isActive: true } }
874
- // - Company predicate: { isActive: true } is also added when traversing company
875
858
  assert.deepStrictEqual(injected, {
876
- company: {
877
- and: [
878
- { isActive: { eq: true } }, // from Department predicate
879
- { isActive: { eq: true } }, // Company's own predicate
880
- ],
881
- },
859
+ company: { isActive: { eq: true } },
882
860
  })
883
861
  })
884
862
 
@@ -1012,8 +990,8 @@ describe('predicates injector - SECURITY: inconsistent predicates must NOT be si
1012
990
  // Company predicate: { isActive: true } - verified when querying Company
1013
991
  // Employee predicate: { department: { company: { name: 'Acme' } } } - checks DIFFERENT field!
1014
992
  //
1015
- // SECURITY: The name='Acme' condition MUST be preserved (not simplified).
1016
- // But Department and Company predicates CAN be simplified because we came through them.
993
+ // SECURITY: The name='Acme' condition MUST be preserved (not simplified)
994
+ // Employee itself is not a back-reference, so its predicate is applied verbatim.
1017
995
 
1018
996
  const companyDepartmentsRelation = acceptFieldVisitor(schema.model, schema.model.entities.Company, 'departments', {
1019
997
  visitColumn: () => {
@@ -1032,50 +1010,20 @@ describe('predicates injector - SECURITY: inconsistent predicates must NOT be si
1032
1010
 
1033
1011
  const injected = injector.inject(schema.model.entities.Employee, {}, departmentEmployeesRelation, ancestorPath)
1034
1012
 
1035
- // The name='Acme' condition is preserved (not simplified)
1036
- // Department's predicate is simplified to { id: always } (we came from Department)
1037
- // Company's predicate is simplified to { id: always } (we came through Company)
1038
1013
  assert.deepStrictEqual(injected, {
1039
- department: {
1040
- and: [
1041
- {
1042
- company: {
1043
- and: [
1044
- { name: { eq: 'Acme' } },
1045
- { id: { always: true } }, // Company predicate simplified
1046
- ],
1047
- },
1048
- },
1049
- { id: { always: true } }, // Department predicate simplified
1050
- ],
1051
- },
1014
+ department: { company: { name: { eq: 'Acme' } } },
1052
1015
  })
1053
1016
  })
1054
1017
 
1055
- it('direct Employee query: ALL nested predicates MUST be applied (no ancestor path)', () => {
1056
- // Query Employee DIRECTLY (not through Company -> Department path)
1057
- // In this case, neither Company nor Department was verified at any parent level.
1058
- // So BOTH Department's and Company's predicates MUST be applied.
1018
+ it('direct Employee query: predicate is applied verbatim (as-definer, no nested re-application)', () => {
1019
+ // Query Employee DIRECTLY (not through Company -> Department path).
1020
+ // The predicate is the author's rule: its relation hops are NOT additionally
1021
+ // guarded by Department's/Company's own read predicates (as-definer semantics).
1059
1022
 
1060
1023
  const injected = injector.inject(schema.model.entities.Employee, {})
1061
1024
 
1062
- // Employee's condition (name='Acme') is preserved
1063
- // Department's predicate { company: { isActive: true } } is applied
1064
- // Company's predicate { isActive: true } is applied inside the traversal
1065
1025
  assert.deepStrictEqual(injected, {
1066
- department: {
1067
- and: [
1068
- {
1069
- company: {
1070
- and: [
1071
- { name: { eq: 'Acme' } },
1072
- { isActive: { eq: true } }, // Company predicate
1073
- ],
1074
- },
1075
- },
1076
- { company: { isActive: { eq: true } } }, // Department predicate
1077
- ],
1078
- },
1026
+ department: { company: { name: { eq: 'Acme' } } },
1079
1027
  })
1080
1028
  })
1081
1029