@live-change/simple-query 0.9.138 → 0.9.142

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/LICENSE.md ADDED
@@ -0,0 +1,11 @@
1
+ Copyright 2019-2024 Michał Łaszczewski
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -27,9 +27,9 @@ const channelMessagesWithUsersAndIdentificationByTime = query({ // definition
27
27
  code(props, { user, message, identification }) => {
28
28
  const { channel, ...range } = props
29
29
  message.time.inside(range)
30
- message.channel.eq(channel)
31
- user.id.eq(message.author)
32
- identification.id.eq(user.id)
30
+ message.channel.eqals(channel)
31
+ user.id.equals(message.au thor)
32
+ identification.id.equals(user.id)
33
33
  }
34
34
  })
35
35
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/simple-query",
3
- "version": "0.9.138",
3
+ "version": "0.9.142",
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.138",
25
+ "@live-change/framework": "^0.9.142",
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": "b1b605b7f1fa4fc3de4720afbb401e2cfff080cf"
33
+ "gitHead": "efc249d458250a4d1cb9bd5ff847de066452dc1c"
34
34
  }
@@ -0,0 +1,3 @@
1
+ async function autoIndex(input, output, { }) {
2
+
3
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import queryFactory from "./query.js"
2
+
3
+ export default queryFactory
package/src/query.ts ADDED
@@ -0,0 +1,823 @@
1
+ import type {
2
+ PropertyDefinitionSpecification, ServiceDefinition, ServiceDefinitionSpecification
3
+ } from "@live-change/framework"
4
+
5
+ import { ModelDefinition, ForeignModelDefinition } from "@live-change/framework"
6
+
7
+ import { PropertyDefinition } from "@live-change/framework"
8
+
9
+ interface Range {
10
+ gt?: string
11
+ gte?: string
12
+ lt?: string
13
+ lte?: string,
14
+ reverse?: boolean,
15
+ limit?: number,
16
+ }
17
+
18
+ interface QueryParameters {
19
+ [key: string]: any
20
+ }
21
+
22
+ interface QueryInputs {
23
+ [key: string]: QueryInput
24
+ }
25
+
26
+ class CanBeStatic {
27
+ $_markStatic() {
28
+ this[staticRuleSymbol] = true
29
+ }
30
+
31
+ $_isStatic() {
32
+ return this[staticRuleSymbol]
33
+ }
34
+ }
35
+
36
+ class RuleSource {
37
+ rule: QueryRule
38
+ input: QueryInput
39
+ type: string
40
+ dependentBy: RuleSource[]
41
+ dependsOn: RuleSource[]
42
+ index: IndexInfo | null
43
+
44
+
45
+ constructor(rule: QueryRule, input: QueryInput, source: QuerySource, type: string) {
46
+ this.rule = rule
47
+ this.input = input
48
+ this.type = type
49
+ this.dependentBy = []
50
+ this.dependsOn = []
51
+ }
52
+ }
53
+
54
+
55
+ class QueryRule extends CanBeStatic {
56
+ $_hasStaticParameter() {
57
+ return false
58
+ }
59
+
60
+ $_getSources(): RuleSource[] {
61
+ return []
62
+ }
63
+
64
+ $_equals(other: QueryRule) {
65
+ return false
66
+ }
67
+
68
+ $_parametersJSON(resultParameters: string[]) {
69
+ return undefined
70
+ }
71
+ }
72
+
73
+ type QueryRules = QueryRule[]
74
+
75
+ type QueryCode = ((parameters: QueryParameters, inputs: QueryInputs) => QueryRules)
76
+
77
+ type QuerySource = ModelDefinition<any> | ForeignModelDefinition | any /// Query Definition will be recursive definition, so use any for now
78
+
79
+
80
+ interface QueryDefinitionSpecification {
81
+ name: string
82
+ properties: Record<string, PropertyDefinitionSpecification>
83
+ returns?: PropertyDefinitionSpecification,
84
+ sources: Record<string, QuerySource>,
85
+ code: QueryCode,
86
+ sourceName: string,
87
+ update: boolean,
88
+ }
89
+
90
+ class OutputMapping {
91
+ result: string
92
+ path: string[]
93
+ alias: string
94
+
95
+ constructor(result: string, path: string[], alias: string) {
96
+ this.result = result
97
+ this.path = path
98
+ this.alias = alias
99
+ }
100
+
101
+ getDescription(indent: string = "") {
102
+ return `OutputMapping(${this.result}.${this.path.join(".")}, ${this.alias})`
103
+ }
104
+
105
+ equals(other: OutputMapping) {
106
+ return this.result === other.result && this.path.join(".") === other.path.join(".") && this.alias === other.alias
107
+ }
108
+ }
109
+
110
+ class IndexInfo {
111
+ singleSource: QuerySource | null = null
112
+ rules: QueryRule[]
113
+ indexParts: OutputMapping[]
114
+ name: string
115
+
116
+ constructor(rules: QueryRule[], indexParts: OutputMapping[], name: string = undefined) {
117
+ this.rules = rules
118
+ this.indexParts = indexParts
119
+ const allSources = rules.map(rule => rule.$_getSources()).flat()
120
+ const firstSource = allSources[0]
121
+ if(allSources.every(source => source === firstSource)) this.singleSource = firstSource
122
+ if(this.singleSource) {
123
+ this.name = name || (this.singleSource.input.$source.getTypeName() + "_" + (indexParts
124
+ .map(part => part.path.join(".")).join("_")))
125
+ } else {
126
+ this.name = name || indexParts
127
+ .map(part => {
128
+ const source = allSources.find(source => source.input.$alias === part.result)
129
+ if(!source) throw new Error("Source not found for index part: " + part)
130
+ return source.input.$source.getTypeName() + "_" + part.path.join(".")
131
+ }).join("_")
132
+ }
133
+ }
134
+
135
+ equals(other: IndexInfo) {
136
+ if(this.rules.length !== other.rules.length) return false
137
+ if(this.name !== other.name) return false
138
+ if(this.indexParts.length !== other.indexParts.length) return false
139
+ if(this.indexParts.some((part, i) => part !== other.indexParts[i])) return false
140
+ for(let i = 0; i < this.rules.length; i++) {
141
+ if(this.rules[i].$_equals(other.rules[i])) return false
142
+ }
143
+ return true
144
+ }
145
+
146
+ getDescription(indent: string = "") {
147
+ return `Index(${this.name}, `+
148
+ ((this.rules.length > 0)
149
+ ? `\n${indent} ${this.rules.map(rule => queryDescription(rule, indent + " ")).join("\n "+indent)}`
150
+ : "")+
151
+ `\n${indent} ${this.indexParts.map(part => part.getDescription(indent + " ")).join("\n "+indent)}`+
152
+ `\n${indent})`
153
+ }
154
+
155
+ $_executionJSON() {
156
+ if(!this.singleSource) throw new Error("Indexes with multiple sources are not supported")
157
+ return {
158
+ sourceType: "index",
159
+ name: this.name,
160
+ alias: this.singleSource.input.$alias + 'Indexed'
161
+ }
162
+ }
163
+ }
164
+
165
+ const staticRuleSymbol = Symbol("static")
166
+
167
+ type ExecutionStep = {
168
+ execution: any,
169
+ next: ExecutionStep[]
170
+ }
171
+
172
+ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
173
+
174
+ service: ServiceDefinition<SDS>
175
+ definition: QueryDefinitionSpecification
176
+ properties: Record<string, PropertyDefinition<any>>
177
+ rules: QueryRules
178
+ firstRule: QueryRule
179
+ rootSources: RuleSource[]
180
+ ruleSources: RuleSource[]
181
+ indexes: IndexInfo[]
182
+
183
+ executionPlan: ExecutionStep[]
184
+ indexPlan: ExecutionStep[]
185
+
186
+ constructor(serviceDefinition: ServiceDefinition<SDS>, definition: QueryDefinitionSpecification) {
187
+ this.service = serviceDefinition
188
+ this.definition = definition
189
+
190
+ this.properties = Object.fromEntries(
191
+ Object.entries(definition.properties)
192
+ .map(
193
+ ([propertyName, propertyDefinition]) => [propertyName, new PropertyDefinition(propertyDefinition)]
194
+ )
195
+ )
196
+
197
+ this.computeRules()
198
+ this.markStaticRules()
199
+
200
+ this.printRules()
201
+
202
+ this.computeDependencies()
203
+ this.computeIndexes()
204
+
205
+ this.printDependencies()
206
+
207
+ }
208
+
209
+ printRules() {
210
+ console.log("QUERY RULES:")
211
+ for(const key in this.rules) {
212
+ console.log(` ${key}:`, queryDescription(this.rules[key], ' '))
213
+ }
214
+ }
215
+
216
+ computeRules() {
217
+ const queryProperties = {}
218
+ for(const propertyName in this.definition.properties) {
219
+ const propertyDefinition = this.definition.properties[propertyName]
220
+ const base = new QueryPropertyBase([propertyName])
221
+ queryProperties[propertyName] = createQueryPropertyProxy(base, propertyName, this.properties[propertyName])
222
+ }
223
+
224
+ const queryInputs = {}
225
+ for(const sourceName in this.definition.sources) {
226
+ const propertyDefinition = this.definition.sources[sourceName]
227
+ const base = new QueryInputBase(this.definition.sources[sourceName], [], sourceName)
228
+ queryInputs[sourceName] = createQueryInputProxy(base)
229
+ }
230
+
231
+ // run the code to collect relations
232
+ this.rules = this.definition.code(queryProperties, queryInputs)
233
+ }
234
+
235
+ markStaticRules() {
236
+ for(const key in this.rules) {
237
+ const rule = this.rules[key]
238
+ markStatic(rule)
239
+ }
240
+ }
241
+
242
+ computeDependencies() {
243
+ const independentRules = this.rules.filter(rule => rule.$_hasStaticParameter())
244
+ if(independentRules.length > 1) {
245
+ console.error("Independent rules:")
246
+ for(const rule of independentRules) {
247
+ console.error(' ' + queryDescription(rule, ' '))
248
+ }
249
+ throw new Error("Multiple independent rules are not supported")
250
+ }
251
+ this.firstRule = independentRules[0] ?? this.rules[0]
252
+ this.rootSources = this.firstRule.$_getSources()
253
+ const providedSources = this.rootSources.slice()
254
+
255
+ const otherSources = []
256
+ for(const rule of this.rules) {
257
+ if(rule === this.firstRule) continue
258
+ const ruleSources = rule.$_getSources()
259
+ for(const ruleSource of ruleSources) {
260
+ otherSources.push(ruleSource)
261
+ }
262
+ }
263
+
264
+ this.ruleSources = this.rootSources.concat(otherSources)
265
+
266
+ while(otherSources.length > 0) {
267
+ /// find sources to link
268
+ let linked = false
269
+ for(const ruleSource of otherSources) {
270
+ for(const providedSource of providedSources) {
271
+ if(ruleSource == providedSource) {
272
+ throw new Error("Rule source is equal to provided source")
273
+ }
274
+ /* console.log("RULE SOURCE", queryDescription(ruleSource.source, ' '))
275
+ console.log("PROVIDED SOURCE", queryDescription(providedSource.source, ' '))
276
+ console.log("MATCH", ruleSource.source === providedSource.source) */
277
+ if(ruleSource.input.$_canBeUsedAsSource(providedSource.input)) {
278
+ ruleSource.dependsOn.push(providedSource)
279
+ providedSource.dependentBy.push(ruleSource)
280
+ otherSources.splice(otherSources.indexOf(ruleSource), 1)
281
+
282
+ for(const otherRuleSource of this.ruleSources) {
283
+ if(otherRuleSource.rule !== ruleSource.rule) continue
284
+ if(otherRuleSource === ruleSource) continue
285
+ if(providedSources.find(s => s === otherRuleSource)) continue
286
+ providedSources.push(otherRuleSource)
287
+ otherSources.splice(otherSources.indexOf(otherRuleSource), 1)
288
+ }
289
+ linked = true
290
+ break
291
+ }
292
+ if(linked) break;
293
+ }
294
+ }
295
+ if(!linked && otherSources.length > 0) {
296
+ console.error("Impossible to link query, found independent sources:")
297
+ for(const ruleSource of otherSources) {
298
+ console.error(' ' + queryDescription(ruleSource, ' '))
299
+ }
300
+ throw new Error("Impossible to link query, found independent sources")
301
+ }
302
+ }
303
+ }
304
+
305
+ printDependencies(indent: string = '') {
306
+ console.log('QUERY DEPENDENCIES:')
307
+ for(const key in this.rules) {
308
+ const rule = this.rules[key]
309
+ console.log(`${indent} RULE ${key}:`)
310
+ console.log(`${indent} ${queryDescription(rule, indent + ' ')}`)
311
+ console.log(`${indent} SOURCES:`)
312
+ for(const ruleSource of this.ruleSources) {
313
+ if(ruleSource.rule !== rule) continue
314
+ console.log(`${indent} ${queryDescription(ruleSource.input.$source, indent + ' ')} as ${ruleSource.input.$alias}`)
315
+ if(ruleSource.dependsOn.length > 0) {
316
+ console.log(`${indent} DEPENDS ON: `+
317
+ `${ruleSource.dependsOn.map(d => this.rules.indexOf(d.rule)).join(', ')}`
318
+ )
319
+ }
320
+ if(ruleSource.dependentBy.length > 0) {
321
+ console.log(`${indent} DEPENDENT BY: `+
322
+ `${ruleSource.dependentBy.map(d => this.rules.indexOf(d.rule)).join(', ')}`
323
+ )
324
+ }
325
+ if(ruleSource.index) {
326
+ console.log(`${indent} ${ruleSource.index.getDescription(indent + ' ')}`)
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ computeIndexes() {
333
+ this.indexes = []
334
+ for(const ruleSource of this.ruleSources) {
335
+ const potentialIndex = ruleSource.input.$_getIndexInfo(this.indexes)
336
+ if(!potentialIndex) continue
337
+ const existingIndex = this.indexes.find(index => index.equals(potentialIndex))
338
+ if(existingIndex) {
339
+ ruleSource.index = existingIndex
340
+ continue
341
+ }
342
+ this.indexes.push(potentialIndex)
343
+ ruleSource.index = potentialIndex
344
+ }
345
+ }
346
+
347
+ computeSourceExecutionPlan(source: RuleSource, resultParameters: string[]) {
348
+ /// TODO: W poniższej linii jest bug, powinno pobierać odmienne source, a nie te bezpośrednio zależne.
349
+ /// Poza tym przy pobieraniu wstecz nie koniecznie potrzeba indexów,
350
+ /// może trzeba wprowadzić index-forward i index-backward ?
351
+ /// A może trzeba wprowadzić analizę tego co mamy i co chcemy uzyskać w przyszłości ?
352
+ const next = source.dependentBy.map(dependent => {
353
+ const otherSource = this.ruleSources.find(s => s.rule === dependent.rule && s != dependent)
354
+ return this.computeSourceExecutionPlan(otherSource, [...resultParameters, source.input.$alias])
355
+ })
356
+ const ruleParameters = JSON.parse(JSON.stringify(source.rule.$_parametersJSON(resultParameters)))
357
+ if(source.index) {
358
+ const indexExecution = {
359
+ ...source.index.$_executionJSON(),
360
+ by: ruleParameters[Object.keys(ruleParameters)[0]]
361
+ }
362
+ const indexNext = [{
363
+ operation: 'object',
364
+ ...source.input.$_executionJSON(),
365
+ by: {
366
+ type: 'result',
367
+ path: [indexExecution.alias, source.index.indexParts.at(-1).alias],
368
+ },
369
+ next
370
+ }]
371
+ return {
372
+ execution: indexExecution,
373
+ next: indexNext
374
+ }
375
+ }
376
+ const execution = {
377
+ ...source.input.$_executionJSON(),
378
+ by: ruleParameters[Object.keys(ruleParameters)[0]]
379
+ }
380
+ const executionPlan = {
381
+ execution,
382
+ next
383
+ }
384
+ return executionPlan
385
+ }
386
+
387
+ computeExecutionPlan() {
388
+ this.executionPlan = []
389
+ for(const rootSource of this.rootSources) {
390
+ this.executionPlan.push(this.computeSourceExecutionPlan(rootSource, []))
391
+ }
392
+ }
393
+
394
+ computeSourceIndexPlan(source: RuleSource, resultParameters: string[], processed: RuleSource[], allProcessed: RuleSource[]) {
395
+ allProcessed.push(source)
396
+ const ruleSources = this.ruleSources
397
+ .filter(s => s.input.$source === source.input.$source && s != source && !processed.includes(s))
398
+ const next = ruleSources.map(ruleSource => {
399
+ const otherSource = this.ruleSources.find(s => s.rule === ruleSource.rule && s != ruleSource)
400
+ if(!otherSource) return null
401
+ if(processed.includes(otherSource)) return null
402
+ return this.computeSourceIndexPlan(
403
+ otherSource,
404
+ [...resultParameters, source.input.$alias],
405
+ [...processed, source],
406
+ allProcessed
407
+ )
408
+ }).filter(s => s !== null)
409
+ const ruleParameters = JSON.parse(JSON.stringify(source.rule.$_parametersJSON(resultParameters)))
410
+ if(source.index) {
411
+ const indexExecution = {
412
+ ...source.index.$_executionJSON(),
413
+ by: ruleParameters[Object.keys(ruleParameters)[0]]
414
+ }
415
+ const indexNext = [{
416
+ operation: 'object',
417
+ ...source.input.$_executionJSON(),
418
+ by: {
419
+ type: 'result',
420
+ path: [indexExecution.alias, source.index.indexParts.at(-1).alias],
421
+ },
422
+ next
423
+ }]
424
+ return {
425
+ execution: indexExecution,
426
+ next: indexNext
427
+ }
428
+ }
429
+ const execution = {
430
+ ...source.input.$_executionJSON(),
431
+ by: ruleParameters[Object.keys(ruleParameters)[0]],
432
+ }
433
+ const executionPlan = {
434
+ execution,
435
+ next
436
+ }
437
+ return executionPlan
438
+ }
439
+
440
+ computeIndexPlan() {
441
+ const indexSources = []
442
+ for(const ruleSource of this.ruleSources) {
443
+ const source = ruleSource.input.$source
444
+ if(indexSources.includes(source)) continue
445
+ indexSources.push(source)
446
+ }
447
+ this.indexPlan = indexSources.map(source => {
448
+ const firstFetch = {
449
+ sourceType: sourceType(source),
450
+ name: source.getTypeName(),
451
+ alias: source.getTypeName(),
452
+ by: { type: 'object', properties: {} } /// infinite range
453
+ }
454
+ const ruleSources = this.ruleSources.filter(s => s.input.$source === source)
455
+ const ruleSourcesAliases = Array.from(new Set(ruleSources.map(s => s.input.$alias)))
456
+ const next:ExecutionStep[] = ruleSourcesAliases.map((alias) => {
457
+ const processed = []
458
+
459
+ const aliasRuleSources = ruleSources.filter(s => s.input.$alias === alias)
460
+ const next:ExecutionStep[] = aliasRuleSources.map((input) => {
461
+ const rule = input.rule
462
+ const output = this.ruleSources.find(s => s.rule === rule && s != input)
463
+ if(!output) return null
464
+ const ignoredSources = ruleSources.filter(s => s.input.$source === source && s.input.$alias === input.input.$alias)
465
+ const executionPlan = this.computeSourceIndexPlan(output, [firstFetch.alias], [input, ...ignoredSources], processed)
466
+ executionPlan.execution.by = { type: 'result', path: [firstFetch.alias, ...input.input.$path] }
467
+ return {
468
+ ...executionPlan,
469
+ }
470
+ }).filter(s => s !== null)
471
+
472
+ const mapping = {
473
+ [alias]: [firstFetch.alias],
474
+ }
475
+ for(const processedSource of processed) {
476
+ mapping[processedSource.input.$alias] = [processedSource.input.$alias]
477
+ }
478
+ const execution = {
479
+ operation: 'output',
480
+ mapping
481
+ }
482
+ return {
483
+ execution,
484
+ next
485
+ }
486
+ }).filter(s => s !== null)
487
+ return {
488
+ execution: firstFetch,
489
+ next
490
+ }
491
+ })
492
+ }
493
+
494
+ prepareQuery() {
495
+ console.log("CREATE INDEXES", this.indexes)
496
+
497
+ process.exit(0)
498
+
499
+ this.computeExecutionPlan()
500
+ console.log("EXECUTION PLAN:")
501
+ console.log(JSON.stringify(this.executionPlan, null, 2))
502
+ /// TODO: create indexes used by query
503
+
504
+
505
+ /// TODO: prepare query
506
+
507
+ process.exit(0)
508
+ }
509
+
510
+ createIndex() {
511
+ this.computeIndexPlan()
512
+ /// TODO: create index from query
513
+ }
514
+ }
515
+
516
+
517
+ export type QueryFactoryFunction<SDS extends ServiceDefinitionSpecification> =
518
+ (definition: QueryDefinitionSpecification) => QueryDefinition<SDS>
519
+
520
+ export default function queryFactory<SDS extends ServiceDefinitionSpecification>(
521
+ serviceDefinition: ServiceDefinition<SDS>
522
+ ) {
523
+ const queryFactoryFunction: QueryFactoryFunction<SDS> =
524
+ (definition: QueryDefinitionSpecification) => {
525
+ const query = new QueryDefinition<SDS>(serviceDefinition, definition)
526
+ query.prepareQuery()
527
+ return query
528
+ }
529
+ return queryFactoryFunction
530
+ }
531
+
532
+ type RuleInput = QueryInputBase | QueryPropertyBase | any
533
+
534
+ function getSource(input: RuleInput): QuerySource {
535
+ if(input instanceof QueryInputBase) return input.$source
536
+ return null
537
+ }
538
+
539
+ function isStatic(element: any) {
540
+ if(typeof element !== "object" || element === null) return true
541
+ return element instanceof CanBeStatic ? element.$_isStatic() :
542
+ (element.constructor.name === "Object" ? element[staticRuleSymbol] : false)
543
+ }
544
+
545
+ function queryDescription(element: any, indent: string = "") {
546
+ const flags = isStatic(element) ? "static " : ""
547
+ if(typeof element.toQueryDescription === "function")
548
+ return flags + element.toQueryDescription(indent)
549
+ const fields = Object.entries(element)
550
+ .map(([key, value]) => `${indent} ${key}: ${queryDescription(value, indent + " ")}`)
551
+ .join("\n")
552
+ if(element.constructor.name !== "Object") return flags + `${element.constructor.name}(${fields})`
553
+ return flags + '{\n'+fields+`\n${indent}}`;
554
+ }
555
+
556
+ function markStatic(element: any) {
557
+ if(element instanceof CanBeStatic) return element.$_markStatic()
558
+ if(typeof element === "object" && element !== null) {
559
+ let allStatic = true
560
+ for(const key in element) {
561
+ markStatic(element[key])
562
+ if(!isStatic(element[key])) allStatic = false
563
+ }
564
+ if(allStatic) element[staticRuleSymbol] = true
565
+ return
566
+ }
567
+ }
568
+
569
+ function parameterJSON(element: any) {
570
+ if(typeof element !== "object" || element === null) return element
571
+ if(element instanceof QueryPropertyBase) return { property: element.$path }
572
+ const output = {
573
+ type: 'object',
574
+ properties: {}
575
+ }
576
+ for(const key in element) {
577
+ output.properties[key] = parameterJSON(element[key])
578
+ }
579
+ return output
580
+ }
581
+
582
+ function parametersJSONForInput(input: RuleInput, resultParameters: string[]) {
583
+ if(isStatic(input)) {
584
+ return parameterJSON(input)
585
+ } else {
586
+ const resultParameter = resultParameters.find(p => p === input.$alias)
587
+ if(resultParameter) {
588
+ return {
589
+ type: 'result',
590
+ path: [resultParameter, ...input.$path],
591
+ }
592
+ }
593
+ }
594
+ }
595
+
596
+ export class RangeRule extends QueryRule {
597
+ $input: RuleInput
598
+ $range: RuleInput
599
+
600
+ constructor(input: QueryInputBase, range: Range) {
601
+ super()
602
+ this.$input = input
603
+ this.$range = range
604
+ }
605
+
606
+ toQueryDescription(indent: string = "") {
607
+ return `Range(`+
608
+ `\n${indent} ${queryDescription(this.$input, indent + " ")}`+
609
+ `\n${indent} ${queryDescription(this.$range, indent + " ")}`+
610
+ `\n${indent})`
611
+ }
612
+
613
+ $_markStatic() {
614
+ markStatic(this.$input)
615
+ markStatic(this.$range)
616
+ //console.log("MARK STATIC", queryDescription(this.$input), queryDescription(this.$range), isStatic(this.$input), isStatic(this.$range))
617
+ this[staticRuleSymbol] = isStatic(this.$input) && isStatic(this.$range)
618
+ }
619
+
620
+ $_isStatic() {
621
+ return this[staticRuleSymbol]
622
+ }
623
+
624
+ $_hasStaticParameter() {
625
+ return isStatic(this.$input) || isStatic(this.$range)
626
+ }
627
+
628
+ $_getSources(): RuleSource[] {
629
+ return [
630
+ new RuleSource(this, this.$input, getSource(this.$input), 'range')
631
+ ].filter(s => s.input != null)
632
+ }
633
+
634
+ $_parametersJSON(resultParameters: string[]) {
635
+ return {
636
+ input: parametersJSONForInput(this.$input, resultParameters),
637
+ range: parametersJSONForInput(this.$range, resultParameters),
638
+ }
639
+ }
640
+ }
641
+
642
+ export class EqualsRule extends QueryRule {
643
+ $inputA: RuleInput
644
+ $inputB: RuleInput
645
+
646
+ constructor(inputA: RuleInput, inputB: RuleInput) {
647
+ super()
648
+ this.$inputA = inputA
649
+ this.$inputB = inputB
650
+ }
651
+
652
+ toQueryDescription(indent: string = "") {
653
+ return `Equals(`+
654
+ `\n${indent} ${queryDescription(this.$inputA, indent + " ")}`+
655
+ `\n${indent} ${queryDescription(this.$inputB, indent + " ")}`+
656
+ `\n${indent})`
657
+ }
658
+
659
+ $_markStatic() {
660
+ markStatic(this.$inputA)
661
+ markStatic(this.$inputB)
662
+ this[staticRuleSymbol] = isStatic(this.$inputA) && isStatic(this.$inputB)
663
+ }
664
+
665
+ $_isStatic() {
666
+ return this[staticRuleSymbol]
667
+ }
668
+
669
+ $_hasStaticParameter() {
670
+ return isStatic(this.$inputA) || isStatic(this.$inputB)
671
+ }
672
+
673
+ $_getSources(): RuleSource[] {
674
+ return [
675
+ new RuleSource(this, this.$inputA, getSource(this.$inputA), 'object'),
676
+ new RuleSource(this, this.$inputB, getSource(this.$inputB), 'object')
677
+ ].filter(s => s.input != null)
678
+ }
679
+
680
+ $_parametersJSON(resultParameters: string[]) {
681
+ return {
682
+ inputA: parametersJSONForInput(this.$inputA, resultParameters),
683
+ inputB: parametersJSONForInput(this.$inputB, resultParameters),
684
+ }
685
+ }
686
+ }
687
+
688
+ function sourceType(source: QuerySource) {
689
+ return (source instanceof ModelDefinition || source instanceof ForeignModelDefinition)
690
+ ? "table" : "index"
691
+ }
692
+
693
+ export class QueryInputBase extends CanBeStatic {
694
+ $source: QuerySource
695
+ $path: string[]
696
+ $alias: string
697
+
698
+ $inside(range: Range) {
699
+ return new RangeRule(this, range)
700
+ }
701
+
702
+ $equals(value: any) {
703
+ return new EqualsRule(this, value)
704
+ }
705
+
706
+ $as(alias: string) {
707
+ return createQueryInputProxy(new QueryInputBase(this.$source, this.$path, alias))
708
+ }
709
+
710
+ constructor(source: QuerySource, path: string[], alias: string = undefined) {
711
+ super()
712
+ this.$source = source
713
+ this.$path = path
714
+ this.$alias = alias
715
+ }
716
+
717
+ toQueryDescription(indent: string = "") {
718
+ return `QueryInput(\n${indent} source: ${queryDescription(this.$source, indent + " ")}`+
719
+ `\n${indent} path: ${this.$path.join(".")}`+
720
+ `\n${indent} alias: ${this.$alias}`+
721
+ `\n${indent})`
722
+ }
723
+
724
+ $_markStatic() {
725
+ /// ignore - QueryInput is not static
726
+ }
727
+
728
+ $_isStatic() {
729
+ return false
730
+ }
731
+
732
+ $_canBeUsedAsSource(input: QueryInputBase) {
733
+ return this.$source === input.$source && this.$alias === input.$alias
734
+ }
735
+
736
+ $_getIndexInfo(indexes: IndexInfo[]): IndexInfo | null {
737
+ if(this.$path.length === 0) return null // id is used
738
+ if(this.$path.length === 1 && this.$path[0] === "id") return null // id is used
739
+ return new IndexInfo([
740
+ new RangeRule(this, {}),
741
+ ], [
742
+ new OutputMapping(this.$alias, this.$path, this.$path[this.$path.length - 1]),
743
+ new OutputMapping(this.$alias, ['id'], 'to')
744
+ ])
745
+ }
746
+
747
+ $_executionJSON() {
748
+ return {
749
+ sourceType: sourceType(this.$source),
750
+ name: this.$source.getTypeName(),
751
+ path: [...this.$path],
752
+ alias: this.$alias
753
+ }
754
+ }
755
+ $_equals(other: QueryInputBase) {
756
+ return this.$source === other.$source && this.$path.join(".") === other.$path.join(".") && this.$alias === other.$alias
757
+ }
758
+ }
759
+
760
+ export class QueryInput extends QueryInputBase {
761
+ [key: string]: QueryInputBase | any /// Proxy class will be added to this
762
+ }
763
+
764
+
765
+ export function createQueryInputProxy(
766
+ base: QueryInputBase
767
+ ) {
768
+ return new Proxy(base, {
769
+ get(target, prop, receiver) {
770
+ const foundInBase = Reflect.get(target, prop, receiver)
771
+ if(foundInBase) return foundInBase
772
+ const newBase = new QueryInputBase(base.$source, [...base.$path, prop as string], base.$alias)
773
+ const inputProxy = createQueryInputProxy(newBase)
774
+ return inputProxy
775
+ }
776
+ })
777
+ }
778
+
779
+ export class QueryPropertyBase extends CanBeStatic {
780
+ $path: string[]
781
+
782
+ constructor(path: string[]) {
783
+ super()
784
+ this.$path = path
785
+ }
786
+
787
+ toQueryDescription(indent: string = "") {
788
+ return `QueryProperty(${this.$path.join(".")})`
789
+ }
790
+
791
+ $_isStatic() {
792
+ return true
793
+ }
794
+
795
+ $_markStatic() {
796
+ /// ignore - QueryProperty is always static
797
+ }
798
+
799
+ $_executionJSON() {
800
+ return {
801
+ type: "property",
802
+ path: this.$path
803
+ }
804
+ }
805
+ }
806
+
807
+ export class QueryProperty extends QueryPropertyBase {
808
+ [key: string]: QueryPropertyBase | any /// Proxy class will be added to this
809
+ }
810
+
811
+ export function createQueryPropertyProxy(
812
+ base: QueryPropertyBase, propertyName: string, propertyDefinition: PropertyDefinition<any>
813
+ ) {
814
+ return new Proxy(base, {
815
+ get(target, prop, receiver) {
816
+ const foundInBase = Reflect.get(target, prop, receiver)
817
+ if(foundInBase) return foundInBase
818
+ const propertyBase = new QueryPropertyBase([...base.$path, propertyName])
819
+ const propertyProxy = createQueryPropertyProxy(propertyBase, propertyName, propertyDefinition)
820
+ return propertyProxy
821
+ }
822
+ })
823
+ }