@contember/engine-content-api 1.2.0-alpha.13 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contember/engine-content-api",
3
- "version": "1.2.0-alpha.13",
3
+ "version": "1.2.0-alpha.14",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/src/index.js",
6
6
  "typings": "dist/src/index.d.ts",
@@ -10,19 +10,19 @@
10
10
  "test": "vitest --no-threads"
11
11
  },
12
12
  "dependencies": {
13
- "@contember/database": "^1.2.0-alpha.13",
14
- "@contember/dic": "^1.2.0-alpha.13",
15
- "@contember/engine-common": "^1.2.0-alpha.13",
16
- "@contember/graphql-utils": "^1.2.0-alpha.13",
17
- "@contember/schema": "^1.2.0-alpha.13",
18
- "@contember/schema-utils": "^1.2.0-alpha.13",
13
+ "@contember/database": "^1.2.0-alpha.14",
14
+ "@contember/dic": "^1.2.0-alpha.14",
15
+ "@contember/engine-common": "^1.2.0-alpha.14",
16
+ "@contember/graphql-utils": "^1.2.0-alpha.14",
17
+ "@contember/schema": "^1.2.0-alpha.14",
18
+ "@contember/schema-utils": "^1.2.0-alpha.14",
19
19
  "@graphql-tools/schema": "^8.3.5",
20
20
  "graphql-tag": "^2.12.5"
21
21
  },
22
22
  "devDependencies": {
23
- "@contember/database-tester": "^1.2.0-alpha.13",
24
- "@contember/engine-api-tester": "^1.2.0-alpha.13",
25
- "@contember/schema-definition": "^1.2.0-alpha.13",
23
+ "@contember/database-tester": "^1.2.0-alpha.14",
24
+ "@contember/engine-api-tester": "^1.2.0-alpha.14",
25
+ "@contember/schema-definition": "^1.2.0-alpha.14",
26
26
  "@graphql-codegen/cli": "^2.6.2",
27
27
  "@graphql-codegen/typescript": "^2.5.1",
28
28
  "@graphql-codegen/typescript-operations": "^2.4.2",
@@ -5,8 +5,8 @@ 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
  }
@@ -16,6 +16,8 @@ export interface WhereOptimizationHints {
16
16
  evaluatedPredicates?: Input.OptionalWhere[]
17
17
  }
18
18
 
19
+ type OptimizedOperand = Input.OptionalWhere | undefined | boolean
20
+
19
21
  export class WhereOptimizer {
20
22
  private static eliminableRelations = new Set<Model.AnyRelationContext['type']>(['oneHasMany', 'oneHasOneInverse', 'oneHasOneOwning'])
21
23
 
@@ -32,7 +34,7 @@ export class WhereOptimizer {
32
34
  if (!WhereOptimizer.eliminableRelations.has(el.type)) {
33
35
  processedRelationPath = []
34
36
  } else {
35
- processedRelationPath.push({ ...el, canEliminate: true, isLast: Number(i) === relationPath.length })
37
+ processedRelationPath.push({ context: el, canEliminate: true, isLast: Number(i) === relationPath.length })
36
38
  }
37
39
  }
38
40
  let result = this.optimizeWhere(where, entity, processedRelationPath)
@@ -40,12 +42,16 @@ export class WhereOptimizer {
40
42
  if (typeof result === 'boolean') {
41
43
  return { [entity.primary]: { [result ? 'always' : 'never']: true } }
42
44
  }
43
- const resultTracker = { count: 0 }
45
+ let changed = false
44
46
  for (const evaluated of evaluatedPredicates) {
45
47
  const evaluatedPredicate = this.optimize(evaluated, entity)
46
- result = replaceWhere(result, evaluatedPredicate, { [entity.primary]: { always: true } }, resultTracker)
48
+ const newResult = replaceWhere(result, evaluatedPredicate, { [entity.primary]: { always: true } })
49
+ if (newResult !== result) {
50
+ result = newResult
51
+ changed = true
52
+ }
47
53
  }
48
- if (resultTracker.count > 0) {
54
+ if (changed) {
49
55
  return this.optimize(result, entity)
50
56
  }
51
57
 
@@ -53,58 +59,95 @@ export class WhereOptimizer {
53
59
  }
54
60
 
55
61
  private optimizeWhere(where: Input.OptionalWhere, entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
56
- return this.optimizeAnd(
57
- Object.entries(where).map(([key, value]) => {
58
- if (value === undefined || value === null) {
59
- return undefined
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
+ }
60
92
 
61
- } else if (key === 'and') {
62
- return this.optimizeAnd((value as readonly Input.OptionalWhere[]).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))
63
95
 
64
- } else if (key === 'or') {
65
- return this.optimizeOr((value as readonly Input.OptionalWhere[]).map(it => this.optimizeWhere(it, entity, relationPath)), entity, relationPath)
96
+ } else {
97
+ operand = this.resolveFieldValue(entity, key, value, relationPath)
98
+ }
66
99
 
67
- } else if (key === 'not') {
68
- return optimizeNot(this.optimizeWhere(value as Input.OptionalWhere, entity, relationPath))
100
+ if (operand === false) {
101
+ return false
69
102
 
70
- } else {
71
- return this.resolveFieldValue(entity, key, value, relationPath)
72
- }
73
- }),
74
- entity,
75
- relationPath,
76
- )
103
+ } else if (operand !== undefined) {
104
+ operands.push(operand)
105
+ }
106
+ }
107
+ return this.optimizeAnd(operands, entity, relationPath)
77
108
  }
78
109
 
79
- private optimizeOr(operands: readonly (Input.OptionalWhere | undefined | boolean)[], entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
110
+ private optimizeOr(operands: readonly OptimizedOperand[], entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
80
111
  const optimized = optimizeOr(operands)
81
112
  if (typeof optimized === 'boolean' || !Array.isArray(optimized.or)) {
82
113
  return optimized
83
114
  }
84
115
  const result = this.crossOptimize(optimized.or, entity, { never: true }, relationPath)
116
+ if (result === optimized.or) {
117
+ return optimized
118
+ }
85
119
  return optimizeOr(result)
86
120
  }
87
121
 
88
- private optimizeAnd(operands: readonly (Input.OptionalWhere | undefined | boolean)[], entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
122
+ private optimizeAnd(operands: readonly OptimizedOperand[], entity: Model.Entity, relationPath: ExtendedRelationContext[]): Input.OptionalWhere | boolean {
89
123
  const optimized = optimizeAnd(operands)
90
124
  if (typeof optimized === 'boolean' || !Array.isArray(optimized.and)) {
91
125
  return optimized
92
126
  }
93
127
  const result = this.crossOptimize(optimized.and, entity, { always: true }, relationPath)
128
+ if (result === optimized.and) {
129
+ return optimized
130
+ }
94
131
  return optimizeAnd(result)
95
132
  }
96
133
 
97
134
  private crossOptimize(operands: Input.OptionalWhere[], entity: Model.Entity, replacement: Input.Condition, relationPath: ExtendedRelationContext[]): (Input.OptionalWhere | boolean)[] {
98
- const result: (Input.OptionalWhere | boolean)[] = [...operands]
135
+ let result: (Input.OptionalWhere | boolean)[] = operands
136
+ let copied = false
99
137
  const count = result.length
100
138
  for (let i = 0; i < count; i++) {
101
139
  for (let j = 0; j < count; j++) {
102
140
  const a = result[j]
103
141
  const b = result[i]
104
142
  if (i !== j && typeof a !== 'boolean' && typeof b !== 'boolean') {
105
- const tracker = { count: 0 }
106
- const elResult = replaceWhere(a, b, { [entity.primary]: replacement }, tracker)
107
- 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
+ }
108
151
  }
109
152
  }
110
153
  }
@@ -123,21 +166,22 @@ export class WhereOptimizer {
123
166
  return { [key]: optimizedCondition }
124
167
  },
125
168
  visitRelation: context => {
126
- const { targetEntity, relation } = context
127
169
  let where = value as Input.OptionalWhere
128
170
  const newRelationPath: ExtendedRelationContext[] = [...relationPath]
129
- 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) {
130
173
  const item = newRelationPath.pop()
131
- if (item?.canEliminate && (item.type === 'oneHasMany' || item.type === 'oneHasOneOwning' || item.type === 'oneHasOneInverse')) {
132
- where = replaceWhere(where, { [targetEntity.primary]: { isNull: true } }, { [targetEntity.primary]: { never: true } })
133
- 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 } })
134
178
  }
135
179
  } else {
136
180
  // first level relation - we are sure root exists
137
- const canEliminate = relationPath.length === 0 || relationPath[relationPath.length - 1].isLast
138
- newRelationPath.push({ ...context, isLast: false, canEliminate })
181
+ const canEliminate = length === 0 || relationPath[length - 1].isLast
182
+ newRelationPath.push({ context, isLast: false, canEliminate })
139
183
  }
140
- const optimizedWhere = this.optimizeWhere(where, targetEntity, newRelationPath)
184
+ const optimizedWhere = this.optimizeWhere(where, context.targetEntity, newRelationPath)
141
185
 
142
186
  if (typeof optimizedWhere === 'boolean') {
143
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.OptionalWhere, find: Input.OptionalWhere, replace: Input.OptionalWhere, resultTracker: ReplaceWhereResult = { count: 0 }): Input.OptionalWhere => {
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.OptionalWhere> = {}
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.OptionalWhere, 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.OptionalWhere[]).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
  }