@live-change/simple-query 0.9.156 → 0.9.158

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.156",
3
+ "version": "0.9.158",
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.156",
25
+ "@live-change/framework": "^0.9.158",
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": "3042fdd1df2a3b696fb7f161304eb23d71b70ad5"
33
+ "gitHead": "bd30de9030c65b93fbb26b6879f574d5f93baea3"
34
34
  }
package/src/query.ts CHANGED
@@ -6,6 +6,16 @@ import { ModelDefinition, ForeignModelDefinition } from "@live-change/framework"
6
6
 
7
7
  import { PropertyDefinition } from "@live-change/framework"
8
8
 
9
+ import { fileURLToPath } from 'url'
10
+ import { dirname, join, resolve } from 'path'
11
+ import { accessSync, readFileSync } from 'fs'
12
+
13
+ const simpleIndexCodePath = resolve(dirname(fileURLToPath(import.meta.url)), 'autoIndex.db.js')
14
+ const simpleIndexCode = readFileSync(simpleIndexCodePath, 'utf8')
15
+ const simpleQueryCodePath = resolve(dirname(fileURLToPath(import.meta.url)), 'simpleQuery.db.js')
16
+ const simpleQueryCode = readFileSync(simpleQueryCodePath, 'utf8')
17
+
18
+
9
19
  interface Range {
10
20
  gt?: string
11
21
  gte?: string
@@ -121,7 +131,7 @@ class IndexInfo {
121
131
  if(this.sources.every(source => source === firstSource)) this.singleSource = firstSource
122
132
  if(this.singleSource) {
123
133
  this.name = name || ''
124
- + (this.singleSource.input.$source.serviceName === service.name ? "" : this.singleSource.input.$source.serviceName + "_")
134
+ + (this.singleSource.input.$source.serviceName === service.name ? "" : service.name + "_")
125
135
  + (
126
136
  this.singleSource.input.$source.getTypeName() + "_" + (indexParts
127
137
  .map(part => part.path.join(".")).join("_"))
@@ -225,6 +235,8 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
225
235
 
226
236
  //this.printDependencies()
227
237
 
238
+ // process.exit(0)
239
+
228
240
  }
229
241
 
230
242
  printRules() {
@@ -370,10 +382,15 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
370
382
  /// Poza tym przy pobieraniu wstecz nie koniecznie potrzeba indexów,
371
383
  /// może trzeba wprowadzić index-forward i index-backward ?
372
384
  /// A może trzeba wprowadzić analizę tego co mamy i co chcemy uzyskać w przyszłości ?
385
+
373
386
  const next = source.dependentBy.map(dependent => {
374
387
  const otherSource = this.ruleSources.find(s => s.rule === dependent.rule && s != dependent)
375
388
  return this.computeSourceExecutionPlan(otherSource, [...resultParameters, source.input.$alias])
376
389
  })
390
+
391
+ console.log("COMPUTING SOURCE EXECUTION PLAN FOR", source.input.$_toQueryDescription(), "WITH", resultParameters)
392
+ console.log("RULE", queryDescription(source.rule, ' '))
393
+
377
394
  const ruleParameters = JSON.parse(JSON.stringify(source.rule.$_parametersJSON(resultParameters)))
378
395
  if(source.index) {
379
396
  const indexExecution = {
@@ -521,25 +538,87 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
521
538
  })
522
539
  }
523
540
 
524
- prepareQuery() {
525
- console.log("CREATE INDEXES", this.indexes)
541
+ createIndexes() {
542
+ //console.log("CREATE INDEXES", this.indexes)
526
543
 
527
- for(const index of this.indexes) {
544
+ /* for(const index of this.indexes) {
528
545
  const indexQuery = index.$_createQuery(this.service)
529
546
  indexQuery.createIndex(index.name, index.indexParts)
547
+ } */
548
+
549
+ const indexesWithQueries = this.indexes.map(index => ({ info: index, query: index.$_createQuery(this.service) }))
550
+ for(const indexQuery of indexesWithQueries) {
551
+ //console.log("CREATE INDEX", indexQuery.info.name)
552
+ //this.printRules()
553
+ //console.log("OUTPUT MAPPINGS", indexQuery.info.indexParts)
554
+ indexQuery.query.computeIndexPlan(indexQuery.info.indexParts)
555
+ //console.log("INDEX", indexQuery.info.name, "PLAN", JSON.stringify(indexQuery.query.indexPlan, null, 2))
530
556
  }
531
557
 
532
- process.exit(0)
558
+ for(let i = 0; i < indexesWithQueries.length; i++) {
559
+ const a = indexesWithQueries[i]
560
+ for(let j = i+1; j < indexesWithQueries.length; j++) {
561
+ const b = indexesWithQueries[j]
562
+ const plansMatch = JSON.stringify(a.query.indexPlan) === JSON.stringify(b.query.indexPlan)
563
+ if(plansMatch) {
564
+ if(a.info.name !== b.info.name) {
565
+ console.log("INDEXES HAVE THE SAME PLAN", a.info.name, b.info.name, "but different names => merge")
566
+ b.info.name = a.info.name
567
+ } else {
568
+ console.error("INDEXES HAVE THE SAME PLAN AND NAME", a.info.name)
569
+ }
570
+ indexesWithQueries.splice(j, 1)
571
+ j--
572
+ continue
573
+ }
574
+ if(a.info.name === b.info.name) { // check for collision
575
+ console.error("INDEXES HAVE THE SAME NAME", a.info.name, "but different plans => error")
576
+ console.log("PLAN A", JSON.stringify(a.query.indexPlan, null, 2))
577
+ console.log("PLAN B", JSON.stringify(b.query.indexPlan, null, 2))
578
+ throw new Error("INDEXES HAVE THE SAME NAME " + a.info.name + " but different plans")
579
+ }
580
+ }
581
+ }
582
+
583
+ for(const index of indexesWithQueries) {
584
+ if(this.service.indexes[index.info.name.slice(this.service.name.length+1)]) {
585
+ if(JSON.stringify(this.indexPlan)
586
+ === JSON.stringify(this.service.indexes[index.info.name.slice(this.service.name.length+1)].parameters.plan))
587
+ continue
588
+ console.error("INDEX", index.info.name, "ALREADY EXISTS BUT DIFFERENT PLAN")
589
+ throw new Error("INDEX" + index.info.name + "ALREADY EXISTS BUT DIFFERENT PLAN")
590
+ }
591
+ this.service.index({
592
+ name: index.info.name.slice(this.service.name.length+1),
593
+ function: simpleIndexCode,
594
+ sourceName: simpleIndexCodePath,
595
+ parameters: {
596
+ plan: index.query.indexPlan
597
+ }
598
+ })
599
+ }
600
+ }
601
+
602
+ prepareQuery() {
603
+ this.createIndexes()
604
+
605
+ console.log("########### PREPARING QUERY!!!!")
606
+
607
+ this.printRules()
608
+
609
+ this.printDependencies()
533
610
 
534
611
  this.computeExecutionPlan()
535
612
  console.log("EXECUTION PLAN:")
536
613
  console.log(JSON.stringify(this.executionPlan, null, 2))
614
+
615
+ process.exit(0)
537
616
  /// TODO: create indexes used by query
538
617
 
539
618
 
540
619
  /// TODO: prepare query
541
620
 
542
- process.exit(0)
621
+ // process.exit(0)
543
622
  }
544
623
 
545
624
  createIndex(name, mapping: OutputMapping[]) {
@@ -547,10 +626,26 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
547
626
  this.printRules()
548
627
  console.log("OUTPUT MAPPINGS", mapping)
549
628
  this.computeIndexPlan(mapping)
550
- console.log("INDEX PLAN", JSON.stringify(this.indexPlan, null, 2))
629
+ console.log("INDEX", name, "PLAN", JSON.stringify(this.indexPlan, null, 2))
551
630
  /// TODO: create index from query
552
631
 
553
- process.exit(0)
632
+ if(this.service.indexes[name]) {
633
+ if(JSON.stringify(this.indexPlan) === JSON.stringify(this.service.indexes[name].parameters.plan))
634
+ return
635
+ console.error("INDEX", name, "ALREADY EXISTS BUT DIFFERENT PLAN")
636
+ throw new Error("INDEX" + name + "ALREADY EXISTS BUT DIFFERENT PLAN")
637
+ }
638
+
639
+ this.service.index({
640
+ name,
641
+ function: simpleIndexCode,
642
+ sourceName: simpleIndexCodePath,
643
+ parameters: {
644
+ plan: this.indexPlan
645
+ }
646
+ })
647
+
648
+ //process.exit(0)
554
649
  }
555
650
  }
556
651
 
@@ -570,7 +665,7 @@ export default function queryFactory<SDS extends ServiceDefinitionSpecification>
570
665
  return queryFactoryFunction
571
666
  }
572
667
 
573
- type RuleInput = QueryInputBase | QueryPropertyBase | any
668
+ type RuleInput = QueryInputLike | QueryPropertyBase | any
574
669
 
575
670
  function getSource(input: RuleInput): QuerySource {
576
671
  if(input instanceof QueryInputBase) return input.$source
@@ -585,12 +680,13 @@ function isStatic(element: any) {
585
680
 
586
681
  function queryDescription(element: any, indent: string = "") {
587
682
  const flags = isStatic(element) ? "static " : ""
588
- if(typeof element.toQueryDescription === "function")
589
- return flags + element.toQueryDescription(indent)
683
+ if(typeof element !== "object" || element === null) return flags + element.toString()
684
+ if(typeof element.$_toQueryDescription === "function")
685
+ return flags + element.$_toQueryDescription(indent)
590
686
  const fields = Object.entries(element)
591
687
  .map(([key, value]) => `${indent} ${key}: ${queryDescription(value, indent + " ")}`)
592
688
  .join("\n")
593
- if(element.constructor.name !== "Object") return flags + `${element.constructor.name}(${fields})`
689
+ if(element.constructor.name !== "Object") return flags + `${element.constructor.name}(\n${fields})`
594
690
  return flags + '{\n'+fields+`\n${indent}}`;
595
691
  }
596
692
 
@@ -609,7 +705,7 @@ function markStatic(element: any) {
609
705
 
610
706
  function parameterJSON(element: any) {
611
707
  if(typeof element !== "object" || element === null) return element
612
- if(element instanceof QueryPropertyBase) return { property: element.$path }
708
+ if(element instanceof QueryPropertyBase) return { type: 'property', path: element.$path }
613
709
  const output = {
614
710
  type: 'object',
615
711
  properties: {}
@@ -623,6 +719,8 @@ function parameterJSON(element: any) {
623
719
  function parametersJSONForInput(input: RuleInput, resultParameters: string[]) {
624
720
  if(isStatic(input)) {
625
721
  return parameterJSON(input)
722
+ } else if(typeof input.$_parametersJSON === "function") {
723
+ return input.$_parametersJSON(resultParameters)
626
724
  } else {
627
725
  const resultParameter = resultParameters.find(p => p === input.$alias)
628
726
  if(resultParameter) {
@@ -631,6 +729,7 @@ function parametersJSONForInput(input: RuleInput, resultParameters: string[]) {
631
729
  path: [resultParameter, ...input.$path],
632
730
  }
633
731
  }
732
+ //throw new Error("Result parameter not found for input: "+input.$alias)
634
733
  }
635
734
  }
636
735
 
@@ -638,13 +737,13 @@ export class RangeRule extends QueryRule {
638
737
  $input: RuleInput
639
738
  $range: RuleInput
640
739
 
641
- constructor(input: QueryInputBase, range: Range) {
740
+ constructor(input: QueryInputLike, range: Range) {
642
741
  super()
643
742
  this.$input = input
644
743
  this.$range = range
645
744
  }
646
745
 
647
- toQueryDescription(indent: string = "") {
746
+ $_toQueryDescription(indent: string = "") {
648
747
  return `Range(`+
649
748
  `\n${indent} ${queryDescription(this.$input, indent + " ")}`+
650
749
  `\n${indent} ${queryDescription(this.$range, indent + " ")}`+
@@ -690,7 +789,7 @@ export class EqualsRule extends QueryRule {
690
789
  this.$inputB = inputB
691
790
  }
692
791
 
693
- toQueryDescription(indent: string = "") {
792
+ $_toQueryDescription(indent: string = "") {
694
793
  return `Equals(`+
695
794
  `\n${indent} ${queryDescription(this.$inputA, indent + " ")}`+
696
795
  `\n${indent} ${queryDescription(this.$inputB, indent + " ")}`+
@@ -731,7 +830,114 @@ function sourceType(source: QuerySource) {
731
830
  ? "table" : "index"
732
831
  }
733
832
 
734
- export class QueryInputBase extends CanBeStatic {
833
+ export interface QueryInputLike {
834
+ $source: QuerySource
835
+ $alias: string
836
+
837
+ $get(...path: string[]): QueryInputLike
838
+ $inside(range: Range): QueryRule
839
+ $equals(value: any): QueryRule
840
+ $concat(...input: QueryInputLike[]): QueryInputLike
841
+
842
+ $_markStatic()
843
+ $_isStatic()
844
+ $_canBeUsedAsSource(input: QueryInputLike)
845
+ $_getIndexInfo(indexes: IndexInfo[], serviceDefinition: ServiceDefinition<any>): IndexInfo | null
846
+ $_executionJSON()
847
+ $_equals(other: QueryInputLike)
848
+
849
+ $_toQueryDescription(indent: string): string
850
+ }
851
+
852
+ export class CompoundQueryInputBase implements QueryInputLike {
853
+ $source: QuerySource
854
+ $paths: string[][]
855
+ $alias: string
856
+
857
+ constructor(source: QuerySource, paths: string[][], alias: string) {
858
+ this.$source = source
859
+ this.$paths = paths
860
+ this.$alias = alias
861
+ }
862
+
863
+ $get(...path: string[]): QueryInputLike {
864
+ throw new Error('compound inputs does not have fields, while fetching field: '+path.join(".")+' of '+this.$alias)
865
+ }
866
+
867
+ $inside(range: Range) {
868
+ return new RangeRule(this, range)
869
+ }
870
+
871
+ $equals(value: any) {
872
+ return new EqualsRule(this, value)
873
+ }
874
+
875
+ $concat(...input: QueryInputLike[]) {
876
+ if(!input.every(i => i.$source === this.$source)) throw new Error('concat only supports inputs from the same source')
877
+ return new CompoundQueryInputBase(this.$source, [...this.$paths, ...input.map(i => {
878
+ if(i instanceof CompoundQueryInputBase) return i.$paths
879
+ if(i instanceof QueryInputBase) return [i.$path]
880
+ throw new Error('concat only supports QueryInputBase and CompoundQueryInputBase')
881
+ }).flat()], this.$alias)
882
+ }
883
+
884
+
885
+ $_markStatic() {
886
+ /// ignore - QueryInput is not static
887
+ }
888
+
889
+ $_isStatic() {
890
+ return false
891
+ }
892
+
893
+ $_canBeUsedAsSource(input: QueryInputLike) {
894
+ return this.$source === input.$source && this.$alias === input.$alias
895
+ }
896
+
897
+
898
+ $_getIndexInfo(indexes: IndexInfo[], serviceDefinition: ServiceDefinition<any>): IndexInfo | null {
899
+ return new IndexInfo([
900
+ new RangeRule(this, {}),
901
+ ], [
902
+ ...this.$paths.map(path => new OutputMapping(this.$alias, path, path.join("_"))),
903
+ new OutputMapping(this.$alias, ['id'], 'to')
904
+ ], serviceDefinition)
905
+ }
906
+
907
+ $_executionJSON() {
908
+ return {
909
+ sourceType: sourceType(this.$source),
910
+ name: this.$source.getTypeName(),
911
+ alias: this.$alias
912
+ }
913
+ }
914
+ $_equals(other: QueryInputLike) {
915
+ if(!(other instanceof CompoundQueryInputBase)) return false
916
+ return this.$source === other.$source
917
+ && this.$paths.length === other.$paths.length
918
+ && this.$paths.every((path, i) => path.join(".") === other.$paths[i].join("."))
919
+ && this.$alias === other.$alias
920
+ }
921
+
922
+ $_toQueryDescription(indent: string = "") {
923
+ return `CompoundQueryInput(\n${indent} source: ${queryDescription(this.$source, indent + " ")}`+
924
+ `\n${indent} paths: ${this.$paths.map(path => path.join(".")).join(", ")}`+
925
+ `\n${indent} alias: ${this.$alias}`+
926
+ `\n${indent})`
927
+ }
928
+
929
+ $_parametersJSON(resultParameters: string[]) {
930
+ const resultParameter = resultParameters.find(p => p === this.$alias)
931
+ if(resultParameter) {
932
+ return this.$paths.map(path => ({
933
+ type: 'result',
934
+ path: [resultParameter, ...path],
935
+ }))
936
+ }
937
+ }
938
+ }
939
+
940
+ export class QueryInputBase extends CanBeStatic implements QueryInputLike {
735
941
  $source: QuerySource
736
942
  $path: string[]
737
943
  $alias: string
@@ -744,6 +950,38 @@ export class QueryInputBase extends CanBeStatic {
744
950
  return new EqualsRule(this, value)
745
951
  }
746
952
 
953
+ $get(...path: string[]): QueryInputLike {
954
+ return new QueryInputBase(this.$source, [...this.$path, ...path], this.$alias)
955
+ }
956
+
957
+ $concat(...input: QueryInputLike[]) {
958
+ if(!input.every(i => i.$source === this.$source)) throw new Error('concat only supports inputs from the same source')
959
+ return new CompoundQueryInputBase(this.$source, [this.$path, ...input.map(i => {
960
+ if(i instanceof CompoundQueryInputBase) return i.$paths
961
+ if(i instanceof QueryInputBase) return [i.$path]
962
+ throw new Error('concat only supports QueryInputBase and CompoundQueryInputBase')
963
+ }).flat()], this.$alias)
964
+ }
965
+
966
+ /**
967
+ * @returns corresponding type property
968
+ * */
969
+ $type() {
970
+ const typePath:string[] = [
971
+ ...this.$path.slice(0, -1),
972
+ this.$path[this.$path.length - 1] + 'Type'
973
+ ]
974
+ return createQueryInputProxy(new QueryInputBase(this.$source, typePath, this.$alias))
975
+ }
976
+
977
+ /**
978
+ * Transforms property to typed property adding propertyType property
979
+ * @returns typed property multiple fields key
980
+ */
981
+ $typed() {
982
+ return this.$type().$concat(this)
983
+ }
984
+
747
985
  $as(alias: string) {
748
986
  return createQueryInputProxy(new QueryInputBase(this.$source, this.$path, alias))
749
987
  }
@@ -755,7 +993,7 @@ export class QueryInputBase extends CanBeStatic {
755
993
  this.$alias = alias
756
994
  }
757
995
 
758
- toQueryDescription(indent: string = "") {
996
+ $_toQueryDescription(indent: string = "") {
759
997
  return `QueryInput(\n${indent} source: ${queryDescription(this.$source, indent + " ")}`+
760
998
  `\n${indent} path: ${this.$path.join(".")}`+
761
999
  `\n${indent} alias: ${this.$alias}`+
@@ -780,7 +1018,7 @@ export class QueryInputBase extends CanBeStatic {
780
1018
  return new IndexInfo([
781
1019
  new RangeRule(this, {}),
782
1020
  ], [
783
- new OutputMapping(this.$alias, this.$path, this.$path[this.$path.length - 1]),
1021
+ new OutputMapping(this.$alias, this.$path, this.$path.join("_")),
784
1022
  new OutputMapping(this.$alias, ['id'], 'to')
785
1023
  ], serviceDefinition)
786
1024
  }
@@ -789,13 +1027,23 @@ export class QueryInputBase extends CanBeStatic {
789
1027
  return {
790
1028
  sourceType: sourceType(this.$source),
791
1029
  name: this.$source.getTypeName(),
792
- path: [...this.$path],
793
1030
  alias: this.$alias
794
1031
  }
795
1032
  }
796
- $_equals(other: QueryInputBase) {
1033
+ $_equals(other: QueryInputLike) {
1034
+ if(!(other instanceof QueryInputBase)) return false
797
1035
  return this.$source === other.$source && this.$path.join(".") === other.$path.join(".") && this.$alias === other.$alias
798
1036
  }
1037
+
1038
+ $_parametersJSON(resultParameters: string[]) {
1039
+ const resultParameter = resultParameters.find(p => p === this.$alias)
1040
+ if(resultParameter) {
1041
+ return {
1042
+ type: 'result',
1043
+ path: [resultParameter, ...this.$path],
1044
+ }
1045
+ }
1046
+ }
799
1047
  }
800
1048
 
801
1049
  export class QueryInput extends QueryInputBase {
@@ -804,13 +1052,13 @@ export class QueryInput extends QueryInputBase {
804
1052
 
805
1053
 
806
1054
  export function createQueryInputProxy(
807
- base: QueryInputBase
1055
+ base: QueryInputLike
808
1056
  ) {
809
1057
  return new Proxy(base, {
810
1058
  get(target, prop, receiver) {
811
1059
  const foundInBase = Reflect.get(target, prop, receiver)
812
1060
  if(foundInBase) return foundInBase
813
- const newBase = new QueryInputBase(base.$source, [...base.$path, prop as string], base.$alias)
1061
+ const newBase = target.$get(prop as string)
814
1062
  const inputProxy = createQueryInputProxy(newBase)
815
1063
  return inputProxy
816
1064
  }
@@ -825,7 +1073,7 @@ export class QueryPropertyBase extends CanBeStatic {
825
1073
  this.$path = path
826
1074
  }
827
1075
 
828
- toQueryDescription(indent: string = "") {
1076
+ $_toQueryDescription(indent: string = "") {
829
1077
  return `QueryProperty(${this.$path.join(".")})`
830
1078
  }
831
1079
 
@@ -0,0 +1,183 @@
1
+ async function autoIndex(input, output, { plan, properties }) {
2
+
3
+ async function getFromSource(source, by) {
4
+ if(typeof by === 'string') return [await source.object(by).get()]
5
+ return await source.range(by).get()
6
+ }
7
+
8
+ async function fetch(planStep, context, oldContext) {
9
+ const { execution, next } = planStep
10
+ const source = await getSource(execution.sourceType, execution.name)
11
+ const by = context && decodeParameter(execution.by, context, properties)
12
+ const oldBy = oldContext && decodeParameter(execution.by, oldContext, properties)
13
+ const data = await getFromSource(source, by)
14
+ const oldData = await getFromSource(source, oldBy)
15
+ context[execution.alias] = data
16
+ oldContext[execution.alias] = oldData
17
+ }
18
+
19
+ async function gatherOutputData(next, context, oldContext) {
20
+ const outputContext = { ...context }
21
+ const oldOutputContext = { ...context }
22
+ /// first execute next to gather all data
23
+ for(const nextStep of next) {
24
+ await fetch(nextStep, outputContext, oldOutputContext)
25
+ }
26
+ return [outputContext, oldOutputContext]
27
+ }
28
+
29
+ /* function extractMappedDataWithId(mapping, context) {
30
+ const outputData = {}
31
+ const idParts = []
32
+ for(const key in execution.mapping) {
33
+ const outputPath = execution.mapping[key]
34
+ outputData[key] = outputPath.reduce((acc, path) => acc?.[path], outputContext)
35
+ if(!outputData[key]) return null
36
+ idParts.push(outputData[key])
37
+ }
38
+ const id = serializeKey(idParts)
39
+ return { id, outputData }
40
+ } */
41
+
42
+ function objectPath(object, path) {
43
+ return path.reduce((acc, path) => acc?.[path], object)
44
+ }
45
+
46
+ function decodeParameter(parameter, context, properties) {
47
+ if(typeof parameter !== 'object') return parameter
48
+ if(Array.isArray(parameter)) return parameter.map(p => decodeParameter(p, context, properties))
49
+ if(parameter.type === 'object') return Object.fromEntries(
50
+ Object.entries(parameter.properties).map(([key, value]) => [key, decodeParameter(value, context, properties)])
51
+ )
52
+ if(parameter.type === 'property') return objectPath(properties, parameter.path)
53
+ if(parameter.type === 'result') return objectPath(context, parameter.path)
54
+ throw new Error(`Invalid parameter type: ${parameter.type}`)
55
+ }
56
+
57
+ async function getSource(sourceType, name) {
58
+ switch(sourceType) {
59
+ case 'table':
60
+ return await input.table(name)
61
+ case 'index':
62
+ return await input.index(name)
63
+ case 'log':
64
+ return await input.log(name)
65
+ default:
66
+ throw new Error(`Invalid source type: ${sourceType}`)
67
+ }
68
+ }
69
+
70
+ function allInPath(path, context) {
71
+ if(!context) return []
72
+ const [ first, ...rest ] = path
73
+ const values = Array.isArray(context[first]) ? context[first] : [context[first]]
74
+ if(rest.length === 0) return values
75
+ return values.map(value => allInPath(rest, value)).flat()
76
+ }
77
+
78
+ function generateOutputData(entries, context) {
79
+ const [ first, ...rest ] = entries
80
+ const values = allInPath(first[1], context)
81
+ //console.log("GENERATE OUTPUT FROM ENGRY", first, "CONTEXT", context, "VALUES", values)
82
+ return values.map(value => {
83
+ if(rest.length === 0) return [{
84
+ [first[0]]: value
85
+ }]
86
+ const generated = generateOutputData(rest, context)
87
+ return generated.map(data => ({
88
+ [first[0]]: value,
89
+ ...data
90
+ }))
91
+ }).flat()
92
+ }
93
+
94
+ function keyEntry(entries, entryData) {
95
+ const idParts = new Array(entries.length)
96
+ for(let i = 0; i < entries.length; i++) {
97
+ idParts[i] = entryData[entries[i][0]]
98
+ if(!idParts[i]) return null
99
+ }
100
+ return { ...entryData, id: serializeKey(idParts) }
101
+ }
102
+
103
+ function keyData(entries, data) {
104
+ return data.map(entry => keyEntry(entries, entry)).filter(entry => entry !== null)
105
+ }
106
+
107
+ function findChanges(data, oldData) {
108
+ //output.debug("FIND CHANGES", data, "OLD DATA", oldData)
109
+ const byKey = new Map()
110
+ for(const entry of data) {
111
+ if(byKey.has(entry.id)) {
112
+ throw new Error(`Duplicate id: ${entry.id}`)
113
+ } else {
114
+ byKey.set(entry.id, { id: entry.id, entry, oldEntry: null })
115
+ }
116
+ }
117
+ for(const entry of oldData) {
118
+ if(byKey.has(entry.id)) {
119
+ byKey.get(entry.id).oldEntry = entry
120
+ } else {
121
+ byKey.set(entry.id, { id: entry.id, entry: null, oldEntry: entry })
122
+ }
123
+ }
124
+ return Array.from(byKey.values())
125
+ }
126
+
127
+ const observations = new Map()
128
+
129
+ async function execute(planStep, context, oldContext) {
130
+ //output.debug("EXECUTE STEP", planStep, "WITH CONTEXT", context, "AND OLD CONTEXT", oldContext)
131
+ const { execution, next } = planStep
132
+ if(execution.operation === 'output') {
133
+ const [outputData, oldOutputData] = await gatherOutputData(next, context, oldContext)
134
+
135
+ //output.debug("OUTPUT DATA", outputData, "OLD OUTPUT DATA", oldOutputData)
136
+
137
+ const mappingEntries = Object.entries(execution.mapping)
138
+ const mappedData = generateOutputData(mappingEntries, outputData)
139
+ const oldMappedData = generateOutputData(mappingEntries, oldOutputData)
140
+
141
+ //output.debug("MAPPED DATA2", mappedData, 'FROM', outputData, "MAP", mappingEntries)
142
+ //output.debug("OLD MAPPED DATA2", oldMappedData, 'FROM', oldOutputData, "MAP", mappingEntries)
143
+
144
+ const keyedData = keyData(mappingEntries, mappedData)
145
+ const oldKeyedData = keyData(mappingEntries, oldMappedData)
146
+
147
+ //output.debug("KEYED DATA", keyedData, "OLD KEYED DATA", oldKeyedData)
148
+
149
+ const changes = findChanges(keyedData, oldKeyedData)
150
+ for(const change of changes) {
151
+ await output.change(change.entry, change.oldEntry)
152
+ }
153
+ return;
154
+ }
155
+ const source = await getSource(execution.sourceType, execution.name)
156
+
157
+ const by = context && decodeParameter(execution.by, context, properties)
158
+ const oldBy = oldContext && decodeParameter(execution.by, oldContext, properties)
159
+ const byKey = by && serializeKey(by)
160
+ const oldByKey = oldBy && serializeKey(oldBy)
161
+
162
+ if(byKey !== oldByKey) {
163
+ if(observations.has(oldByKey)) {
164
+ const observation = observations.get(oldByKey)
165
+ observation.dispose()
166
+ observations.delete(oldByKey)
167
+ }
168
+ if(!observations.has(byKey)) {
169
+ const observation = await source.range(by).onChange(async (obj, oldObj) => {
170
+ const nextContext = { ...context, [execution.alias]: obj }
171
+ const nextOldContext = { ...context, [execution.alias]: oldObj }
172
+ // not ...oldContext, oldContext was used in previous observation
173
+ for(const nextStep of next) {
174
+ await execute(nextStep, nextContext, nextOldContext)
175
+ }
176
+ })
177
+ observations.set(byKey, observation)
178
+ }
179
+ }
180
+ }
181
+
182
+ await Promise.all(plan.map(step => execute(step, {}, null)))
183
+ }
@@ -0,0 +1,175 @@
1
+ async function simpleQuery(input, output, { _query, ...params }) {
2
+
3
+ const plan = _query.plan
4
+ const idFunction = idFunction && eval(`(${_query.idFunction})`)
5
+
6
+ function sourceChangeStream(source, by) {
7
+ if(typeof by === 'string') return source.object(by)
8
+ if(Array.isArray(by)) return source.object(serializeKey(by))
9
+ return source.range(by)
10
+ }
11
+
12
+ function objectPath(object, path) {
13
+ return path.reduce((acc, path) => acc?.[path], object)
14
+ }
15
+
16
+ function decodeParameter(parameter, context, properties) {
17
+ if(typeof parameter !== 'object') return parameter
18
+ if(Array.isArray(parameter)) return parameter.map(p => decodeParameter(p, context, properties))
19
+ if(parameter.type === 'object') return Object.fromEntries(
20
+ Object.entries(parameter.properties).map(([key, value]) => [key, decodeParameter(value, context, properties)])
21
+ )
22
+ if(parameter.type === 'property') return objectPath(properties, parameter.path)
23
+ if(parameter.type === 'result') return objectPath(context, parameter.path)
24
+ throw new Error(`Invalid parameter type: ${parameter.type}`)
25
+ }
26
+
27
+ async function getSource(sourceType, name) {
28
+ switch(sourceType) {
29
+ case 'table':
30
+ return await input.table(name)
31
+ case 'index':
32
+ return await input.index(name)
33
+ case 'log':
34
+ return await input.log(name)
35
+ default:
36
+ throw new Error(`Invalid source type: ${sourceType}`)
37
+ }
38
+ }
39
+
40
+ class DataObservation {
41
+
42
+ #planStep = null
43
+ #context = null
44
+ #source = null
45
+ #by = null
46
+ #onChange = null
47
+
48
+ #observation = null
49
+
50
+ #dependentObservations = new Map()
51
+
52
+ #resultsPromise = null
53
+ #results = null
54
+
55
+ constructor(planStep, context, source, by, onChange) {
56
+ this.#planStep = planStep
57
+ this.#context = context
58
+ this.#source = source
59
+ this.#by = by
60
+ this.#onChange = onChange
61
+ }
62
+
63
+ async start() {
64
+ const planStep = this.#planStep
65
+ const context = this.#context
66
+ if(!this.#source) this.#source = await getSource(planStep.execution.sourceType, planStep.execution.name)
67
+ 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) => {
70
+ const id = obj?.id || oldObj?.id
71
+ if(!id) return
72
+
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
+ const nextContext = { ...context, [planStep.execution.alias]: obj }
98
+ const nextOldContext = { ...context, [planStep.execution.alias]: oldObj }
99
+
100
+ const objectObservations = []
101
+
102
+ for(const nextStep of planStep.next) {
103
+ const nextSource = await getSource(nextStep.execution.sourceType, nextStep.execution.name)
104
+ const nextBy = decodeParameter(nextStep.execution.by, nextContext, params)
105
+ 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])
108
+ if(nextByKey !== nextOldByKey) {
109
+ if(this.#dependentObservations.has(nextOldByKey)) {
110
+ const dependentObservation = this.#dependentObservations.get(nextOldByKey)
111
+ dependentObservation.dispose()
112
+ this.#dependentObservations.delete(nextOldByKey)
113
+ }
114
+ if(!this.#dependentObservations.has(nextByKey)) {
115
+ const dependentObservation = new DataObservation(nextStep, nextContext, nextSource, nextBy,
116
+ (context, oldContext, observation) => this.handleDependentChange(context, oldContext, observation, id))
117
+ this.#dependentObservations.set(nextByKey, dependentObservation)
118
+ await dependentObservation.start()
119
+ }
120
+ }
121
+ if(nextByKey) objectObservations.push(this.#dependentObservations.get(nextByKey))
122
+ }
123
+
124
+ /// TODO: do object observations cross product, add self, and push change to parent
125
+ })
126
+ this.#resultsPromise = observationPromise.then(() => {
127
+ this.#results = results
128
+ return results
129
+ })
130
+ this.#observation = await observationPromise
131
+ }
132
+
133
+ async results() {
134
+ if(!this.#results) return this.#results
135
+ return await this.#resultsPromise
136
+ }
137
+
138
+ 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)
151
+ }
152
+
153
+ dispose() {
154
+ this.#observation.dispose()
155
+ this.#dependentObservations.forEach(dependentObservation => dependentObservation.dispose())
156
+ this.#dependentObservations.clear()
157
+ this.#results = null
158
+ this.#resultsPromise = null
159
+ this.#observation = null
160
+ this.#source = null
161
+ this.#by = null
162
+ this.#planStep = null
163
+ this.#context = null
164
+ }
165
+ }
166
+
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
+ }))
172
+
173
+ await Promise.all(rootObservations.map(observation => observation.start()))
174
+
175
+ }
@@ -1,3 +0,0 @@
1
- async function autoIndex(input, output, { }) {
2
-
3
- }