@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 +3 -3
- package/src/query.ts +272 -24
- package/src/simpleIndex.db.js +183 -0
- package/src/simpleQuery.db.js +175 -0
- package/src/autoIndex.db.js +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/simple-query",
|
|
3
|
-
"version": "0.9.
|
|
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.
|
|
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": "
|
|
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 ? "" :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
589
|
-
|
|
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:
|
|
740
|
+
constructor(input: QueryInputLike, range: Range) {
|
|
642
741
|
super()
|
|
643
742
|
this.$input = input
|
|
644
743
|
this.$range = range
|
|
645
744
|
}
|
|
646
745
|
|
|
647
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
+
}
|
package/src/autoIndex.db.js
DELETED