@contember/engine-content-api 2.2.0-alpha.5 → 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/dist/development/src/acl/PredicatesInjector.cjs +1 -68
- package/dist/development/src/acl/PredicatesInjector.cjs.map +1 -1
- package/dist/development/src/acl/PredicatesInjector.js +1 -68
- package/dist/development/src/acl/PredicatesInjector.js.map +1 -1
- package/dist/production/src/acl/PredicatesInjector.cjs +1 -68
- package/dist/production/src/acl/PredicatesInjector.cjs.map +1 -1
- package/dist/production/src/acl/PredicatesInjector.js +1 -68
- package/dist/production/src/acl/PredicatesInjector.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/dist/types/acl/PredicatesInjector.d.ts +0 -6
- package/dist/types/acl/PredicatesInjector.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/acl/PredicatesInjector.ts +1 -104
- package/tests/cases/unit/predicatesInjector.test.ts +15 -67
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/engine-content-api",
|
|
3
|
-
"version": "2.2.0-alpha.
|
|
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.
|
|
30
|
-
"@contember/dic": "2.2.0-alpha.
|
|
31
|
-
"@contember/graphql-utils": "2.2.0-alpha.
|
|
32
|
-
"@contember/logger": "2.2.0-alpha.
|
|
33
|
-
"@contember/schema": "2.2.0-alpha.
|
|
34
|
-
"@contember/schema-definition": "2.2.0-alpha.
|
|
35
|
-
"@contember/schema-utils": "2.2.0-alpha.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
1056
|
-
// Query Employee DIRECTLY (not through Company -> Department path)
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
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
|
|