@contember/engine-content-api 1.2.0-alpha.12 → 1.2.0-alpha.14
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 +76 -23
- package/dist/src/mapper/select/optimizer/WhereOptimizer.js.map +1 -1
- package/dist/src/mapper/select/optimizer/WhereReplacer.d.ts +1 -4
- package/dist/src/mapper/select/optimizer/WhereReplacer.d.ts.map +1 -1
- package/dist/src/mapper/select/optimizer/WhereReplacer.js +27 -9
- 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 +96 -40
- package/src/mapper/select/optimizer/WhereReplacer.ts +27 -15
- 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
|
}
|
|
@@ -5,12 +5,19 @@ import { optimizeAnd, optimizeNot, optimizeOr } from './helpers'
|
|
|
5
5
|
import { replaceWhere } from './WhereReplacer'
|
|
6
6
|
|
|
7
7
|
type ExtendedRelationContext =
|
|
8
|
-
& Model.AnyRelationContext
|
|
9
8
|
& {
|
|
9
|
+
context: Model.AnyRelationContext
|
|
10
10
|
canEliminate: boolean
|
|
11
11
|
isLast: boolean
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface WhereOptimizationHints {
|
|
15
|
+
relationPath?: Model.AnyRelationContext[]
|
|
16
|
+
evaluatedPredicates?: Input.OptionalWhere[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type OptimizedOperand = Input.OptionalWhere | undefined | boolean
|
|
20
|
+
|
|
14
21
|
export class WhereOptimizer {
|
|
15
22
|
private static eliminableRelations = new Set<Model.AnyRelationContext['type']>(['oneHasMany', 'oneHasOneInverse', 'oneHasOneOwning'])
|
|
16
23
|
|
|
@@ -20,79 +27,127 @@ export class WhereOptimizer {
|
|
|
20
27
|
) {
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
public optimize(where: Input.OptionalWhere, entity: Model.Entity, relationPath
|
|
30
|
+
public optimize(where: Input.OptionalWhere, entity: Model.Entity, { relationPath = [], evaluatedPredicates = [] }: WhereOptimizationHints = {}): Input.OptionalWhere {
|
|
24
31
|
let processedRelationPath: ExtendedRelationContext[] = []
|
|
25
32
|
for (const i in relationPath) {
|
|
26
33
|
const el = relationPath[i]
|
|
27
34
|
if (!WhereOptimizer.eliminableRelations.has(el.type)) {
|
|
28
35
|
processedRelationPath = []
|
|
29
36
|
} else {
|
|
30
|
-
processedRelationPath.push({
|
|
37
|
+
processedRelationPath.push({ context: el, canEliminate: true, isLast: Number(i) === relationPath.length })
|
|
31
38
|
}
|
|
32
|
-
processedRelationPath.push({ ...el, canEliminate: true, isLast: Number(i) === relationPath.length })
|
|
33
39
|
}
|
|
34
|
-
|
|
40
|
+
let result = this.optimizeWhere(where, entity, processedRelationPath)
|
|
35
41
|
|
|
36
42
|
if (typeof result === 'boolean') {
|
|
37
43
|
return { [entity.primary]: { [result ? 'always' : 'never']: true } }
|
|
38
44
|
}
|
|
45
|
+
let changed = false
|
|
46
|
+
for (const evaluated of evaluatedPredicates) {
|
|
47
|
+
const evaluatedPredicate = this.optimize(evaluated, entity)
|
|
48
|
+
const newResult = replaceWhere(result, evaluatedPredicate, { [entity.primary]: { always: true } })
|
|
49
|
+
if (newResult !== result) {
|
|
50
|
+
result = newResult
|
|
51
|
+
changed = true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (changed) {
|
|
55
|
+
return this.optimize(result, entity)
|
|
56
|
+
}
|
|
39
57
|
|
|
40
58
|
return result
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
private optimizeWhere(where: Input.OptionalWhere, entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
private optimizeWhere(where: Input.OptionalWhere, entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
|
|
62
|
+
const operands: OptimizedOperand[] = []
|
|
63
|
+
|
|
64
|
+
for (const key in where) {
|
|
65
|
+
const value = where[key]
|
|
66
|
+
let operand: OptimizedOperand = undefined
|
|
67
|
+
if (value === undefined || value === null) {
|
|
68
|
+
continue
|
|
69
|
+
} else if (key === 'and') {
|
|
70
|
+
const innerOperands: (Input.OptionalWhere | boolean)[] = []
|
|
71
|
+
for (const innerValue of value as readonly Input.OptionalWhere[]) {
|
|
72
|
+
const innerOperand = this.optimizeWhere(innerValue, entity, relationPath)
|
|
73
|
+
if (innerOperand === false) {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
innerOperands.push(innerOperand)
|
|
77
|
+
}
|
|
78
|
+
operand = this.optimizeAnd(innerOperands, entity, relationPath)
|
|
79
|
+
} else if (key === 'or') {
|
|
80
|
+
const innerOperands: (Input.OptionalWhere | boolean)[] = []
|
|
81
|
+
for (const innerValue of value as readonly Input.OptionalWhere[]) {
|
|
82
|
+
const innerOperand = this.optimizeWhere(innerValue, entity, relationPath)
|
|
83
|
+
if (innerOperand === true) {
|
|
84
|
+
operand = true
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
innerOperands.push(innerOperand)
|
|
88
|
+
}
|
|
89
|
+
if (operand !== true) {
|
|
90
|
+
operand = this.optimizeOr(innerOperands, entity, relationPath)
|
|
91
|
+
}
|
|
48
92
|
|
|
49
|
-
|
|
50
|
-
|
|
93
|
+
} else if (key === 'not') {
|
|
94
|
+
operand = optimizeNot(this.optimizeWhere(value as Input.OptionalWhere, entity, relationPath))
|
|
51
95
|
|
|
52
|
-
|
|
53
|
-
|
|
96
|
+
} else {
|
|
97
|
+
operand = this.resolveFieldValue(entity, key, value, relationPath)
|
|
98
|
+
}
|
|
54
99
|
|
|
55
|
-
|
|
56
|
-
|
|
100
|
+
if (operand === false) {
|
|
101
|
+
return false
|
|
57
102
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
relationPath,
|
|
64
|
-
)
|
|
103
|
+
} else if (operand !== undefined) {
|
|
104
|
+
operands.push(operand)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return this.optimizeAnd(operands, entity, relationPath)
|
|
65
108
|
}
|
|
66
109
|
|
|
67
|
-
private optimizeOr(operands: readonly
|
|
110
|
+
private optimizeOr(operands: readonly OptimizedOperand[], entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
|
|
68
111
|
const optimized = optimizeOr(operands)
|
|
69
112
|
if (typeof optimized === 'boolean' || !Array.isArray(optimized.or)) {
|
|
70
113
|
return optimized
|
|
71
114
|
}
|
|
72
115
|
const result = this.crossOptimize(optimized.or, entity, { never: true }, relationPath)
|
|
116
|
+
if (result === optimized.or) {
|
|
117
|
+
return optimized
|
|
118
|
+
}
|
|
73
119
|
return optimizeOr(result)
|
|
74
120
|
}
|
|
75
121
|
|
|
76
|
-
private optimizeAnd(operands: readonly
|
|
122
|
+
private optimizeAnd(operands: readonly OptimizedOperand[], entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
|
|
77
123
|
const optimized = optimizeAnd(operands)
|
|
78
124
|
if (typeof optimized === 'boolean' || !Array.isArray(optimized.and)) {
|
|
79
125
|
return optimized
|
|
80
126
|
}
|
|
81
127
|
const result = this.crossOptimize(optimized.and, entity, { always: true }, relationPath)
|
|
128
|
+
if (result === optimized.and) {
|
|
129
|
+
return optimized
|
|
130
|
+
}
|
|
82
131
|
return optimizeAnd(result)
|
|
83
132
|
}
|
|
84
133
|
|
|
85
|
-
private crossOptimize(operands: Input.
|
|
86
|
-
|
|
134
|
+
private crossOptimize(operands: Input.OptionalWhere[], entity: Model.Entity, replacement: Input.Condition, relationPath: ExtendedRelationContext[]): (Input.OptionalWhere | boolean)[] {
|
|
135
|
+
let result: (Input.OptionalWhere | boolean)[] = operands
|
|
136
|
+
let copied = false
|
|
87
137
|
const count = result.length
|
|
88
138
|
for (let i = 0; i < count; i++) {
|
|
89
139
|
for (let j = 0; j < count; j++) {
|
|
90
140
|
const a = result[j]
|
|
91
141
|
const b = result[i]
|
|
92
142
|
if (i !== j && typeof a !== 'boolean' && typeof b !== 'boolean') {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
143
|
+
const elResult = replaceWhere(a, b, { [entity.primary]: replacement })
|
|
144
|
+
if (elResult !== a) {
|
|
145
|
+
if (!copied) {
|
|
146
|
+
result = [...result]
|
|
147
|
+
copied = true
|
|
148
|
+
}
|
|
149
|
+
result[j] = this.optimizeWhere(elResult, entity, relationPath)
|
|
150
|
+
}
|
|
96
151
|
}
|
|
97
152
|
}
|
|
98
153
|
}
|
|
@@ -100,7 +155,7 @@ export class WhereOptimizer {
|
|
|
100
155
|
}
|
|
101
156
|
|
|
102
157
|
private resolveFieldValue(entity: Model.Entity, key: string, value: Input.OptionalWhere[string], relationPath: ExtendedRelationContext[]) {
|
|
103
|
-
return acceptFieldVisitor<Input.
|
|
158
|
+
return acceptFieldVisitor<Input.OptionalWhere | boolean>(this.model, entity, key, {
|
|
104
159
|
visitColumn: () => {
|
|
105
160
|
const optimizedCondition = this.conditionOptimizer.optimize(value as Input.Condition)
|
|
106
161
|
|
|
@@ -111,21 +166,22 @@ export class WhereOptimizer {
|
|
|
111
166
|
return { [key]: optimizedCondition }
|
|
112
167
|
},
|
|
113
168
|
visitRelation: context => {
|
|
114
|
-
|
|
115
|
-
let where = value as Input.Where
|
|
169
|
+
let where = value as Input.OptionalWhere
|
|
116
170
|
const newRelationPath: ExtendedRelationContext[] = [...relationPath]
|
|
117
|
-
|
|
171
|
+
const length = relationPath.length
|
|
172
|
+
if (length > 0 && relationPath[length - 1].context.targetRelation?.name === context.relation.name) {
|
|
118
173
|
const item = newRelationPath.pop()
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
where = replaceWhere(where, { [targetEntity.primary]: { isNull:
|
|
174
|
+
const type = item!.context.type
|
|
175
|
+
if (item?.canEliminate && (type === 'oneHasMany' || type === 'oneHasOneOwning' || type === 'oneHasOneInverse')) {
|
|
176
|
+
where = replaceWhere(where, { [context.targetEntity.primary]: { isNull: true } }, { [context.targetEntity.primary]: { never: true } })
|
|
177
|
+
where = replaceWhere(where, { [context.targetEntity.primary]: { isNull: false } }, { [context.targetEntity.primary]: { always: true } })
|
|
122
178
|
}
|
|
123
179
|
} else {
|
|
124
180
|
// first level relation - we are sure root exists
|
|
125
|
-
const canEliminate =
|
|
126
|
-
newRelationPath.push({
|
|
181
|
+
const canEliminate = length === 0 || relationPath[length - 1].isLast
|
|
182
|
+
newRelationPath.push({ context, isLast: false, canEliminate })
|
|
127
183
|
}
|
|
128
|
-
const optimizedWhere = this.optimizeWhere(where, targetEntity, newRelationPath)
|
|
184
|
+
const optimizedWhere = this.optimizeWhere(where, context.targetEntity, newRelationPath)
|
|
129
185
|
|
|
130
186
|
if (typeof optimizedWhere === 'boolean') {
|
|
131
187
|
return optimizedWhere
|
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
import { Input, Writable } from '@contember/schema'
|
|
2
2
|
import deepEqual from 'fast-deep-equal'
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
count: number
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const replaceWhere = (subject: Input.Where, find: Input.Where, replace: Input.Where, resultTracker: ReplaceWhereResult = { count: 0 }): Input.Where => {
|
|
4
|
+
export const replaceWhere = (subject: Input.OptionalWhere, find: Input.OptionalWhere, replace: Input.OptionalWhere): Input.OptionalWhere => {
|
|
9
5
|
if (deepEqual(subject, find)) {
|
|
10
|
-
resultTracker.count++
|
|
11
6
|
return replace
|
|
12
7
|
}
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
let result: Writable<Input.OptionalWhere> = subject
|
|
9
|
+
let copied = false
|
|
10
|
+
for (const key in subject) {
|
|
11
|
+
const value = subject[key]
|
|
15
12
|
if (key === 'not') {
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const newNot = replaceWhere(value as Input.OptionalWhere, find, replace)
|
|
14
|
+
if (newNot !== value) {
|
|
15
|
+
if (!copied) {
|
|
16
|
+
result = { ...result }
|
|
17
|
+
}
|
|
18
|
+
result['not'] = newNot
|
|
19
|
+
}
|
|
18
20
|
} else if (key === 'and' || key === 'or') {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const items: Input.OptionalWhere[] = []
|
|
22
|
+
let itemChanged = false
|
|
23
|
+
for (const el of value as Input.OptionalWhere[]) {
|
|
24
|
+
const newEl = replaceWhere(el, find, replace)
|
|
25
|
+
if (newEl !== el) {
|
|
26
|
+
itemChanged = true
|
|
27
|
+
}
|
|
28
|
+
items.push(newEl)
|
|
29
|
+
}
|
|
30
|
+
if (itemChanged) {
|
|
31
|
+
if (!copied) {
|
|
32
|
+
result = { ...result }
|
|
33
|
+
}
|
|
34
|
+
result[key] = items
|
|
35
|
+
}
|
|
23
36
|
}
|
|
24
|
-
|
|
25
37
|
}
|
|
26
38
|
return result
|
|
27
39
|
}
|
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
|
+
|