@contember/engine-content-api 1.2.0-alpha.12 → 1.2.0-alpha.13
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/EvaluatedPredicateReplacer.d.ts +2 -2
- package/dist/src/acl/EvaluatedPredicateReplacer.d.ts.map +1 -1
- package/dist/src/acl/EvaluatedPredicateReplacer.js.map +1 -1
- package/dist/src/acl/PredicateFactory.d.ts +9 -3
- package/dist/src/acl/PredicateFactory.d.ts.map +1 -1
- package/dist/src/acl/PredicateFactory.js +42 -24
- package/dist/src/acl/PredicateFactory.js.map +1 -1
- package/dist/src/acl/PredicatesInjector.d.ts.map +1 -1
- package/dist/src/acl/PredicatesInjector.js +1 -0
- package/dist/src/acl/PredicatesInjector.js.map +1 -1
- package/dist/src/extensions/hasManyToHasOneReducer/HasManyToHasOneReducerExecutionHandler.js +31 -27
- package/dist/src/extensions/hasManyToHasOneReducer/HasManyToHasOneReducerExecutionHandler.js.map +1 -1
- package/dist/src/extensions/paginatedHasMany/PaginatedHasManyExecutionHandler.js +18 -14
- package/dist/src/extensions/paginatedHasMany/PaginatedHasManyExecutionHandler.js.map +1 -1
- package/dist/src/mapper/JunctionTableManager.js.map +1 -1
- package/dist/src/mapper/Mapper.js +3 -3
- package/dist/src/mapper/Mapper.js.map +1 -1
- package/dist/src/mapper/MapperContainer.d.ts.map +1 -1
- package/dist/src/mapper/MapperContainer.js +2 -2
- package/dist/src/mapper/MapperContainer.js.map +1 -1
- package/dist/src/mapper/delete/DeleteExecutor.js +1 -1
- package/dist/src/mapper/delete/DeleteExecutor.js.map +1 -1
- package/dist/src/mapper/insert/InsertBuilder.d.ts +1 -1
- package/dist/src/mapper/insert/InsertBuilder.d.ts.map +1 -1
- package/dist/src/mapper/insert/InsertBuilder.js.map +1 -1
- package/dist/src/mapper/select/RelationFetcher.js +1 -1
- package/dist/src/mapper/select/RelationFetcher.js.map +1 -1
- package/dist/src/mapper/select/SelectBuilder.d.ts +3 -1
- package/dist/src/mapper/select/SelectBuilder.d.ts.map +1 -1
- package/dist/src/mapper/select/SelectBuilder.js +50 -12
- package/dist/src/mapper/select/SelectBuilder.js.map +1 -1
- package/dist/src/mapper/select/SelectExecutionHandler.d.ts +15 -4
- package/dist/src/mapper/select/SelectExecutionHandler.d.ts.map +1 -1
- package/dist/src/mapper/select/SelectHydrator.d.ts +3 -2
- package/dist/src/mapper/select/SelectHydrator.d.ts.map +1 -1
- package/dist/src/mapper/select/SelectHydrator.js +4 -5
- package/dist/src/mapper/select/SelectHydrator.js.map +1 -1
- package/dist/src/mapper/select/WhereBuilder.d.ts +3 -3
- package/dist/src/mapper/select/WhereBuilder.d.ts.map +1 -1
- package/dist/src/mapper/select/WhereBuilder.js +4 -4
- package/dist/src/mapper/select/WhereBuilder.js.map +1 -1
- package/dist/src/mapper/select/handlers/FieldsVisitor.d.ts +1 -0
- package/dist/src/mapper/select/handlers/FieldsVisitor.d.ts.map +1 -1
- package/dist/src/mapper/select/handlers/FieldsVisitor.js +118 -91
- package/dist/src/mapper/select/handlers/FieldsVisitor.js.map +1 -1
- package/dist/src/mapper/select/handlers/MetaHandler.d.ts.map +1 -1
- package/dist/src/mapper/select/handlers/MetaHandler.js +6 -11
- package/dist/src/mapper/select/handlers/MetaHandler.js.map +1 -1
- package/dist/src/mapper/select/optimizer/WhereOptimizer.d.ts +5 -1
- package/dist/src/mapper/select/optimizer/WhereOptimizer.d.ts.map +1 -1
- package/dist/src/mapper/select/optimizer/WhereOptimizer.js +10 -3
- package/dist/src/mapper/select/optimizer/WhereOptimizer.js.map +1 -1
- package/dist/src/mapper/select/optimizer/WhereReplacer.d.ts +1 -1
- package/dist/src/mapper/select/optimizer/WhereReplacer.d.ts.map +1 -1
- package/dist/src/mapper/select/optimizer/WhereReplacer.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/tests/cases/integration/graphQlRequests/query/acl/differentRowAndFieldPredicate.test.js +51 -4
- package/dist/tests/cases/integration/graphQlRequests/query/acl/differentRowAndFieldPredicate.test.js.map +1 -1
- package/dist/tests/cases/integration/graphQlRequests/query/acl/fieldRestrictedByLocale.test.js +26 -25
- package/dist/tests/cases/integration/graphQlRequests/query/acl/fieldRestrictedByLocale.test.js.map +1 -1
- package/dist/tests/cases/integration/graphQlRequests/query/acl/nestedPredicate.test.js +9 -6
- package/dist/tests/cases/integration/graphQlRequests/query/acl/nestedPredicate.test.js.map +1 -1
- package/dist/tests/cases/integration/graphQlRequests/query/acl/rowPredicateSubset.test.d.ts +2 -0
- package/dist/tests/cases/integration/graphQlRequests/query/acl/rowPredicateSubset.test.d.ts.map +1 -0
- package/dist/tests/cases/integration/graphQlRequests/query/acl/rowPredicateSubset.test.js +109 -0
- package/dist/tests/cases/integration/graphQlRequests/query/acl/rowPredicateSubset.test.js.map +1 -0
- package/dist/tests/cases/integration/graphQlRequests/query/graphqlFeatures/alias.test.js +9 -13
- package/dist/tests/cases/integration/graphQlRequests/query/graphqlFeatures/alias.test.js.map +1 -1
- package/dist/tests/cases/unit/predicatesInjector.test.js +31 -3
- package/dist/tests/cases/unit/predicatesInjector.test.js.map +1 -1
- package/dist/tests/cases/unit/whereOptimizer.test.js +24 -21
- package/dist/tests/cases/unit/whereOptimizer.test.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/acl/EvaluatedPredicateReplacer.ts +4 -4
- package/src/acl/PredicateFactory.ts +59 -36
- package/src/acl/PredicatesInjector.ts +2 -1
- package/src/extensions/hasManyToHasOneReducer/HasManyToHasOneReducerExecutionHandler.ts +5 -5
- package/src/extensions/paginatedHasMany/PaginatedHasManyExecutionHandler.ts +5 -5
- package/src/mapper/JunctionTableManager.ts +3 -3
- package/src/mapper/Mapper.ts +3 -3
- package/src/mapper/MapperContainer.ts +2 -1
- package/src/mapper/delete/DeleteExecutor.ts +1 -1
- package/src/mapper/insert/InsertBuilder.ts +2 -2
- package/src/mapper/select/RelationFetcher.ts +1 -1
- package/src/mapper/select/SelectBuilder.ts +69 -13
- package/src/mapper/select/SelectExecutionHandler.ts +15 -8
- package/src/mapper/select/SelectHydrator.ts +13 -7
- package/src/mapper/select/WhereBuilder.ts +5 -5
- package/src/mapper/select/handlers/FieldsVisitor.ts +53 -67
- package/src/mapper/select/handlers/MetaHandler.ts +6 -17
- package/src/mapper/select/optimizer/WhereOptimizer.ts +25 -13
- package/src/mapper/select/optimizer/WhereReplacer.ts +4 -4
- package/tests/cases/integration/graphQlRequests/query/acl/differentRowAndFieldPredicate.test.ts +54 -4
- package/tests/cases/integration/graphQlRequests/query/acl/fieldRestrictedByLocale.test.ts +26 -25
- package/tests/cases/integration/graphQlRequests/query/acl/nestedPredicate.test.ts +9 -6
- package/tests/cases/integration/graphQlRequests/query/acl/rowPredicateSubset.test.ts +104 -0
- package/tests/cases/integration/graphQlRequests/query/graphqlFeatures/alias.test.ts +9 -13
- package/tests/cases/unit/predicatesInjector.test.ts +37 -7
- package/tests/cases/unit/whereOptimizer.test.ts +20 -17
|
@@ -18,35 +18,12 @@ export class FieldsVisitor implements Model.RelationByTypeVisitor<void>, Model.C
|
|
|
18
18
|
|
|
19
19
|
visitColumn({ entity, column }: Model.ColumnContext): void {
|
|
20
20
|
const columnPath = this.executionContext.path
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const columnAlias = columnPath.alias
|
|
21
|
+
const tableAlias = columnPath.back().alias
|
|
22
|
+
const columnAlias = columnPath.alias
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
column.name,
|
|
29
|
-
)
|
|
30
|
-
? this.predicateFactory.create(entity, Acl.Operation.read, [column.name])
|
|
31
|
-
: undefined
|
|
32
|
-
|
|
33
|
-
if (!fieldPredicate || Object.keys(fieldPredicate).length === 0) {
|
|
34
|
-
return qb.select([tableAlias, column.columnName], columnAlias)
|
|
35
|
-
}
|
|
36
|
-
return this.whereBuilder.buildAdvanced(entity, columnPath.back(), fieldPredicate, cb =>
|
|
37
|
-
qb.select(
|
|
38
|
-
expr =>
|
|
39
|
-
expr.case(caseExpr =>
|
|
40
|
-
caseExpr
|
|
41
|
-
.when(
|
|
42
|
-
whenExpr => whenExpr.selectCondition(cb),
|
|
43
|
-
thenExpr => thenExpr.select([tableAlias, column.columnName]),
|
|
44
|
-
)
|
|
45
|
-
.else(elseExpr => elseExpr.raw('null')),
|
|
46
|
-
),
|
|
47
|
-
columnAlias,
|
|
48
|
-
),
|
|
49
|
-
)
|
|
24
|
+
this.executionContext.addColumn({
|
|
25
|
+
query: qb => qb.select([tableAlias, column.columnName], columnAlias),
|
|
26
|
+
predicate: this.getRequiredPredicate(entity, column),
|
|
50
27
|
})
|
|
51
28
|
}
|
|
52
29
|
|
|
@@ -55,20 +32,19 @@ export class FieldsVisitor implements Model.RelationByTypeVisitor<void>, Model.C
|
|
|
55
32
|
if (!field) {
|
|
56
33
|
throw new Error()
|
|
57
34
|
}
|
|
58
|
-
this.executionContext.addData(
|
|
59
|
-
relationContext.entity.primary,
|
|
60
|
-
async ids =>
|
|
61
|
-
this.relationFetcher.fetchManyHasManyGroups(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
35
|
+
this.executionContext.addData({
|
|
36
|
+
field: relationContext.entity.primary,
|
|
37
|
+
dataProvider: async ids =>
|
|
38
|
+
this.relationFetcher.fetchManyHasManyGroups({
|
|
39
|
+
mapper: this.mapper,
|
|
40
|
+
field: field,
|
|
41
|
+
relationContext,
|
|
42
|
+
relationPath: this.relationPath,
|
|
43
|
+
ids: ids,
|
|
44
|
+
}),
|
|
45
|
+
defaultValue: [],
|
|
46
|
+
predicate: this.getRequiredPredicate(relationContext.entity, relationContext.relation),
|
|
47
|
+
})
|
|
72
48
|
}
|
|
73
49
|
|
|
74
50
|
public visitManyHasManyOwning(relationContext: Model.ManyHasManyOwningContext): void {
|
|
@@ -77,9 +53,9 @@ export class FieldsVisitor implements Model.RelationByTypeVisitor<void>, Model.C
|
|
|
77
53
|
throw new Error()
|
|
78
54
|
}
|
|
79
55
|
|
|
80
|
-
this.executionContext.addData(
|
|
81
|
-
relationContext.entity.primary,
|
|
82
|
-
async ids =>
|
|
56
|
+
this.executionContext.addData({
|
|
57
|
+
field: relationContext.entity.primary,
|
|
58
|
+
dataProvider: async ids =>
|
|
83
59
|
this.relationFetcher.fetchManyHasManyGroups({
|
|
84
60
|
mapper: this.mapper,
|
|
85
61
|
field: field,
|
|
@@ -87,8 +63,9 @@ export class FieldsVisitor implements Model.RelationByTypeVisitor<void>, Model.C
|
|
|
87
63
|
relationPath: this.relationPath,
|
|
88
64
|
ids: ids,
|
|
89
65
|
}),
|
|
90
|
-
[],
|
|
91
|
-
|
|
66
|
+
defaultValue: [],
|
|
67
|
+
predicate: this.getRequiredPredicate(relationContext.entity, relationContext.relation),
|
|
68
|
+
})
|
|
92
69
|
}
|
|
93
70
|
|
|
94
71
|
public visitOneHasMany(relationContext: Model.OneHasManyContext): void {
|
|
@@ -97,9 +74,9 @@ export class FieldsVisitor implements Model.RelationByTypeVisitor<void>, Model.C
|
|
|
97
74
|
throw new Error()
|
|
98
75
|
}
|
|
99
76
|
|
|
100
|
-
this.executionContext.addData(
|
|
101
|
-
relationContext.entity.primary,
|
|
102
|
-
async ids =>
|
|
77
|
+
this.executionContext.addData({
|
|
78
|
+
field: relationContext.entity.primary,
|
|
79
|
+
dataProvider: async ids =>
|
|
103
80
|
this.relationFetcher.fetchOneHasManyGroups({
|
|
104
81
|
mapper: this.mapper,
|
|
105
82
|
objectNode: field,
|
|
@@ -107,15 +84,16 @@ export class FieldsVisitor implements Model.RelationByTypeVisitor<void>, Model.C
|
|
|
107
84
|
relationPath: this.relationPath,
|
|
108
85
|
ids: ids,
|
|
109
86
|
}),
|
|
110
|
-
[],
|
|
111
|
-
|
|
87
|
+
defaultValue: [],
|
|
88
|
+
predicate: this.getRequiredPredicate(relationContext.entity, relationContext.relation),
|
|
89
|
+
})
|
|
112
90
|
}
|
|
113
91
|
|
|
114
92
|
public visitOneHasOneInverse(relationContext: Model.OneHasOneInverseContext): void {
|
|
115
93
|
const { entity, targetRelation, targetEntity } = relationContext
|
|
116
|
-
this.executionContext.addData(
|
|
117
|
-
entity.primary,
|
|
118
|
-
async ids => {
|
|
94
|
+
this.executionContext.addData({
|
|
95
|
+
field: entity.primary,
|
|
96
|
+
dataProvider: async ids => {
|
|
119
97
|
const idsWhere: Input.Where = {
|
|
120
98
|
[targetRelation.name]: {
|
|
121
99
|
[entity.primary]: {
|
|
@@ -137,15 +115,16 @@ export class FieldsVisitor implements Model.RelationByTypeVisitor<void>, Model.C
|
|
|
137
115
|
relationContext,
|
|
138
116
|
], targetRelation.name)
|
|
139
117
|
},
|
|
140
|
-
null,
|
|
141
|
-
|
|
118
|
+
defaultValue: null,
|
|
119
|
+
predicate: this.getRequiredPredicate(relationContext.entity, relationContext.relation),
|
|
120
|
+
})
|
|
142
121
|
}
|
|
143
122
|
|
|
144
123
|
public visitOneHasOneOwning(relationContext: Model.OneHasOneOwningContext): void {
|
|
145
124
|
const { relation, targetEntity } = relationContext
|
|
146
|
-
this.executionContext.addData(
|
|
147
|
-
relation.name,
|
|
148
|
-
async ids => {
|
|
125
|
+
this.executionContext.addData({
|
|
126
|
+
field: relation.name,
|
|
127
|
+
dataProvider: async ids => {
|
|
149
128
|
const idsWhere: Input.Where = {
|
|
150
129
|
[targetEntity.primary]: {
|
|
151
130
|
in: ids,
|
|
@@ -165,15 +144,16 @@ export class FieldsVisitor implements Model.RelationByTypeVisitor<void>, Model.C
|
|
|
165
144
|
relationContext,
|
|
166
145
|
], targetEntity.primary)
|
|
167
146
|
},
|
|
168
|
-
null,
|
|
169
|
-
|
|
147
|
+
defaultValue: null,
|
|
148
|
+
predicate: this.getRequiredPredicate(relationContext.entity, relationContext.relation),
|
|
149
|
+
})
|
|
170
150
|
}
|
|
171
151
|
|
|
172
152
|
public visitManyHasOne(relationContext: Model.ManyHasOneContext): void {
|
|
173
153
|
const { relation, targetEntity } = relationContext
|
|
174
|
-
this.executionContext.addData(
|
|
175
|
-
relation.name,
|
|
176
|
-
async ids => {
|
|
154
|
+
this.executionContext.addData({
|
|
155
|
+
field: relation.name,
|
|
156
|
+
dataProvider: async ids => {
|
|
177
157
|
const idsWhere: Input.Where = {
|
|
178
158
|
[targetEntity.primary]: {
|
|
179
159
|
in: ids,
|
|
@@ -193,7 +173,13 @@ export class FieldsVisitor implements Model.RelationByTypeVisitor<void>, Model.C
|
|
|
193
173
|
relationContext,
|
|
194
174
|
], targetEntity.primary)
|
|
195
175
|
},
|
|
196
|
-
null,
|
|
197
|
-
|
|
176
|
+
defaultValue: null,
|
|
177
|
+
predicate: this.getRequiredPredicate(relationContext.entity, relationContext.relation),
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private getRequiredPredicate(entity: Model.Entity, field: Model.AnyField): Acl.Predicate | undefined {
|
|
182
|
+
const fieldPredicate = this.predicateFactory.getFieldPredicate(entity, Acl.Operation.read, field.name)
|
|
183
|
+
return fieldPredicate.isSameAsPrimary ? undefined : fieldPredicate.predicate
|
|
198
184
|
}
|
|
199
185
|
}
|
|
@@ -32,25 +32,14 @@ export class MetaHandler implements SelectExecutionHandler<{}> {
|
|
|
32
32
|
metaPath: Path,
|
|
33
33
|
operation: Acl.Operation.read | Acl.Operation.update,
|
|
34
34
|
): void {
|
|
35
|
-
const { entity
|
|
35
|
+
const { entity } = context
|
|
36
36
|
if (entity.primary === fieldName) {
|
|
37
37
|
return
|
|
38
38
|
}
|
|
39
|
-
const fieldPredicate = this.predicateFactory.
|
|
40
|
-
context.addColumn(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
expr.selectCondition(condition => {
|
|
45
|
-
condition = cb(condition)
|
|
46
|
-
if (condition.isEmpty()) {
|
|
47
|
-
return condition.raw('true')
|
|
48
|
-
}
|
|
49
|
-
return condition
|
|
50
|
-
}),
|
|
51
|
-
metaPath.alias,
|
|
52
|
-
),
|
|
53
|
-
)
|
|
54
|
-
}, metaPath)
|
|
39
|
+
const fieldPredicate = this.predicateFactory.getFieldPredicate(entity, operation, fieldName)
|
|
40
|
+
context.addColumn({
|
|
41
|
+
path: metaPath,
|
|
42
|
+
valueGetter: context.addPredicate(fieldPredicate.predicate),
|
|
43
|
+
})
|
|
55
44
|
}
|
|
56
45
|
}
|
|
@@ -11,6 +11,11 @@ type ExtendedRelationContext =
|
|
|
11
11
|
isLast: boolean
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface WhereOptimizationHints {
|
|
15
|
+
relationPath?: Model.AnyRelationContext[]
|
|
16
|
+
evaluatedPredicates?: Input.OptionalWhere[]
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
export class WhereOptimizer {
|
|
15
20
|
private static eliminableRelations = new Set<Model.AnyRelationContext['type']>(['oneHasMany', 'oneHasOneInverse', 'oneHasOneOwning'])
|
|
16
21
|
|
|
@@ -20,7 +25,7 @@ export class WhereOptimizer {
|
|
|
20
25
|
) {
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
public optimize(where: Input.OptionalWhere, entity: Model.Entity, relationPath
|
|
28
|
+
public optimize(where: Input.OptionalWhere, entity: Model.Entity, { relationPath = [], evaluatedPredicates = [] }: WhereOptimizationHints = {}): Input.OptionalWhere {
|
|
24
29
|
let processedRelationPath: ExtendedRelationContext[] = []
|
|
25
30
|
for (const i in relationPath) {
|
|
26
31
|
const el = relationPath[i]
|
|
@@ -29,31 +34,38 @@ export class WhereOptimizer {
|
|
|
29
34
|
} else {
|
|
30
35
|
processedRelationPath.push({ ...el, canEliminate: true, isLast: Number(i) === relationPath.length })
|
|
31
36
|
}
|
|
32
|
-
processedRelationPath.push({ ...el, canEliminate: true, isLast: Number(i) === relationPath.length })
|
|
33
37
|
}
|
|
34
|
-
|
|
38
|
+
let result = this.optimizeWhere(where, entity, processedRelationPath)
|
|
35
39
|
|
|
36
40
|
if (typeof result === 'boolean') {
|
|
37
41
|
return { [entity.primary]: { [result ? 'always' : 'never']: true } }
|
|
38
42
|
}
|
|
43
|
+
const resultTracker = { count: 0 }
|
|
44
|
+
for (const evaluated of evaluatedPredicates) {
|
|
45
|
+
const evaluatedPredicate = this.optimize(evaluated, entity)
|
|
46
|
+
result = replaceWhere(result, evaluatedPredicate, { [entity.primary]: { always: true } }, resultTracker)
|
|
47
|
+
}
|
|
48
|
+
if (resultTracker.count > 0) {
|
|
49
|
+
return this.optimize(result, entity)
|
|
50
|
+
}
|
|
39
51
|
|
|
40
52
|
return result
|
|
41
53
|
}
|
|
42
54
|
|
|
43
|
-
private optimizeWhere(where: Input.OptionalWhere, entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.
|
|
55
|
+
private optimizeWhere(where: Input.OptionalWhere, entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
|
|
44
56
|
return this.optimizeAnd(
|
|
45
57
|
Object.entries(where).map(([key, value]) => {
|
|
46
58
|
if (value === undefined || value === null) {
|
|
47
59
|
return undefined
|
|
48
60
|
|
|
49
61
|
} else if (key === 'and') {
|
|
50
|
-
return this.optimizeAnd((value as readonly Input.
|
|
62
|
+
return this.optimizeAnd((value as readonly Input.OptionalWhere[]).map(it => this.optimizeWhere(it, entity, relationPath)), entity, relationPath)
|
|
51
63
|
|
|
52
64
|
} else if (key === 'or') {
|
|
53
|
-
return this.optimizeOr((value as readonly Input.
|
|
65
|
+
return this.optimizeOr((value as readonly Input.OptionalWhere[]).map(it => this.optimizeWhere(it, entity, relationPath)), entity, relationPath)
|
|
54
66
|
|
|
55
67
|
} else if (key === 'not') {
|
|
56
|
-
return optimizeNot(this.optimizeWhere(value as Input.
|
|
68
|
+
return optimizeNot(this.optimizeWhere(value as Input.OptionalWhere, entity, relationPath))
|
|
57
69
|
|
|
58
70
|
} else {
|
|
59
71
|
return this.resolveFieldValue(entity, key, value, relationPath)
|
|
@@ -64,7 +76,7 @@ export class WhereOptimizer {
|
|
|
64
76
|
)
|
|
65
77
|
}
|
|
66
78
|
|
|
67
|
-
private optimizeOr(operands: readonly (Input.
|
|
79
|
+
private optimizeOr(operands: readonly (Input.OptionalWhere | undefined | boolean)[], entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
|
|
68
80
|
const optimized = optimizeOr(operands)
|
|
69
81
|
if (typeof optimized === 'boolean' || !Array.isArray(optimized.or)) {
|
|
70
82
|
return optimized
|
|
@@ -73,7 +85,7 @@ export class WhereOptimizer {
|
|
|
73
85
|
return optimizeOr(result)
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
private optimizeAnd(operands: readonly (Input.
|
|
88
|
+
private optimizeAnd(operands: readonly (Input.OptionalWhere | undefined | boolean)[], entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
|
|
77
89
|
const optimized = optimizeAnd(operands)
|
|
78
90
|
if (typeof optimized === 'boolean' || !Array.isArray(optimized.and)) {
|
|
79
91
|
return optimized
|
|
@@ -82,8 +94,8 @@ export class WhereOptimizer {
|
|
|
82
94
|
return optimizeAnd(result)
|
|
83
95
|
}
|
|
84
96
|
|
|
85
|
-
private crossOptimize(operands: Input.
|
|
86
|
-
const result: (Input.
|
|
97
|
+
private crossOptimize(operands: Input.OptionalWhere[], entity: Model.Entity, replacement: Input.Condition, relationPath: ExtendedRelationContext[]): (Input.OptionalWhere | boolean)[] {
|
|
98
|
+
const result: (Input.OptionalWhere | boolean)[] = [...operands]
|
|
87
99
|
const count = result.length
|
|
88
100
|
for (let i = 0; i < count; i++) {
|
|
89
101
|
for (let j = 0; j < count; j++) {
|
|
@@ -100,7 +112,7 @@ export class WhereOptimizer {
|
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
private resolveFieldValue(entity: Model.Entity, key: string, value: Input.OptionalWhere[string], relationPath: ExtendedRelationContext[]) {
|
|
103
|
-
return acceptFieldVisitor<Input.
|
|
115
|
+
return acceptFieldVisitor<Input.OptionalWhere | boolean>(this.model, entity, key, {
|
|
104
116
|
visitColumn: () => {
|
|
105
117
|
const optimizedCondition = this.conditionOptimizer.optimize(value as Input.Condition)
|
|
106
118
|
|
|
@@ -112,7 +124,7 @@ export class WhereOptimizer {
|
|
|
112
124
|
},
|
|
113
125
|
visitRelation: context => {
|
|
114
126
|
const { targetEntity, relation } = context
|
|
115
|
-
let where = value as Input.
|
|
127
|
+
let where = value as Input.OptionalWhere
|
|
116
128
|
const newRelationPath: ExtendedRelationContext[] = [...relationPath]
|
|
117
129
|
if (relationPath.length && relationPath[relationPath.length - 1].targetRelation?.name === relation.name) {
|
|
118
130
|
const item = newRelationPath.pop()
|
|
@@ -5,18 +5,18 @@ export interface ReplaceWhereResult {
|
|
|
5
5
|
count: number
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export const replaceWhere = (subject: Input.
|
|
8
|
+
export const replaceWhere = (subject: Input.OptionalWhere, find: Input.OptionalWhere, replace: Input.OptionalWhere, resultTracker: ReplaceWhereResult = { count: 0 }): Input.OptionalWhere => {
|
|
9
9
|
if (deepEqual(subject, find)) {
|
|
10
10
|
resultTracker.count++
|
|
11
11
|
return replace
|
|
12
12
|
}
|
|
13
|
-
const result: Writable<Input.
|
|
13
|
+
const result: Writable<Input.OptionalWhere> = {}
|
|
14
14
|
for (const [key, value] of Object.entries(subject)) {
|
|
15
15
|
if (key === 'not') {
|
|
16
|
-
result['not'] = replaceWhere(value as Input.
|
|
16
|
+
result['not'] = replaceWhere(value as Input.OptionalWhere, find, replace, resultTracker)
|
|
17
17
|
|
|
18
18
|
} else if (key === 'and' || key === 'or') {
|
|
19
|
-
result[key] = (value as Input.
|
|
19
|
+
result[key] = (value as Input.OptionalWhere[]).map(it => replaceWhere(it, find, replace, resultTracker))
|
|
20
20
|
|
|
21
21
|
} else {
|
|
22
22
|
result[key] = value
|
package/tests/cases/integration/graphQlRequests/query/acl/differentRowAndFieldPredicate.test.ts
CHANGED
|
@@ -41,16 +41,18 @@ test('different predicate on row and field level', async () => {
|
|
|
41
41
|
executes: [
|
|
42
42
|
{
|
|
43
43
|
sql: SQL`
|
|
44
|
-
select
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
select
|
|
45
|
+
"root_"."id" as "root_id",
|
|
46
|
+
"root_"."username" = ? as "root___predicate_authorPredicate",
|
|
47
|
+
"root_"."username" as "root_username"
|
|
48
|
+
from "public"."author" as "root_" where not("root_"."username" is null)
|
|
48
49
|
`,
|
|
49
50
|
parameters: ['johndoe'],
|
|
50
51
|
response: {
|
|
51
52
|
rows: [
|
|
52
53
|
{
|
|
53
54
|
root_id: testUuid(1),
|
|
55
|
+
root___predicate_authorPredicate: true,
|
|
54
56
|
root_username: 'johndoe',
|
|
55
57
|
},
|
|
56
58
|
],
|
|
@@ -71,3 +73,51 @@ test('different predicate on row and field level', async () => {
|
|
|
71
73
|
})
|
|
72
74
|
|
|
73
75
|
|
|
76
|
+
|
|
77
|
+
test('different predicate on row and field level - false', async () => {
|
|
78
|
+
await execute({
|
|
79
|
+
schema: schema,
|
|
80
|
+
permissions: permissions,
|
|
81
|
+
variables: {},
|
|
82
|
+
query: GQL`
|
|
83
|
+
query {
|
|
84
|
+
listAuthor {
|
|
85
|
+
id
|
|
86
|
+
username
|
|
87
|
+
}
|
|
88
|
+
}`,
|
|
89
|
+
executes: [
|
|
90
|
+
{
|
|
91
|
+
sql: SQL`
|
|
92
|
+
select
|
|
93
|
+
"root_"."id" as "root_id",
|
|
94
|
+
"root_"."username" = ? as "root___predicate_authorPredicate",
|
|
95
|
+
"root_"."username" as "root_username"
|
|
96
|
+
from "public"."author" as "root_" where not("root_"."username" is null)
|
|
97
|
+
`,
|
|
98
|
+
parameters: ['johndoe'],
|
|
99
|
+
response: {
|
|
100
|
+
rows: [
|
|
101
|
+
{
|
|
102
|
+
root_id: testUuid(1),
|
|
103
|
+
root___predicate_authorPredicate: false,
|
|
104
|
+
root_username: 'johndoe',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
return: {
|
|
111
|
+
data: {
|
|
112
|
+
listAuthor: [
|
|
113
|
+
{
|
|
114
|
+
id: testUuid(1),
|
|
115
|
+
username: null,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
|
|
@@ -63,26 +63,23 @@ test('querying id and title', async () => {
|
|
|
63
63
|
}`,
|
|
64
64
|
executes: [
|
|
65
65
|
{
|
|
66
|
-
sql: SQL`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
parameters: ['cs', 'cs'],
|
|
66
|
+
sql: SQL`SELECT
|
|
67
|
+
"root_"."id" AS "root_id",
|
|
68
|
+
"root_"."locale" IN (?) AS "root___predicate_localePredicate",
|
|
69
|
+
"root_"."title" AS "root_title"
|
|
70
|
+
FROM "public"."post_locale" AS "root_"`,
|
|
71
|
+
parameters: ['cs'],
|
|
73
72
|
response: {
|
|
74
73
|
rows: [
|
|
75
74
|
{
|
|
76
75
|
root_id: testUuid(1),
|
|
77
76
|
root_title: null,
|
|
78
|
-
|
|
79
|
-
root__meta_title_updatable: false,
|
|
77
|
+
root___predicate_localePredicate: false,
|
|
80
78
|
},
|
|
81
79
|
{
|
|
82
80
|
root_id: testUuid(2),
|
|
83
81
|
root_title: 'bar',
|
|
84
|
-
|
|
85
|
-
root__meta_title_updatable: false,
|
|
82
|
+
root___predicate_localePredicate: true,
|
|
86
83
|
},
|
|
87
84
|
],
|
|
88
85
|
},
|
|
@@ -134,8 +131,9 @@ test('querying with extra where', async () => {
|
|
|
134
131
|
executes: [
|
|
135
132
|
{
|
|
136
133
|
sql: SQL`select
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
"root_"."id" as "root_id",
|
|
135
|
+
"root_"."locale" in (?) as "root___predicate_localePredicate",
|
|
136
|
+
"root_"."title" as "root_title"
|
|
139
137
|
from "public"."post_locale" as "root_"
|
|
140
138
|
where "root_"."title" = ? and "root_"."locale" in (?)`,
|
|
141
139
|
parameters: ['cs', 'foo', 'cs'],
|
|
@@ -144,7 +142,7 @@ test('querying with extra where', async () => {
|
|
|
144
142
|
{
|
|
145
143
|
root_id: testUuid(1),
|
|
146
144
|
root_title: 'foo',
|
|
147
|
-
|
|
145
|
+
root___predicate_localePredicate: true,
|
|
148
146
|
},
|
|
149
147
|
],
|
|
150
148
|
},
|
|
@@ -217,10 +215,11 @@ test('not defined acl variable', async () => {
|
|
|
217
215
|
}`,
|
|
218
216
|
executes: [
|
|
219
217
|
{
|
|
220
|
-
sql: SQL`
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
sql: SQL`
|
|
219
|
+
select "root_"."id" as "root_id",
|
|
220
|
+
false as "root___predicate_localePredicate",
|
|
221
|
+
"root_"."title" as "root_title"
|
|
222
|
+
from "public"."post_locale" as "root_"`,
|
|
224
223
|
parameters: [],
|
|
225
224
|
response: {
|
|
226
225
|
rows: [],
|
|
@@ -270,23 +269,25 @@ test('querying on nested field', async () => {
|
|
|
270
269
|
},
|
|
271
270
|
},
|
|
272
271
|
{
|
|
273
|
-
sql: SQL`
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
272
|
+
sql: SQL`
|
|
273
|
+
select "root_"."post_id" as "__grouping_key",
|
|
274
|
+
"root_"."id" as "root_id",
|
|
275
|
+
"root_"."locale" in (?) as "root___predicate_localePredicate",
|
|
276
|
+
"root_"."title" as "root_title"
|
|
277
|
+
from "public"."post_locale" as "root_" where "root_"."post_id" in (?, ?)`,
|
|
279
278
|
parameters: ['cs', testUuid(1), testUuid(2)],
|
|
280
279
|
response: {
|
|
281
280
|
rows: [
|
|
282
281
|
{
|
|
283
282
|
__grouping_key: testUuid(1),
|
|
284
283
|
root_id: testUuid(3),
|
|
285
|
-
|
|
284
|
+
root___predicate_localePredicate: false,
|
|
285
|
+
root_title: 'foo',
|
|
286
286
|
},
|
|
287
287
|
{
|
|
288
288
|
__grouping_key: testUuid(2),
|
|
289
289
|
root_id: testUuid(4),
|
|
290
|
+
root___predicate_localePredicate: true,
|
|
290
291
|
root_title: 'bar',
|
|
291
292
|
},
|
|
292
293
|
],
|
|
@@ -90,17 +90,20 @@ test('querying post + author name', async () => {
|
|
|
90
90
|
},
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
|
-
sql: SQL`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
sql: SQL`SELECT
|
|
94
|
+
"root_"."id" AS "root_id",
|
|
95
|
+
"root_country"."name" IN (?) AS "root___predicate_countryPredicate",
|
|
96
|
+
"root_"."name" AS "root_name",
|
|
97
|
+
"root_"."id" AS "root_id"
|
|
98
|
+
FROM "public"."author" AS "root_"
|
|
99
|
+
LEFT JOIN "public"."country" AS "root_country" ON "root_"."country_id" = "root_country"."id"
|
|
100
|
+
WHERE "root_"."id" IN (?, ?)`,
|
|
99
101
|
parameters: ['Czechia', testUuid(3), testUuid(4)],
|
|
100
102
|
response: {
|
|
101
103
|
rows: [
|
|
102
104
|
{
|
|
103
105
|
root_id: testUuid(3),
|
|
106
|
+
root___predicate_countryPredicate: true,
|
|
104
107
|
root_name: 'John',
|
|
105
108
|
},
|
|
106
109
|
],
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { SchemaDefinition as def, AclDefinition as acl, createSchema } from '@contember/schema-definition'
|
|
2
|
+
import { Acl } from '@contember/schema'
|
|
3
|
+
import { test } from 'vitest'
|
|
4
|
+
import { execute } from '../../../../../src/test'
|
|
5
|
+
import { GQL, SQL } from '../../../../../src/tags'
|
|
6
|
+
import { testUuid } from '../../../../../src/testUuid'
|
|
7
|
+
import { PermissionFactory } from '../../../../../../src'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
namespace RowPredicateSubset {
|
|
11
|
+
export const readerRole = acl.createRole('reader')
|
|
12
|
+
@acl.allow(readerRole, {
|
|
13
|
+
when: {
|
|
14
|
+
isPublished: { eq: true },
|
|
15
|
+
},
|
|
16
|
+
read: ['isPublished', 'isUnlocked', 'title'],
|
|
17
|
+
})
|
|
18
|
+
@acl.allow(readerRole, {
|
|
19
|
+
when: {
|
|
20
|
+
and: [
|
|
21
|
+
{ isUnlocked: { eq: true } },
|
|
22
|
+
{ isPublished: { eq: true } },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
read: ['content'],
|
|
26
|
+
})
|
|
27
|
+
export class Article {
|
|
28
|
+
isPublished = def.boolColumn()
|
|
29
|
+
isUnlocked = def.boolColumn()
|
|
30
|
+
title = def.stringColumn()
|
|
31
|
+
content = def.stringColumn()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
test('row level predicate includes main predicate', async () => {
|
|
38
|
+
const schema = createSchema(RowPredicateSubset)
|
|
39
|
+
|
|
40
|
+
const permissions = new PermissionFactory(schema.model).create(schema.acl, ['reader'])
|
|
41
|
+
|
|
42
|
+
await execute({
|
|
43
|
+
schema: schema.model,
|
|
44
|
+
permissions: permissions,
|
|
45
|
+
variables: {},
|
|
46
|
+
query: GQL`
|
|
47
|
+
query {
|
|
48
|
+
listArticle {
|
|
49
|
+
id
|
|
50
|
+
title
|
|
51
|
+
content
|
|
52
|
+
}
|
|
53
|
+
}`,
|
|
54
|
+
executes: [
|
|
55
|
+
{
|
|
56
|
+
sql: SQL`SELECT
|
|
57
|
+
"root_"."id" AS "root_id",
|
|
58
|
+
TRUE AS "root___predicate_isPublished_eq_true",
|
|
59
|
+
"root_"."title" AS "root_title",
|
|
60
|
+
"root_"."is_unlocked" = ? AS "root___predicate_and_isUnlocked_eq_true_isPubl",
|
|
61
|
+
"root_"."content" AS "root_content"
|
|
62
|
+
FROM "public"."article" AS "root_"
|
|
63
|
+
WHERE "root_"."is_published" = ?`,
|
|
64
|
+
parameters: [true, true],
|
|
65
|
+
response: {
|
|
66
|
+
rows: [
|
|
67
|
+
{
|
|
68
|
+
root_id: testUuid(1),
|
|
69
|
+
root___predicate_isPublished_eq_true: true,
|
|
70
|
+
root_title: 'Hello',
|
|
71
|
+
root___predicate_and_isUnlocked_eq_true_isPubl: false,
|
|
72
|
+
root_content: 'Foo bar',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
root_id: testUuid(2),
|
|
76
|
+
root___predicate_isPublished_eq_true: true,
|
|
77
|
+
root_title: 'Hello 2',
|
|
78
|
+
root___predicate_and_isUnlocked_eq_true_isPubl: true,
|
|
79
|
+
root_content: 'Lorem ipsum',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
return: {
|
|
86
|
+
data: {
|
|
87
|
+
listArticle: [
|
|
88
|
+
{
|
|
89
|
+
id: testUuid(1),
|
|
90
|
+
title: 'Hello',
|
|
91
|
+
content: null,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: testUuid(2),
|
|
95
|
+
title: 'Hello 2',
|
|
96
|
+
content: 'Lorem ipsum',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
|