@live-change/simple-query 0.9.162 → 0.9.163

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": "@live-change/simple-query",
3
- "version": "0.9.162",
3
+ "version": "0.9.163",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "type": "module",
24
24
  "dependencies": {
25
- "@live-change/framework": "^0.9.162",
25
+ "@live-change/framework": "^0.9.163",
26
26
  "pluralize": "^8.0.0"
27
27
  },
28
28
  "devDependencies": {
@@ -30,5 +30,5 @@
30
30
  "typedoc-plugin-markdown": "^4.6.3",
31
31
  "typedoc-plugin-rename-defaults": "^0.7.3"
32
32
  },
33
- "gitHead": "59cd1485ca6d76bd87a6634a92d6e661e5803d5e"
33
+ "gitHead": "c410e1dacd07daed9a5c55367abd7f19d9a575cc"
34
34
  }
package/src/query.ts CHANGED
@@ -2,7 +2,16 @@ import {
2
2
  PropertyDefinitionSpecification, ServiceDefinition, ServiceDefinitionSpecification
3
3
  } from "@live-change/framework"
4
4
 
5
- import { ModelDefinition, ForeignModelDefinition } from "@live-change/framework"
5
+ import {
6
+ ModelDefinition, ForeignModelDefinition, ContextBase, QueryParameters as FrameworkQueryParameters,
7
+ QueryDefinition as FrameworkQueryDefinition,
8
+ QueryDefinitionSpecification as FrameworkQueryDefinitionSpecification,
9
+ ViewDefinition,
10
+ EventDefinition,
11
+ TriggerDefinition,
12
+ ActionDefinition,
13
+ ViewDefinitionSpecificationDaoPath
14
+ } from "@live-change/framework"
6
15
 
7
16
  import { PropertyDefinition } from "@live-change/framework"
8
17
 
@@ -17,7 +26,7 @@ const simpleQueryCode = readFileSync(simpleQueryCodePath, 'utf8')
17
26
 
18
27
 
19
28
  interface Range {
20
- gt?: string
29
+ gt?: string
21
30
  gte?: string
22
31
  lt?: string
23
32
  lte?: string,
@@ -51,7 +60,6 @@ class RuleSource {
51
60
  dependsOn: RuleSource[]
52
61
  index: IndexInfo | null
53
62
 
54
-
55
63
  constructor(rule: QueryRule, input: QueryInput, source: QuerySource, type: string) {
56
64
  this.rule = rule
57
65
  this.input = input
@@ -94,6 +102,14 @@ interface QueryDefinitionSpecification {
94
102
  sources?: Record<string, QuerySource>,
95
103
  code?: QueryCode,
96
104
  update?: boolean,
105
+ id: Function | string,
106
+ timeout?: number,
107
+ requestTimeout?: number,
108
+ validation?: (parameters: FrameworkQueryParameters, context: ContextBase) => Promise<any>
109
+ view?: Record<string, any>
110
+ event?: Record<string, any>
111
+ trigger?: Record<string, any>
112
+ action?: Record<string, any>
97
113
  }
98
114
 
99
115
  class OutputMapping {
@@ -181,7 +197,8 @@ class IndexInfo {
181
197
  $_createQuery(service: ServiceDefinition<any>) {
182
198
  return new QueryDefinition(service, {
183
199
  name: this.name,
184
- properties: {}
200
+ properties: {},
201
+ id: (x:any) => x
185
202
  }, this.rules)
186
203
  }
187
204
  }
@@ -207,6 +224,8 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
207
224
  executionPlan: ExecutionStep[]
208
225
  indexPlan: ExecutionStep[]
209
226
 
227
+ preparedQuery: FrameworkQueryDefinition<FrameworkQueryDefinitionSpecification>
228
+
210
229
  constructor(
211
230
  serviceDefinition: ServiceDefinition<SDS>, definition: QueryDefinitionSpecification, rules: QueryRule[] = undefined
212
231
  ) {
@@ -340,7 +359,7 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
340
359
  for(const key in this.rules) {
341
360
  const rule = this.rules[key]
342
361
  console.log(`${indent} RULE ${key}:`)
343
- console.log(`${indent} ${queryDescription(rule, indent + ' ')}`)
362
+ console.log(`${indent} ${queryDescription(rule, indent + ' ')}`)
344
363
  console.log(`${indent} SOURCES:`)
345
364
  for(const ruleSource of this.ruleSources) {
346
365
  if(ruleSource.rule !== rule) continue
@@ -377,32 +396,33 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
377
396
  }
378
397
  }
379
398
 
380
- computeSourceExecutionPlan(source: RuleSource, resultParameters: string[]) {
381
- /// TODO: W poniższej linii jest bug, powinno pobierać odmienne source, a nie te bezpośrednio zależne.
382
- /// Poza tym przy pobieraniu wstecz nie koniecznie potrzeba indexów,
383
- /// może trzeba wprowadzić index-forward i index-backward ?
384
- /// A może trzeba wprowadzić analizę tego co mamy i co chcemy uzyskać w przyszłości ?
399
+ computeSourceExecutionPlan(source: RuleSource, resultParameters: string[]) {
400
+ /// TODO: Może trzeba wprowadzić analizę tego co mamy i co chcemy uzyskać w przyszłości ?
385
401
 
386
402
  const next = source.dependentBy.map(dependent => {
387
403
  const otherSource = this.ruleSources.find(s => s.rule === dependent.rule && s != dependent)
388
- return this.computeSourceExecutionPlan(otherSource, [...resultParameters, source.input.$alias])
404
+ const nextStep = this.computeSourceExecutionPlan(otherSource, [...resultParameters, source.input.$alias])
405
+ return nextStep
389
406
  })
390
407
 
391
408
  console.log("COMPUTING SOURCE EXECUTION PLAN FOR", source.input.$_toQueryDescription(), "WITH", resultParameters)
392
409
  console.log("RULE", queryDescription(source.rule, ' '))
393
410
 
394
411
  const ruleParameters = JSON.parse(JSON.stringify(source.rule.$_parametersJSON(resultParameters)))
412
+ console.log("RULE PARAMETERS", ruleParameters)
395
413
  if(source.index) {
396
414
  const indexExecution = {
397
415
  ...source.index.$_executionJSON(),
398
- by: ruleParameters[Object.keys(ruleParameters)[0]]
416
+ by: parameterEnsureRange(ruleParameters[Object.keys(ruleParameters)[0]])
399
417
  }
400
418
  const indexNext = [{
401
- operation: 'object',
402
- ...source.input.$_executionJSON(),
403
- by: {
404
- type: 'result',
405
- path: [indexExecution.alias, source.index.indexParts.at(-1).alias],
419
+ execution: {
420
+ operation: 'object',
421
+ ...source.input.$_executionJSON(),
422
+ by: {
423
+ type: 'result',
424
+ path: [indexExecution.alias, source.index.indexParts.at(-1).alias],
425
+ },
406
426
  },
407
427
  next
408
428
  }]
@@ -612,13 +632,34 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
612
632
  console.log("EXECUTION PLAN:")
613
633
  console.log(JSON.stringify(this.executionPlan, null, 2))
614
634
 
615
- process.exit(0)
616
- /// TODO: create indexes used by query
617
-
618
-
619
- /// TODO: prepare query
635
+ //process.exit(0)
620
636
 
621
- // process.exit(0)
637
+ const idFunctionCode = (typeof this.definition.id === 'string')
638
+ ? this.definition.id : `(${this.definition.id})`
639
+
640
+ console.log("ID FUNCTION", idFunctionCode)
641
+
642
+ this.preparedQuery = this.service.query({
643
+ name: this.definition.name,
644
+ properties: this.definition.properties,
645
+ returns: this.definition.returns,
646
+ code: simpleQueryCode,
647
+ sourceName: simpleQueryCodePath,
648
+ update: this.definition.update,
649
+ timeout: this.definition.timeout,
650
+ requestTimeout: this.definition.requestTimeout,
651
+ validation: this.definition.validation,
652
+ config: {
653
+ plan: this.executionPlan,
654
+ idFunction: idFunctionCode
655
+ },
656
+ view: this.definition.view,
657
+ trigger: this.definition.trigger,
658
+ action: this.definition.action,
659
+ event: this.definition.event
660
+ })
661
+
662
+ return this.preparedQuery
622
663
  }
623
664
 
624
665
  createIndex(name, mapping: OutputMapping[]) {
@@ -649,6 +690,22 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
649
690
  }
650
691
  }
651
692
 
693
+ function parameterIsRange(parameter: any) {
694
+ if(typeof parameter !== 'object' || parameter === null) return false
695
+ if(Array.isArray(parameter)) return parameterIsRange(parameter.at(-1))
696
+ if(parameter.type === 'object') return true
697
+ return false
698
+ }
699
+
700
+ function parameterEnsureRange(parameter: any) {
701
+ if(parameterIsRange(parameter)) return parameter
702
+ if(Array.isArray(parameter)) return [...parameter, { type: 'object', properties: {} }]
703
+ return {
704
+ type: 'array',
705
+ items: [parameter, { type: 'object', properties: {} }]
706
+ }
707
+ }
708
+
652
709
 
653
710
  export type QueryFactoryFunction<SDS extends ServiceDefinitionSpecification> =
654
711
  (definition: QueryDefinitionSpecification) => QueryDefinition<SDS>
@@ -675,7 +732,9 @@ function getSource(input: RuleInput): QuerySource {
675
732
  function isStatic(element: any) {
676
733
  if(typeof element !== "object" || element === null) return true
677
734
  return element instanceof CanBeStatic ? element.$_isStatic() :
678
- (element.constructor.name === "Object" ? element[staticRuleSymbol] : false)
735
+ ((element.constructor.name === "Object" || element.constructor.name === "Array")
736
+ ? element[staticRuleSymbol]
737
+ : false)
679
738
  }
680
739
 
681
740
  function queryDescription(element: any, indent: string = "") {
@@ -706,6 +765,7 @@ function markStatic(element: any) {
706
765
  function parameterJSON(element: any) {
707
766
  if(typeof element !== "object" || element === null) return element
708
767
  if(element instanceof QueryPropertyBase) return { type: 'property', path: element.$path }
768
+ if(Array.isArray(element)) return element.map(item => parameterJSON(item))
709
769
  const output = {
710
770
  type: 'object',
711
771
  properties: {}
@@ -768,7 +828,7 @@ export class RangeRule extends QueryRule {
768
828
  $_getSources(): RuleSource[] {
769
829
  return [
770
830
  new RuleSource(this, this.$input, getSource(this.$input), 'range')
771
- ].filter(s => s.input != null)
831
+ ].filter(s => s.input?.$source != null)
772
832
  }
773
833
 
774
834
  $_parametersJSON(resultParameters: string[]) {
@@ -814,7 +874,7 @@ export class EqualsRule extends QueryRule {
814
874
  return [
815
875
  new RuleSource(this, this.$inputA, getSource(this.$inputA), 'object'),
816
876
  new RuleSource(this, this.$inputB, getSource(this.$inputB), 'object')
817
- ].filter(s => s.input != null)
877
+ ].filter(s => s.input?.$source != null)
818
878
  }
819
879
 
820
880
  $_parametersJSON(resultParameters: string[]) {
@@ -1,11 +1,38 @@
1
1
  async function simpleQuery(input, output, { _query, ...params }) {
2
2
 
3
3
  const plan = _query.plan
4
- const idFunction = idFunction && eval(`(${_query.idFunction})`)
4
+ const idFunction = _query.idFunction && eval(`(${_query.idFunction})`)
5
5
 
6
6
  function sourceChangeStream(source, by) {
7
+ /// TODO: support arrays that have object - prefixed range queries
7
8
  if(typeof by === 'string') return source.object(by)
8
- if(Array.isArray(by)) return source.object(serializeKey(by))
9
+ if(Array.isArray(by)) {
10
+ if(typeof by.at(-1) === 'string') {
11
+ return source.object(serializeKey(by))
12
+ } else {
13
+ if(by.slice(0, -1).every(item => typeof item === 'string')) {
14
+ const prefix = serializeKeyData(by.slice(0, -1))
15
+ const range = by.at(-1)
16
+ const prefixedRange = {
17
+ gt: range.gt ? prefix + range.gt : undefined,
18
+ gte: range.gte ? prefix + range.gte : undefined,
19
+ lt: range.lt ? prefix + range.lt : undefined,
20
+ lte: range.lte ? prefix + range.lte : undefined,
21
+ reverse: range.reverse,
22
+ limit: range.limit,
23
+ }
24
+ if(!(prefixedRange.gt || prefixedRange.gte)) {
25
+ prefixedRange.gte = prefix
26
+ }
27
+ if(!(prefixedRange.lt || prefixedRange.lte)) {
28
+ prefixedRange.lte = prefix + "\xFF\xFF\xFF\xFF"
29
+ }
30
+ return source.range(prefixedRange)
31
+ } else {
32
+ throw new Error("Impossible to compute range from array: " + JSON.stringify(by))
33
+ }
34
+ }
35
+ }
9
36
  return source.range(by)
10
37
  }
11
38
 
@@ -50,14 +77,16 @@ async function simpleQuery(input, output, { _query, ...params }) {
50
77
  #dependentObservations = new Map()
51
78
 
52
79
  #resultsPromise = null
53
- #results = null
80
+ #results = []
81
+ #idFunction = null
54
82
 
55
- constructor(planStep, context, source, by, onChange) {
83
+ constructor(planStep, context, source, by, onChange, idFunction) {
56
84
  this.#planStep = planStep
57
85
  this.#context = context
58
86
  this.#source = source
59
87
  this.#by = by
60
88
  this.#onChange = onChange
89
+ this.#idFunction = idFunction
61
90
  }
62
91
 
63
92
  async start() {
@@ -65,46 +94,27 @@ async function simpleQuery(input, output, { _query, ...params }) {
65
94
  const context = this.#context
66
95
  if(!this.#source) this.#source = await getSource(planStep.execution.sourceType, planStep.execution.name)
67
96
  if(!this.#by) this.#by = decodeParameter(planStep.execution.by, context, params)
68
- let results = []
69
- const observationPromise = sourceChangeStream(this.#source, this.#by).onChange(async (obj, oldObj) => {
97
+ //output.debug("STARTING OBSERVATION", planStep.execution.sourceType, planStep.execution.name, "BY", this.#by)
98
+ const sourceStream = sourceChangeStream(this.#source, this.#by)
99
+ const observationPromise = sourceStream.onChange(async (obj, oldObj) => {
100
+ /* output.debug(this.#planStep.execution.alias, "!", planStep.execution.sourceType, planStep.execution.name, "BY", this.#by,
101
+ 'CHANGE', obj, oldObj) */
70
102
  const id = obj?.id || oldObj?.id
71
103
  if(!id) return
72
104
 
73
- if(!this.#results) { // still fetching
74
- results.push(obj)
75
- } else { // already fetched - maintain memory list of results
76
- if(oldObj) {
77
- const index = this.#results.findIndex(result => result.id === oldObj.id)
78
- if(index !== -1) {
79
- if(obj) {
80
- this.#results[index] = obj
81
- } else {
82
- this.#results.splice(index, 1)
83
- }
84
- }
85
- } else if(obj) {
86
- const index = this.#results.findIndex(result => result.id >= oldObj.id) // idempotency check
87
- if(index === -1) {
88
- results.push(obj)
89
- } else if(this.#results[index].id === oldObj.id) {
90
- this.#results[index] = obj
91
- } else {
92
- this.#results.splice(index, 0, obj)
93
- }
94
- }
95
- }
96
-
97
105
  const nextContext = { ...context, [planStep.execution.alias]: obj }
98
106
  const nextOldContext = { ...context, [planStep.execution.alias]: oldObj }
99
107
 
100
- const objectObservations = []
108
+ //const objectObservations = []
109
+
110
+ const oldJoinedResults = oldObj ? await this.#joinResults([ oldObj ]) : []
101
111
 
102
112
  for(const nextStep of planStep.next) {
103
113
  const nextSource = await getSource(nextStep.execution.sourceType, nextStep.execution.name)
104
114
  const nextBy = decodeParameter(nextStep.execution.by, nextContext, params)
105
115
  const nextOldBy = decodeParameter(nextStep.execution.by, nextOldContext, params)
106
- const nextByKey = nextBy && serializeKey([nextStep.execution.alias, nextBy])
107
- const nextOldByKey = nextOldBy && serializeKey([nextStep.execution.alias, nextOldBy])
116
+ const nextByKey = nextBy && serializeKeyData([nextStep.execution.alias, nextBy])
117
+ const nextOldByKey = nextOldBy && serializeKeyData([nextStep.execution.alias, nextOldBy])
108
118
  if(nextByKey !== nextOldByKey) {
109
119
  if(this.#dependentObservations.has(nextOldByKey)) {
110
120
  const dependentObservation = this.#dependentObservations.get(nextOldByKey)
@@ -118,36 +128,110 @@ async function simpleQuery(input, output, { _query, ...params }) {
118
128
  await dependentObservation.start()
119
129
  }
120
130
  }
121
- if(nextByKey) objectObservations.push(this.#dependentObservations.get(nextByKey))
131
+ //if(nextByKey) objectObservations.push(this.#dependentObservations.get(nextByKey))
132
+ }
133
+
134
+ const newJoinedResults = obj ? await this.#joinResults([ obj ]) : []
135
+
136
+ //output.debug(this.#planStep.execution.alias, "oldJoinedResults", oldJoinedResults, "from", oldObj)
137
+ //output.debug(this.#planStep.execution.alias, "newJoinedResults", newJoinedResults, "from", obj)
138
+
139
+ for(const oldJoinedResult of oldJoinedResults) {
140
+ if(!oldJoinedResult) throw new Error("oldJoinedResult is null")
141
+ if(!newJoinedResults.some(newResult => newResult.id === oldJoinedResult.id))
142
+ this.#joinedChange(null, oldJoinedResult)
143
+ }
144
+ for(const newJoinedResult of newJoinedResults) {
145
+ const oldJoinedResult = oldJoinedResults.find(oldResult => oldResult.id === newJoinedResult.id)
146
+ if(oldJoinedResult) {
147
+ if(JSON.stringify(newJoinedResult) !== JSON.stringify(oldJoinedResult)) {
148
+ this.#joinedChange(newJoinedResult, oldJoinedResult)
149
+ }
150
+ } else {
151
+ this.#joinedChange(newJoinedResult, null)
152
+ }
122
153
  }
123
154
 
124
- /// TODO: do object observations cross product, add self, and push change to parent
125
155
  })
126
156
  this.#resultsPromise = observationPromise.then(() => {
127
- this.#results = results
128
- return results
157
+ return this.#results
129
158
  })
130
159
  this.#observation = await observationPromise
131
160
  }
132
161
 
162
+ async #joinedChange(obj, oldObj, observation) {
163
+ if(oldObj) {
164
+ const index = this.#results.findIndex(result => result.id === oldObj.id)
165
+ if(index !== -1) {
166
+ if(obj) {
167
+ this.#results[index] = obj
168
+ } else {
169
+ this.#results.splice(index, 1)
170
+ }
171
+ }
172
+ } else if(obj) {
173
+ const index = this.#results.findIndex(result => result.id >= obj.id)
174
+ if(index === -1) {
175
+ this.#results.push(obj)
176
+ } else if(this.#results[index].id === obj.id) {
177
+ this.#results[index] = obj
178
+ } else {
179
+ this.#results.splice(index, 0, obj)
180
+ }
181
+ }
182
+ await this.#onChange(obj, oldObj, this)
183
+ }
184
+
185
+ async #joinResults(results) {
186
+ let joinedResults = results.map(result => ({
187
+ __result_id_patrs: [result.id],
188
+ [this.#planStep.execution.alias]: result
189
+ }))
190
+ // output.debug("joined results", joinedResults)
191
+ for(const dependentObservation of this.#dependentObservations.values()) {
192
+ const dependentResults = await dependentObservation.results()
193
+ // output.debug(" dep results", dependentObservation.#planStep.execution.alias, dependentResults)
194
+ joinedResults = joinedResults.flatMap(
195
+ (joinedResult) => {
196
+ if(dependentResults.length === 0) return [
197
+ { /// TODO: check if optional (not mandatory)
198
+ ...joinedResult,
199
+ [dependentObservation.#planStep.execution.alias]: null
200
+ }
201
+ ]
202
+ return dependentResults.map(dependentJoinedResult => ({
203
+ ...dependentJoinedResult,
204
+ ...joinedResult,
205
+ __result_id_patrs: joinedResult.__result_id_patrs.concat(dependentJoinedResult.__result_id_patrs)
206
+ }))
207
+ }
208
+ )
209
+ }
210
+ //output.debug("joinedResultsAfterDependencies", joinedResults)
211
+ for(const joinedResult of joinedResults) {
212
+ joinedResult.id = serializeKey(
213
+ this.#idFunction ? this.#idFunction(joinedResult) : joinedResult.__result_id_patrs
214
+ )
215
+ }
216
+ return joinedResults
217
+ }
218
+
133
219
  async results() {
134
220
  if(!this.#results) return this.#results
135
221
  return await this.#resultsPromise
136
222
  }
137
223
 
138
224
  async handleDependentChange(context, oldContext, observation, id) {
139
- const currentResult = this.#results.find(result => result.id === id)
140
- if(!currentResult) return /// or delete associated objects
141
- const currentContext = { [this.#planStep.execution.alias]: currentResult }
142
- await this.#onChange({
143
- ...currentContext,
144
- ...context,
145
- ...currentContext,
146
- }, {
147
- ...currentContext,
148
- ...oldContext,
149
- ...currentContext,
150
- }, this)
225
+ if(!this.#results) return
226
+ // we need to find thre results affected by the change
227
+ for(const result of this.#results) { // bunary search can be used here for better performance
228
+ if(result.__result_id_patrs[0] === id) {
229
+ const oldResult = { ...result }
230
+ const alias = observation.#planStep.execution.alias
231
+ result[alias] = context[alias]
232
+ this.#onChange(result, oldResult, observation) /// TODO: handle cases when part is mandatory
233
+ }
234
+ }
151
235
  }
152
236
 
153
237
  dispose() {
@@ -164,12 +248,21 @@ async function simpleQuery(input, output, { _query, ...params }) {
164
248
  }
165
249
  }
166
250
 
167
- const rootObservations = plan.map(step => new DataObservation(step, {}, null, null, async (newContext, oldContext, observation) => {
168
- const idParts = idFunction ? idFunction(newContext) : Object.values(newContext).map(v => v?.id)
169
- const id = serializeKey(idParts)
170
- await output.change({ ...newContext, id }, { ...oldContext, id })
171
- }))
251
+ const rootObservations = plan.map(step => new DataObservation(step, {}, null, null,
252
+ (result, oldResult, observation) => {
253
+ if(result) delete result.__result_id_patrs
254
+ if(oldResult) delete oldResult.__result_id_patrs
255
+ output.change(result, oldResult)
256
+ },
257
+ idFunction
258
+ /* async (newContext, oldContext, observation) => {
259
+ const idParts = idFunction ? idFunction(newContext) : Object.values(newContext).map(v => v?.id)
260
+ const id = serializeKey(idParts)
261
+ await output.change({ ...newContext, id }, { ...oldContext, id })
262
+ } */
263
+ ))
172
264
 
173
265
  await Promise.all(rootObservations.map(observation => observation.start()))
266
+ await Promise.all(rootObservations.map(observation => observation.results()))
174
267
 
175
268
  }