@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.
Files changed (101) hide show
  1. package/dist/src/acl/EvaluatedPredicateReplacer.d.ts +2 -2
  2. package/dist/src/acl/EvaluatedPredicateReplacer.d.ts.map +1 -1
  3. package/dist/src/acl/EvaluatedPredicateReplacer.js.map +1 -1
  4. package/dist/src/acl/PredicateFactory.d.ts +9 -3
  5. package/dist/src/acl/PredicateFactory.d.ts.map +1 -1
  6. package/dist/src/acl/PredicateFactory.js +42 -24
  7. package/dist/src/acl/PredicateFactory.js.map +1 -1
  8. package/dist/src/acl/PredicatesInjector.d.ts.map +1 -1
  9. package/dist/src/acl/PredicatesInjector.js +1 -0
  10. package/dist/src/acl/PredicatesInjector.js.map +1 -1
  11. package/dist/src/extensions/hasManyToHasOneReducer/HasManyToHasOneReducerExecutionHandler.js +31 -27
  12. package/dist/src/extensions/hasManyToHasOneReducer/HasManyToHasOneReducerExecutionHandler.js.map +1 -1
  13. package/dist/src/extensions/paginatedHasMany/PaginatedHasManyExecutionHandler.js +18 -14
  14. package/dist/src/extensions/paginatedHasMany/PaginatedHasManyExecutionHandler.js.map +1 -1
  15. package/dist/src/mapper/JunctionTableManager.js.map +1 -1
  16. package/dist/src/mapper/Mapper.js +3 -3
  17. package/dist/src/mapper/Mapper.js.map +1 -1
  18. package/dist/src/mapper/MapperContainer.d.ts.map +1 -1
  19. package/dist/src/mapper/MapperContainer.js +2 -2
  20. package/dist/src/mapper/MapperContainer.js.map +1 -1
  21. package/dist/src/mapper/delete/DeleteExecutor.js +1 -1
  22. package/dist/src/mapper/delete/DeleteExecutor.js.map +1 -1
  23. package/dist/src/mapper/insert/InsertBuilder.d.ts +1 -1
  24. package/dist/src/mapper/insert/InsertBuilder.d.ts.map +1 -1
  25. package/dist/src/mapper/insert/InsertBuilder.js.map +1 -1
  26. package/dist/src/mapper/select/RelationFetcher.js +1 -1
  27. package/dist/src/mapper/select/RelationFetcher.js.map +1 -1
  28. package/dist/src/mapper/select/SelectBuilder.d.ts +3 -1
  29. package/dist/src/mapper/select/SelectBuilder.d.ts.map +1 -1
  30. package/dist/src/mapper/select/SelectBuilder.js +50 -12
  31. package/dist/src/mapper/select/SelectBuilder.js.map +1 -1
  32. package/dist/src/mapper/select/SelectExecutionHandler.d.ts +15 -4
  33. package/dist/src/mapper/select/SelectExecutionHandler.d.ts.map +1 -1
  34. package/dist/src/mapper/select/SelectHydrator.d.ts +3 -2
  35. package/dist/src/mapper/select/SelectHydrator.d.ts.map +1 -1
  36. package/dist/src/mapper/select/SelectHydrator.js +4 -5
  37. package/dist/src/mapper/select/SelectHydrator.js.map +1 -1
  38. package/dist/src/mapper/select/WhereBuilder.d.ts +3 -3
  39. package/dist/src/mapper/select/WhereBuilder.d.ts.map +1 -1
  40. package/dist/src/mapper/select/WhereBuilder.js +4 -4
  41. package/dist/src/mapper/select/WhereBuilder.js.map +1 -1
  42. package/dist/src/mapper/select/handlers/FieldsVisitor.d.ts +1 -0
  43. package/dist/src/mapper/select/handlers/FieldsVisitor.d.ts.map +1 -1
  44. package/dist/src/mapper/select/handlers/FieldsVisitor.js +118 -91
  45. package/dist/src/mapper/select/handlers/FieldsVisitor.js.map +1 -1
  46. package/dist/src/mapper/select/handlers/MetaHandler.d.ts.map +1 -1
  47. package/dist/src/mapper/select/handlers/MetaHandler.js +6 -11
  48. package/dist/src/mapper/select/handlers/MetaHandler.js.map +1 -1
  49. package/dist/src/mapper/select/optimizer/WhereOptimizer.d.ts +5 -1
  50. package/dist/src/mapper/select/optimizer/WhereOptimizer.d.ts.map +1 -1
  51. package/dist/src/mapper/select/optimizer/WhereOptimizer.js +76 -23
  52. package/dist/src/mapper/select/optimizer/WhereOptimizer.js.map +1 -1
  53. package/dist/src/mapper/select/optimizer/WhereReplacer.d.ts +1 -4
  54. package/dist/src/mapper/select/optimizer/WhereReplacer.d.ts.map +1 -1
  55. package/dist/src/mapper/select/optimizer/WhereReplacer.js +27 -9
  56. package/dist/src/mapper/select/optimizer/WhereReplacer.js.map +1 -1
  57. package/dist/src/tsconfig.tsbuildinfo +1 -1
  58. package/dist/tests/cases/integration/graphQlRequests/query/acl/differentRowAndFieldPredicate.test.js +51 -4
  59. package/dist/tests/cases/integration/graphQlRequests/query/acl/differentRowAndFieldPredicate.test.js.map +1 -1
  60. package/dist/tests/cases/integration/graphQlRequests/query/acl/fieldRestrictedByLocale.test.js +26 -25
  61. package/dist/tests/cases/integration/graphQlRequests/query/acl/fieldRestrictedByLocale.test.js.map +1 -1
  62. package/dist/tests/cases/integration/graphQlRequests/query/acl/nestedPredicate.test.js +9 -6
  63. package/dist/tests/cases/integration/graphQlRequests/query/acl/nestedPredicate.test.js.map +1 -1
  64. package/dist/tests/cases/integration/graphQlRequests/query/acl/rowPredicateSubset.test.d.ts +2 -0
  65. package/dist/tests/cases/integration/graphQlRequests/query/acl/rowPredicateSubset.test.d.ts.map +1 -0
  66. package/dist/tests/cases/integration/graphQlRequests/query/acl/rowPredicateSubset.test.js +109 -0
  67. package/dist/tests/cases/integration/graphQlRequests/query/acl/rowPredicateSubset.test.js.map +1 -0
  68. package/dist/tests/cases/integration/graphQlRequests/query/graphqlFeatures/alias.test.js +9 -13
  69. package/dist/tests/cases/integration/graphQlRequests/query/graphqlFeatures/alias.test.js.map +1 -1
  70. package/dist/tests/cases/unit/predicatesInjector.test.js +31 -3
  71. package/dist/tests/cases/unit/predicatesInjector.test.js.map +1 -1
  72. package/dist/tests/cases/unit/whereOptimizer.test.js +24 -21
  73. package/dist/tests/cases/unit/whereOptimizer.test.js.map +1 -1
  74. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  75. package/package.json +10 -10
  76. package/src/acl/EvaluatedPredicateReplacer.ts +4 -4
  77. package/src/acl/PredicateFactory.ts +59 -36
  78. package/src/acl/PredicatesInjector.ts +2 -1
  79. package/src/extensions/hasManyToHasOneReducer/HasManyToHasOneReducerExecutionHandler.ts +5 -5
  80. package/src/extensions/paginatedHasMany/PaginatedHasManyExecutionHandler.ts +5 -5
  81. package/src/mapper/JunctionTableManager.ts +3 -3
  82. package/src/mapper/Mapper.ts +3 -3
  83. package/src/mapper/MapperContainer.ts +2 -1
  84. package/src/mapper/delete/DeleteExecutor.ts +1 -1
  85. package/src/mapper/insert/InsertBuilder.ts +2 -2
  86. package/src/mapper/select/RelationFetcher.ts +1 -1
  87. package/src/mapper/select/SelectBuilder.ts +69 -13
  88. package/src/mapper/select/SelectExecutionHandler.ts +15 -8
  89. package/src/mapper/select/SelectHydrator.ts +13 -7
  90. package/src/mapper/select/WhereBuilder.ts +5 -5
  91. package/src/mapper/select/handlers/FieldsVisitor.ts +53 -67
  92. package/src/mapper/select/handlers/MetaHandler.ts +6 -17
  93. package/src/mapper/select/optimizer/WhereOptimizer.ts +96 -40
  94. package/src/mapper/select/optimizer/WhereReplacer.ts +27 -15
  95. package/tests/cases/integration/graphQlRequests/query/acl/differentRowAndFieldPredicate.test.ts +54 -4
  96. package/tests/cases/integration/graphQlRequests/query/acl/fieldRestrictedByLocale.test.ts +26 -25
  97. package/tests/cases/integration/graphQlRequests/query/acl/nestedPredicate.test.ts +9 -6
  98. package/tests/cases/integration/graphQlRequests/query/acl/rowPredicateSubset.test.ts +104 -0
  99. package/tests/cases/integration/graphQlRequests/query/graphqlFeatures/alias.test.ts +9 -13
  100. package/tests/cases/unit/predicatesInjector.test.ts +37 -7
  101. 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
- this.executionContext.addColumn(qb => {
22
- const tableAlias = columnPath.back().alias
23
- const columnAlias = columnPath.alias
21
+ const tableAlias = columnPath.back().alias
22
+ const columnAlias = columnPath.alias
24
23
 
25
- const fieldPredicate = this.predicateFactory.shouldApplyCellLevelPredicate(
26
- entity,
27
- Acl.Operation.read,
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
- mapper: this.mapper,
64
- field: field,
65
- relationContext,
66
- relationPath: this.relationPath,
67
- ids: ids,
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, path } = context
35
+ const { entity } = context
36
36
  if (entity.primary === fieldName) {
37
37
  return
38
38
  }
39
- const fieldPredicate = this.predicateFactory.create(entity, operation, [fieldName])
40
- context.addColumn(qb => {
41
- return this.whereBuilder.buildAdvanced(entity, path.back(), fieldPredicate, cb =>
42
- qb.select(
43
- expr =>
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: Model.AnyRelationContext[] = []): Input.Where {
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({ ...el, canEliminate: true, isLast: Number(i) === relationPath.length })
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
- const result = this.optimizeWhere(where, entity, processedRelationPath)
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.Where | boolean {
44
- return this.optimizeAnd(
45
- Object.entries(where).map(([key, value]) => {
46
- if (value === undefined || value === null) {
47
- return undefined
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
- } else if (key === 'and') {
50
- return this.optimizeAnd((value as readonly Input.Where[]).map(it => this.optimizeWhere(it, entity, relationPath)), entity, relationPath)
93
+ } else if (key === 'not') {
94
+ operand = optimizeNot(this.optimizeWhere(value as Input.OptionalWhere, entity, relationPath))
51
95
 
52
- } else if (key === 'or') {
53
- return this.optimizeOr((value as readonly Input.Where[]).map(it => this.optimizeWhere(it, entity, relationPath)), entity, relationPath)
96
+ } else {
97
+ operand = this.resolveFieldValue(entity, key, value, relationPath)
98
+ }
54
99
 
55
- } else if (key === 'not') {
56
- return optimizeNot(this.optimizeWhere(value as Input.Where, entity, relationPath))
100
+ if (operand === false) {
101
+ return false
57
102
 
58
- } else {
59
- return this.resolveFieldValue(entity, key, value, relationPath)
60
- }
61
- }),
62
- entity,
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 (Input.Where | undefined | boolean)[], entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.Where | boolean {
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 (Input.Where | undefined | boolean)[], entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.Where | boolean {
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.Where[], entity: Model.Entity, replacement: Input.Condition, relationPath: ExtendedRelationContext[]): (Input.Where | boolean)[] {
86
- const result: (Input.Where | boolean)[] = [...operands]
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 tracker = { count: 0 }
94
- const elResult = replaceWhere(a, b, { [entity.primary]: replacement }, tracker)
95
- result[j] = tracker.count > 0 ? this.optimizeWhere(elResult, entity, relationPath) : elResult
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.Where | boolean>(this.model, entity, key, {
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
- const { targetEntity, relation } = context
115
- let where = value as Input.Where
169
+ let where = value as Input.OptionalWhere
116
170
  const newRelationPath: ExtendedRelationContext[] = [...relationPath]
117
- if (relationPath.length && relationPath[relationPath.length - 1].targetRelation?.name === relation.name) {
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
- if (item?.canEliminate && (item.type === 'oneHasMany' || item.type === 'oneHasOneOwning' || item.type === 'oneHasOneInverse')) {
120
- where = replaceWhere(where, { [targetEntity.primary]: { isNull: true } }, { [targetEntity.primary]: { never: true } })
121
- where = replaceWhere(where, { [targetEntity.primary]: { isNull: false } }, { [targetEntity.primary]: { always: true } })
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 = relationPath.length === 0 || relationPath[relationPath.length - 1].isLast
126
- newRelationPath.push({ ...context, isLast: false, canEliminate })
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 interface ReplaceWhereResult {
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
- const result: Writable<Input.Where> = {}
14
- for (const [key, value] of Object.entries(subject)) {
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
- result['not'] = replaceWhere(value as Input.Where, find, replace, resultTracker)
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
- result[key] = (value as Input.Where[]).map(it => replaceWhere(it, find, replace, resultTracker))
20
-
21
- } else {
22
- result[key] = value
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
  }
@@ -41,16 +41,18 @@ test('different predicate on row and field level', async () => {
41
41
  executes: [
42
42
  {
43
43
  sql: SQL`
44
- select "root_"."id" as "root_id",
45
- case when "root_"."username" = ? then "root_"."username" else null end as "root_username"
46
- from "public"."author" as "root_"
47
- where not("root_"."username" is null)
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
+