@effect-app/infra 4.0.0-beta.212 → 4.0.0-beta.214
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/CHANGELOG.md +28 -0
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
- package/dist/Model/Repository/internal/internal.js +6 -2
- package/dist/Model/Repository/validation.d.ts +8 -8
- package/dist/Model/query/dsl.d.ts +76 -1
- package/dist/Model/query/dsl.d.ts.map +1 -1
- package/dist/Model/query/dsl.js +111 -1
- package/dist/Model/query/new-kid-interpreter.d.ts +38 -2
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +80 -4
- package/dist/RequestContext.d.ts +12 -12
- package/dist/Store/Cosmos/query.d.ts +5 -1
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +63 -23
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +1 -1
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +86 -2
- package/dist/Store/SQL/Pg.d.ts.map +1 -1
- package/dist/Store/SQL/Pg.js +1 -1
- package/dist/Store/SQL/query.d.ts +5 -1
- package/dist/Store/SQL/query.d.ts.map +1 -1
- package/dist/Store/SQL/query.js +51 -1
- package/dist/Store/SQL.d.ts.map +1 -1
- package/dist/Store/SQL.js +1 -1
- package/dist/Store/service.d.ts +5 -2
- package/dist/Store/service.d.ts.map +1 -1
- package/dist/Store/service.js +1 -1
- package/package.json +2 -2
- package/src/Model/Repository/internal/internal.ts +5 -1
- package/src/Model/query/dsl.ts +191 -0
- package/src/Model/query/new-kid-interpreter.ts +124 -4
- package/src/Store/Cosmos/query.ts +80 -23
- package/src/Store/Cosmos.ts +10 -2
- package/src/Store/Memory.ts +96 -4
- package/src/Store/SQL/Pg.ts +10 -1
- package/src/Store/SQL/query.ts +65 -1
- package/src/Store/SQL.ts +19 -2
- package/src/Store/service.ts +9 -2
- package/test/query.test.ts +156 -1
- package/test/rawQuery.test.ts +36 -1
- package/test/sql-store.test.ts +362 -0
package/src/Model/query/dsl.ts
CHANGED
|
@@ -97,6 +97,46 @@ export type QueryProjection<
|
|
|
97
97
|
R,
|
|
98
98
|
TType
|
|
99
99
|
>
|
|
100
|
+
|
|
101
|
+
export type ComputedProjectionOperation = (q: Query<any>) => QueryWhere<any, any, any>
|
|
102
|
+
|
|
103
|
+
export type ComputedProjectionExpression =
|
|
104
|
+
| {
|
|
105
|
+
readonly _tag: "relation-count"
|
|
106
|
+
readonly path: string
|
|
107
|
+
readonly operation?: ComputedProjectionOperation
|
|
108
|
+
}
|
|
109
|
+
| {
|
|
110
|
+
readonly _tag: "relation-any"
|
|
111
|
+
readonly path: string
|
|
112
|
+
readonly operation?: ComputedProjectionOperation
|
|
113
|
+
}
|
|
114
|
+
| {
|
|
115
|
+
readonly _tag: "relation-every"
|
|
116
|
+
readonly path: string
|
|
117
|
+
readonly operation: ComputedProjectionOperation
|
|
118
|
+
}
|
|
119
|
+
| {
|
|
120
|
+
readonly _tag: "relation-distinct-count"
|
|
121
|
+
readonly path: string
|
|
122
|
+
readonly field: string
|
|
123
|
+
readonly operation?: ComputedProjectionOperation
|
|
124
|
+
}
|
|
125
|
+
| {
|
|
126
|
+
readonly _tag: "relation-sum"
|
|
127
|
+
readonly path: string
|
|
128
|
+
readonly field: string
|
|
129
|
+
readonly operation?: ComputedProjectionOperation
|
|
130
|
+
}
|
|
131
|
+
| {
|
|
132
|
+
readonly _tag: "relation-collect"
|
|
133
|
+
readonly path: string
|
|
134
|
+
readonly field: string
|
|
135
|
+
readonly distinct: boolean
|
|
136
|
+
readonly operation?: ComputedProjectionOperation
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type ComputedProjectionMap = Readonly<Record<string, ComputedProjectionExpression>>
|
|
100
140
|
export type Q<TFieldValues extends FieldValues> =
|
|
101
141
|
| Initial<TFieldValues>
|
|
102
142
|
| Where<TFieldValues>
|
|
@@ -211,6 +251,7 @@ export class Project<A, TFieldValues extends FieldValues, R, TType extends "one"
|
|
|
211
251
|
current: Query<TFieldValues> | QueryWhere<any, TFieldValues> | QueryEnd<TFieldValues, TType>
|
|
212
252
|
schema: S.Codec<A, TFieldValues, R>
|
|
213
253
|
mode: "collect" | "project" | "transform"
|
|
254
|
+
computed?: ComputedProjectionMap
|
|
214
255
|
}>
|
|
215
256
|
implements QueryProjection<TFieldValues, A, R>
|
|
216
257
|
{
|
|
@@ -299,6 +340,35 @@ export const count: {
|
|
|
299
340
|
): QueryProjection<ExtractFieldValuesRefined<Q>, NonNegativeInt, never, "count", ExtractExclusiveness<Q>>
|
|
300
341
|
} = (current) => new Count({ current })
|
|
301
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Attach a projection schema to a query.
|
|
345
|
+
*
|
|
346
|
+
* The `select` clause sent to the store is derived from the schema's encoded
|
|
347
|
+
* AST property names (top-level + per-array sub-keys), so a projection always
|
|
348
|
+
* narrows what is read from the store. The repository augments that select
|
|
349
|
+
* with `id` and `_etag` for change tracking. See {@link toFilter} and the
|
|
350
|
+
* dispatch in `Repository/internal/internal.ts`.
|
|
351
|
+
*
|
|
352
|
+
* Modes — pick based on shape of the decoded value and on whether the
|
|
353
|
+
* persistence-model (PM) reverse-mapping is needed:
|
|
354
|
+
*
|
|
355
|
+
* - `"transform"` (default when `mode` omitted): goes through the repo's
|
|
356
|
+
* `parseMany`/`parseMany2` pipeline. The raw row is reverse-mapped via the
|
|
357
|
+
* etag/PM cache (re-injecting `_etag` and any PM-shape state) before
|
|
358
|
+
* decoding. Decode failures `orDie` (error channel = `never`). Use when
|
|
359
|
+
* the schema operates on the full PM shape (e.g. full-entity reads that
|
|
360
|
+
* must preserve etag tracking).
|
|
361
|
+
*
|
|
362
|
+
* - `"project"`: decodes the raw encoded row directly with the supplied
|
|
363
|
+
* schema. No PM reverse-mapping, no etag cache merge. Decode failures
|
|
364
|
+
* surface as `S.SchemaError`. Use for slim DTOs / aggregations that do not
|
|
365
|
+
* need etag tracking and whose schema input is a plain subset of `Encoded`.
|
|
366
|
+
*
|
|
367
|
+
* - `"collect"`: like `"project"`, but the schema yields `Option<A>` and
|
|
368
|
+
* `None` values are dropped post-decode (`Array.getSomes`). Use to filter
|
|
369
|
+
* rows during decode (e.g. discriminated-union narrowing where some rows
|
|
370
|
+
* should not appear in the result).
|
|
371
|
+
*/
|
|
302
372
|
export const project: {
|
|
303
373
|
<
|
|
304
374
|
Q extends Query<any> | QueryWhere<any, any, any> | QueryEnd<any, "one" | "many", any>,
|
|
@@ -356,6 +426,127 @@ export const project: {
|
|
|
356
426
|
) => QueryProjection<ExtractFieldValuesRefined<Q>, A, R, ExtractTType<Q>, E>
|
|
357
427
|
} = (schema: any, mode = "transform") => (current: any) => new Project({ current, schema, mode } as any)
|
|
358
428
|
|
|
429
|
+
export const relation = <TFieldValues extends FieldValues>(
|
|
430
|
+
path: FieldPath<TFieldValues>
|
|
431
|
+
) => ({
|
|
432
|
+
count: (operation?: ComputedProjectionOperation): ComputedProjectionExpression =>
|
|
433
|
+
operation
|
|
434
|
+
? {
|
|
435
|
+
_tag: "relation-count",
|
|
436
|
+
path: path as string,
|
|
437
|
+
operation
|
|
438
|
+
}
|
|
439
|
+
: {
|
|
440
|
+
_tag: "relation-count",
|
|
441
|
+
path: path as string
|
|
442
|
+
},
|
|
443
|
+
any: (operation?: ComputedProjectionOperation): ComputedProjectionExpression =>
|
|
444
|
+
operation
|
|
445
|
+
? {
|
|
446
|
+
_tag: "relation-any",
|
|
447
|
+
path: path as string,
|
|
448
|
+
operation
|
|
449
|
+
}
|
|
450
|
+
: {
|
|
451
|
+
_tag: "relation-any",
|
|
452
|
+
path: path as string
|
|
453
|
+
},
|
|
454
|
+
every: (operation: ComputedProjectionOperation): ComputedProjectionExpression => ({
|
|
455
|
+
_tag: "relation-every",
|
|
456
|
+
path: path as string,
|
|
457
|
+
operation
|
|
458
|
+
}),
|
|
459
|
+
distinctCount: (field: string, operation?: ComputedProjectionOperation): ComputedProjectionExpression =>
|
|
460
|
+
operation
|
|
461
|
+
? {
|
|
462
|
+
_tag: "relation-distinct-count",
|
|
463
|
+
path: path as string,
|
|
464
|
+
field,
|
|
465
|
+
operation
|
|
466
|
+
}
|
|
467
|
+
: {
|
|
468
|
+
_tag: "relation-distinct-count",
|
|
469
|
+
path: path as string,
|
|
470
|
+
field
|
|
471
|
+
},
|
|
472
|
+
sum: (field: string, operation?: ComputedProjectionOperation): ComputedProjectionExpression =>
|
|
473
|
+
operation
|
|
474
|
+
? {
|
|
475
|
+
_tag: "relation-sum",
|
|
476
|
+
path: path as string,
|
|
477
|
+
field,
|
|
478
|
+
operation
|
|
479
|
+
}
|
|
480
|
+
: {
|
|
481
|
+
_tag: "relation-sum",
|
|
482
|
+
path: path as string,
|
|
483
|
+
field
|
|
484
|
+
},
|
|
485
|
+
collect: (field: string, operation?: ComputedProjectionOperation): ComputedProjectionExpression =>
|
|
486
|
+
operation
|
|
487
|
+
? {
|
|
488
|
+
_tag: "relation-collect",
|
|
489
|
+
path: path as string,
|
|
490
|
+
field,
|
|
491
|
+
distinct: false,
|
|
492
|
+
operation
|
|
493
|
+
}
|
|
494
|
+
: {
|
|
495
|
+
_tag: "relation-collect",
|
|
496
|
+
path: path as string,
|
|
497
|
+
field,
|
|
498
|
+
distinct: false
|
|
499
|
+
},
|
|
500
|
+
collectDistinct: (field: string, operation?: ComputedProjectionOperation): ComputedProjectionExpression =>
|
|
501
|
+
operation
|
|
502
|
+
? {
|
|
503
|
+
_tag: "relation-collect",
|
|
504
|
+
path: path as string,
|
|
505
|
+
field,
|
|
506
|
+
distinct: true,
|
|
507
|
+
operation
|
|
508
|
+
}
|
|
509
|
+
: {
|
|
510
|
+
_tag: "relation-collect",
|
|
511
|
+
path: path as string,
|
|
512
|
+
field,
|
|
513
|
+
distinct: true
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
export const computed = <T extends ComputedProjectionMap>(value: T): T => value
|
|
518
|
+
|
|
519
|
+
export const projectComputed: {
|
|
520
|
+
<
|
|
521
|
+
Q extends Query<any> | QueryWhere<any, any, any> | QueryEnd<any, "one" | "many", any>,
|
|
522
|
+
I extends Record<string, unknown>,
|
|
523
|
+
A = ExtractFieldValuesRefined<Q>,
|
|
524
|
+
R = never,
|
|
525
|
+
E extends boolean = ExtractExclusiveness<Q>
|
|
526
|
+
>(
|
|
527
|
+
schema: S.Codec<Option.Option<A>, I, R>,
|
|
528
|
+
computedProjection: ComputedProjectionMap,
|
|
529
|
+
mode: "collect"
|
|
530
|
+
): (
|
|
531
|
+
current: Q
|
|
532
|
+
) => QueryProjection<ExtractFieldValuesRefined<Q>, A, R, ExtractTType<Q>, E>
|
|
533
|
+
|
|
534
|
+
<
|
|
535
|
+
Q extends Query<any> | QueryWhere<any, any, any> | QueryEnd<any, "one" | "many", any>,
|
|
536
|
+
I extends Record<string, unknown>,
|
|
537
|
+
A = ExtractFieldValuesRefined<Q>,
|
|
538
|
+
R = never,
|
|
539
|
+
E extends boolean = ExtractExclusiveness<Q>
|
|
540
|
+
>(
|
|
541
|
+
schema: S.Codec<A, I, R>,
|
|
542
|
+
computedProjection: ComputedProjectionMap,
|
|
543
|
+
mode?: "project"
|
|
544
|
+
): (
|
|
545
|
+
current: Q
|
|
546
|
+
) => QueryProjection<ExtractFieldValuesRefined<Q>, A, R, ExtractTType<Q>, E>
|
|
547
|
+
} = (schema: any, computedProjection: ComputedProjectionMap, mode = "project") => (current: any) =>
|
|
548
|
+
new Project({ current, schema, mode, computed: computedProjection } as any)
|
|
549
|
+
|
|
359
550
|
type GetArV<T> = T extends readonly (infer R)[] ? R : never
|
|
360
551
|
|
|
361
552
|
export type FilterContinuations<IsCurrentInitial extends boolean = false> = {
|
|
@@ -8,6 +8,42 @@ import type { FieldValues } from "../filter/types.js"
|
|
|
8
8
|
import type { FieldPath } from "../filter/types/path/eager.js"
|
|
9
9
|
import { make, type Q, type QAll } from "../query/dsl.js"
|
|
10
10
|
|
|
11
|
+
export type ComputedProjectionIrExpression =
|
|
12
|
+
| {
|
|
13
|
+
readonly _tag: "relation-count"
|
|
14
|
+
readonly path: string
|
|
15
|
+
readonly filter: readonly FilterResult[]
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
readonly _tag: "relation-any"
|
|
19
|
+
readonly path: string
|
|
20
|
+
readonly filter: readonly FilterResult[]
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
readonly _tag: "relation-every"
|
|
24
|
+
readonly path: string
|
|
25
|
+
readonly filter: readonly FilterResult[]
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
readonly _tag: "relation-distinct-count"
|
|
29
|
+
readonly path: string
|
|
30
|
+
readonly field: string
|
|
31
|
+
readonly filter: readonly FilterResult[]
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
readonly _tag: "relation-sum"
|
|
35
|
+
readonly path: string
|
|
36
|
+
readonly field: string
|
|
37
|
+
readonly filter: readonly FilterResult[]
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
readonly _tag: "relation-collect"
|
|
41
|
+
readonly path: string
|
|
42
|
+
readonly field: string
|
|
43
|
+
readonly distinct: boolean
|
|
44
|
+
readonly filter: readonly FilterResult[]
|
|
45
|
+
}
|
|
46
|
+
|
|
11
47
|
type Result<TFieldValues extends FieldValues, A = TFieldValues, R = never> = {
|
|
12
48
|
filter: FilterResult[]
|
|
13
49
|
schema: S.Codec<A, TFieldValues, R> | undefined
|
|
@@ -16,6 +52,7 @@ type Result<TFieldValues extends FieldValues, A = TFieldValues, R = never> = {
|
|
|
16
52
|
order: { key: FieldPath<TFieldValues>; direction: "ASC" | "DESC" }[]
|
|
17
53
|
ttype: "one" | "many" | "count" | undefined
|
|
18
54
|
mode: "collect" | "project" | "transform" | undefined
|
|
55
|
+
computed: Record<string, ComputedProjectionIrExpression> | undefined
|
|
19
56
|
}
|
|
20
57
|
|
|
21
58
|
const interpret = <
|
|
@@ -33,7 +70,8 @@ const interpret = <
|
|
|
33
70
|
skip: undefined,
|
|
34
71
|
order: [],
|
|
35
72
|
ttype: undefined,
|
|
36
|
-
mode: undefined
|
|
73
|
+
mode: undefined,
|
|
74
|
+
computed: undefined
|
|
37
75
|
}
|
|
38
76
|
|
|
39
77
|
const upd = (
|
|
@@ -46,6 +84,7 @@ const interpret = <
|
|
|
46
84
|
if (v.ttype !== undefined) data.ttype = v.ttype
|
|
47
85
|
if (v.schema !== undefined) data.schema = v.schema
|
|
48
86
|
if (v.mode !== undefined) data.mode = v.mode
|
|
87
|
+
if (v.computed !== undefined) data.computed = v.computed
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
const applyPath = (path: string) => (_: FilterResult): FilterResult =>
|
|
@@ -135,8 +174,44 @@ const interpret = <
|
|
|
135
174
|
},
|
|
136
175
|
project: (v) => {
|
|
137
176
|
upd(interpret(v.current))
|
|
177
|
+
if (v.computed && v.mode === "transform") {
|
|
178
|
+
throw new Error("Computed projections require mode 'project' or 'collect', not 'transform'")
|
|
179
|
+
}
|
|
138
180
|
data.schema = v.schema
|
|
139
|
-
data.mode = v.
|
|
181
|
+
data.mode = v.computed
|
|
182
|
+
? v.mode === "collect" ? "collect" : "project"
|
|
183
|
+
: v.mode
|
|
184
|
+
data.computed = v.computed
|
|
185
|
+
? Object.fromEntries(
|
|
186
|
+
Object.entries(v.computed).map(([key, expression]) => {
|
|
187
|
+
const e = expression
|
|
188
|
+
const filter = e.operation ? interpret(e.operation(make())).filter.map(applyPath(e.path)) : []
|
|
189
|
+
switch (e._tag) {
|
|
190
|
+
case "relation-count":
|
|
191
|
+
case "relation-any":
|
|
192
|
+
case "relation-every":
|
|
193
|
+
return [key, { _tag: e._tag, path: e.path, filter } as ComputedProjectionIrExpression]
|
|
194
|
+
case "relation-distinct-count":
|
|
195
|
+
case "relation-sum":
|
|
196
|
+
return [
|
|
197
|
+
key,
|
|
198
|
+
{ _tag: e._tag, path: e.path, field: e.field, filter } as ComputedProjectionIrExpression
|
|
199
|
+
]
|
|
200
|
+
case "relation-collect":
|
|
201
|
+
return [
|
|
202
|
+
key,
|
|
203
|
+
{
|
|
204
|
+
_tag: e._tag,
|
|
205
|
+
path: e.path,
|
|
206
|
+
field: e.field,
|
|
207
|
+
distinct: e.distinct,
|
|
208
|
+
filter
|
|
209
|
+
} as ComputedProjectionIrExpression
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
)
|
|
214
|
+
: undefined
|
|
140
215
|
}
|
|
141
216
|
})
|
|
142
217
|
)
|
|
@@ -157,12 +232,16 @@ export const toFilter = <
|
|
|
157
232
|
R,
|
|
158
233
|
TFieldValuesRefined extends TFieldValues = TFieldValues
|
|
159
234
|
>(
|
|
160
|
-
q: QAll<TFieldValues, TFieldValuesRefined, A, R
|
|
235
|
+
q: QAll<TFieldValues, TFieldValuesRefined, A, R>,
|
|
236
|
+
baseSchema?: S.Schema<unknown>
|
|
161
237
|
) => {
|
|
162
238
|
// TODO: Native interpreter for each db adapter, instead of the intermediate "new-kid" format
|
|
163
239
|
const a = interpret(q)
|
|
164
240
|
const schema = a.schema
|
|
165
|
-
let select: (keyof TFieldValues | { key: string; subKeys: string[] }
|
|
241
|
+
let select: (keyof TFieldValues | { key: string; subKeys: string[] } | {
|
|
242
|
+
key: string
|
|
243
|
+
computed: ComputedProjectionIrExpression
|
|
244
|
+
})[] = []
|
|
166
245
|
// TODO: support more complex (nested) schemas?
|
|
167
246
|
if (schema) {
|
|
168
247
|
const t = walkTransformation(SchemaAST.toEncoded(schema.ast))
|
|
@@ -194,12 +273,53 @@ export const toFilter = <
|
|
|
194
273
|
}
|
|
195
274
|
}
|
|
196
275
|
}
|
|
276
|
+
const computed = a.computed
|
|
277
|
+
const getSelectKey = (_: (typeof select)[number]) => {
|
|
278
|
+
if (typeof _ === "string") {
|
|
279
|
+
return _
|
|
280
|
+
}
|
|
281
|
+
if (typeof _ === "object" && _ !== null && "key" in _) {
|
|
282
|
+
return _.key
|
|
283
|
+
}
|
|
284
|
+
return String(_)
|
|
285
|
+
}
|
|
286
|
+
const schemaKeys = select.map(getSelectKey)
|
|
287
|
+
const nonEncodedSchemaKeys = (() => {
|
|
288
|
+
if (!baseSchema) {
|
|
289
|
+
return [] as string[]
|
|
290
|
+
}
|
|
291
|
+
const encoded = walkTransformation(SchemaAST.toEncoded(baseSchema.ast))
|
|
292
|
+
if (!S.AST.isObjects(encoded)) {
|
|
293
|
+
return [] as string[]
|
|
294
|
+
}
|
|
295
|
+
const encodedKeys = encoded.propertySignatures.map((_) => _.name as string)
|
|
296
|
+
return schemaKeys.filter((key) => !encodedKeys.includes(key))
|
|
297
|
+
})()
|
|
298
|
+
const missingComputedKeys = nonEncodedSchemaKeys.filter((key) => !(computed && key in computed))
|
|
299
|
+
|
|
300
|
+
if (Array.isArrayNonEmpty(missingComputedKeys)) {
|
|
301
|
+
throw new Error(`Missing computed projections for schema keys: ${missingComputedKeys.join(", ")}`)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (computed) {
|
|
305
|
+
const computedKeys = Object.keys(computed)
|
|
306
|
+
const extraComputedKeys = computedKeys.filter((key) => !schemaKeys.includes(key))
|
|
307
|
+
if (Array.isArrayNonEmpty(extraComputedKeys)) {
|
|
308
|
+
throw new Error(`Computed projection keys must exist in projection schema: ${extraComputedKeys.join(", ")}`)
|
|
309
|
+
}
|
|
310
|
+
select = select.filter((_) => {
|
|
311
|
+
const key = getSelectKey(_)
|
|
312
|
+
return !(key in computed)
|
|
313
|
+
})
|
|
314
|
+
select.push(...Object.entries(computed).map(([key, expression]) => ({ key, computed: expression })))
|
|
315
|
+
}
|
|
197
316
|
return dropUndefinedT({
|
|
198
317
|
t: null as unknown as TFieldValues,
|
|
199
318
|
limit: a.limit,
|
|
200
319
|
skip: a.skip,
|
|
201
320
|
select: Option.getOrUndefined(toNonEmptyArray(select)),
|
|
202
321
|
schema,
|
|
322
|
+
computed,
|
|
203
323
|
order: Option.getOrUndefined(toNonEmptyArray(a.order)),
|
|
204
324
|
ttype: a.ttype,
|
|
205
325
|
mode: a.mode ?? "transform",
|
|
@@ -4,6 +4,7 @@ import { Array, Effect, type NonEmptyReadonlyArray } from "effect-app"
|
|
|
4
4
|
import { assertUnreachable } from "effect-app/utils"
|
|
5
5
|
import { InfraLogger } from "../../logger.js"
|
|
6
6
|
import type { FilterR, FilterResult, Ops } from "../../Model/filter/filterApi.js"
|
|
7
|
+
import type { ComputedProjectionIrExpression } from "../../Model/query.js"
|
|
7
8
|
import { isRelationCheck } from "../codeFilter.js"
|
|
8
9
|
import type { SupportedValues } from "../service.js"
|
|
9
10
|
|
|
@@ -40,12 +41,20 @@ export function buildWhereCosmosQuery3(
|
|
|
40
41
|
filter: readonly FilterResult[],
|
|
41
42
|
name: string,
|
|
42
43
|
defaultValues: Record<string, unknown>,
|
|
43
|
-
select?: NonEmptyReadonlyArray<
|
|
44
|
+
select?: NonEmptyReadonlyArray<
|
|
45
|
+
string | {
|
|
46
|
+
key: string
|
|
47
|
+
subKeys: readonly string[]
|
|
48
|
+
} | {
|
|
49
|
+
key: string
|
|
50
|
+
computed: ComputedProjectionIrExpression
|
|
51
|
+
}
|
|
52
|
+
>,
|
|
44
53
|
order?: NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }>,
|
|
45
54
|
skip?: number,
|
|
46
55
|
limit?: number
|
|
47
56
|
) {
|
|
48
|
-
const statement = (x: FilterR, i: number
|
|
57
|
+
const statement = (x: FilterR, i: number) => {
|
|
49
58
|
if (x.path === idKey) {
|
|
50
59
|
x = { ...x, path: "id" }
|
|
51
60
|
}
|
|
@@ -60,8 +69,6 @@ export function buildWhereCosmosQuery3(
|
|
|
60
69
|
|
|
61
70
|
const v = "@v" + i
|
|
62
71
|
|
|
63
|
-
const realValue = values[i]
|
|
64
|
-
|
|
65
72
|
switch (x.op) {
|
|
66
73
|
case "in":
|
|
67
74
|
return `ARRAY_CONTAINS(${v}, ${k})`
|
|
@@ -74,14 +81,22 @@ export function buildWhereCosmosQuery3(
|
|
|
74
81
|
return `(NOT ARRAY_CONTAINS(${k}, ${v}))`
|
|
75
82
|
|
|
76
83
|
case "includes-any":
|
|
77
|
-
return `ARRAY_CONTAINS_ANY(${k}, ${
|
|
84
|
+
return `ARRAY_CONTAINS_ANY(${k}, ${
|
|
85
|
+
(x.value as unknown as readonly unknown[]).map((_, i) => `${v}__${i}`).join(", ")
|
|
86
|
+
})`
|
|
78
87
|
case "notIncludes-any":
|
|
79
|
-
return `(NOT ARRAY_CONTAINS_ANY(${k}, ${
|
|
88
|
+
return `(NOT ARRAY_CONTAINS_ANY(${k}, ${
|
|
89
|
+
(x.value as unknown as readonly unknown[]).map((_, i) => `${v}__${i}`).join(", ")
|
|
90
|
+
}))`
|
|
80
91
|
|
|
81
92
|
case "includes-all":
|
|
82
|
-
return `ARRAY_CONTAINS_ALL(${k}, ${
|
|
93
|
+
return `ARRAY_CONTAINS_ALL(${k}, ${
|
|
94
|
+
(x.value as unknown as readonly unknown[]).map((_, i) => `${v}__${i}`).join(", ")
|
|
95
|
+
})`
|
|
83
96
|
case "notIncludes-all":
|
|
84
|
-
return `(NOT ARRAY_CONTAINS_ALL(${k}, ${
|
|
97
|
+
return `(NOT ARRAY_CONTAINS_ALL(${k}, ${
|
|
98
|
+
(x.value as unknown as readonly unknown[]).map((_, i) => `${v}__${i}`).join(", ")
|
|
99
|
+
}))`
|
|
85
100
|
|
|
86
101
|
case "contains":
|
|
87
102
|
return `CONTAINS(${k}, ${v}, true)`
|
|
@@ -165,7 +180,7 @@ export function buildWhereCosmosQuery3(
|
|
|
165
180
|
: _
|
|
166
181
|
: _
|
|
167
182
|
|
|
168
|
-
const print = (state: readonly FilterResult[],
|
|
183
|
+
const print = (state: readonly FilterResult[], isRelation: string | null, every: boolean) => {
|
|
169
184
|
let s = ""
|
|
170
185
|
let l = 0
|
|
171
186
|
const printN = (n: number) => {
|
|
@@ -174,13 +189,13 @@ export function buildWhereCosmosQuery3(
|
|
|
174
189
|
for (const e of state) {
|
|
175
190
|
switch (e.t) {
|
|
176
191
|
case "where":
|
|
177
|
-
s += statement(e, i
|
|
192
|
+
s += statement(e, i++)
|
|
178
193
|
break
|
|
179
194
|
case "or":
|
|
180
|
-
s += ` OR ${statement(e, i
|
|
195
|
+
s += ` OR ${statement(e, i++)}`
|
|
181
196
|
break
|
|
182
197
|
case "and":
|
|
183
|
-
s += ` AND ${statement(e, i
|
|
198
|
+
s += ` AND ${statement(e, i++)}`
|
|
184
199
|
break
|
|
185
200
|
case "or-scope": {
|
|
186
201
|
++l
|
|
@@ -189,7 +204,7 @@ export function buildWhereCosmosQuery3(
|
|
|
189
204
|
if (rel) {
|
|
190
205
|
const rel = (e.result[0]! as { path: string }).path.split(".-1.")[0]
|
|
191
206
|
s += isRelation
|
|
192
|
-
? ` OR (\n${printN(l + 1)}${print(e.result,
|
|
207
|
+
? ` OR (\n${printN(l + 1)}${print(e.result, rel, every)}\n${printN(l)})`
|
|
193
208
|
: ` OR (\n${printN(l + 1)}${
|
|
194
209
|
every ? "NOT " : ""
|
|
195
210
|
}EXISTS(SELECT VALUE ${rel} FROM ${rel} IN f.${rel} WHERE ${
|
|
@@ -197,13 +212,12 @@ export function buildWhereCosmosQuery3(
|
|
|
197
212
|
e
|
|
198
213
|
.result
|
|
199
214
|
.map(flip(every)),
|
|
200
|
-
values,
|
|
201
215
|
rel,
|
|
202
216
|
every
|
|
203
217
|
)
|
|
204
218
|
}))`
|
|
205
219
|
} else {
|
|
206
|
-
s += ` OR (\n${printN(l + 1)}${print(e.result,
|
|
220
|
+
s += ` OR (\n${printN(l + 1)}${print(e.result, null, every)}\n${printN(l)})`
|
|
207
221
|
}
|
|
208
222
|
--l
|
|
209
223
|
break
|
|
@@ -215,14 +229,14 @@ export function buildWhereCosmosQuery3(
|
|
|
215
229
|
if (rel) {
|
|
216
230
|
const rel = (e.result[0]! as { path: string }).path.split(".-1.")[0]
|
|
217
231
|
s += isRelation
|
|
218
|
-
? ` AND (\n${printN(l + 1)}${print(e.result,
|
|
232
|
+
? ` AND (\n${printN(l + 1)}${print(e.result, rel, every)}\n${printN(l)})`
|
|
219
233
|
: ` AND (\n${printN(l + 1)}${
|
|
220
234
|
every ? "NOT " : ""
|
|
221
235
|
}EXISTS(SELECT VALUE ${rel} FROM ${rel} IN f.${rel} WHERE ${
|
|
222
|
-
print(e.result.map(flip(every)),
|
|
236
|
+
print(e.result.map(flip(every)), rel, every)
|
|
223
237
|
}))`
|
|
224
238
|
} else {
|
|
225
|
-
s += ` AND (\n${printN(l + 1)}${print(e.result,
|
|
239
|
+
s += ` AND (\n${printN(l + 1)}${print(e.result, null, every)}\n${printN(l)})`
|
|
226
240
|
}
|
|
227
241
|
--l
|
|
228
242
|
break
|
|
@@ -234,12 +248,12 @@ export function buildWhereCosmosQuery3(
|
|
|
234
248
|
if (rel) {
|
|
235
249
|
const rel = (e.result[0]! as { path: string }).path.split(".-1.")[0]
|
|
236
250
|
s += isRelation
|
|
237
|
-
? `(\n${printN(l + 1)}${print(e.result,
|
|
251
|
+
? `(\n${printN(l + 1)}${print(e.result, rel, every)}\n${printN(l)})`
|
|
238
252
|
: `(\n${printN(l + 1)}${every ? "NOT " : ""}EXISTS(SELECT VALUE ${rel} FROM ${rel} IN f.${rel} WHERE ${
|
|
239
|
-
print(e.result.map(flip(every)),
|
|
253
|
+
print(e.result.map(flip(every)), rel, every)
|
|
240
254
|
}))`
|
|
241
255
|
} else {
|
|
242
|
-
s += `(\n${printN(l + 1)}${print(e.result,
|
|
256
|
+
s += `(\n${printN(l + 1)}${print(e.result, null, every)}\n${printN(l)})`
|
|
243
257
|
}
|
|
244
258
|
// ;--l
|
|
245
259
|
break
|
|
@@ -272,7 +286,48 @@ export function buildWhereCosmosQuery3(
|
|
|
272
286
|
? getValues(_.result)
|
|
273
287
|
: [_]
|
|
274
288
|
)
|
|
275
|
-
const
|
|
289
|
+
const computedFilters = select
|
|
290
|
+
? select.flatMap((_) => typeof _ === "object" && "computed" in _ ? getValues(_.computed.filter) : [])
|
|
291
|
+
: []
|
|
292
|
+
const values = [...computedFilters, ...getValues(filter)]
|
|
293
|
+
|
|
294
|
+
const computedSelectExpr = (key: string, computed: ComputedProjectionIrExpression) => {
|
|
295
|
+
const relationPath = computed.path
|
|
296
|
+
const relationAlias = relationPath
|
|
297
|
+
const relationSource = dottedToAccess(`f.${relationPath}`)
|
|
298
|
+
const where = computed.filter.length > 0
|
|
299
|
+
? ` WHERE ${print(computed.filter, relationPath, false)}`
|
|
300
|
+
: ""
|
|
301
|
+
switch (computed._tag) {
|
|
302
|
+
case "relation-count":
|
|
303
|
+
return `(SELECT VALUE COUNT(1) FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
|
|
304
|
+
case "relation-any":
|
|
305
|
+
return `EXISTS(SELECT VALUE ${relationAlias} FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
|
|
306
|
+
case "relation-every": {
|
|
307
|
+
// ∀x.P(x) ≡ ¬∃x.¬P(x). Cosmos has no NOT(...) on EXISTS subqueries directly,
|
|
308
|
+
// but we can flip via NOT EXISTS(... WHERE NOT (filter)).
|
|
309
|
+
if (computed.filter.length === 0) return `true AS ${key}`
|
|
310
|
+
return `NOT EXISTS(SELECT VALUE ${relationAlias} FROM ${relationAlias} IN ${relationSource} WHERE NOT (${
|
|
311
|
+
print(computed.filter, relationPath, false)
|
|
312
|
+
})) AS ${key}`
|
|
313
|
+
}
|
|
314
|
+
case "relation-distinct-count": {
|
|
315
|
+
const fieldRef = dottedToAccess(`${relationAlias}.${computed.field}`)
|
|
316
|
+
return `(SELECT VALUE COUNT(1) FROM (SELECT DISTINCT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where})) AS ${key}`
|
|
317
|
+
}
|
|
318
|
+
case "relation-sum": {
|
|
319
|
+
const fieldRef = dottedToAccess(`${relationAlias}.${computed.field}`)
|
|
320
|
+
return `(SELECT VALUE SUM(${fieldRef}) FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
|
|
321
|
+
}
|
|
322
|
+
case "relation-collect": {
|
|
323
|
+
const fieldRef = dottedToAccess(`${relationAlias}.${computed.field}`)
|
|
324
|
+
if (computed.distinct) {
|
|
325
|
+
return `ARRAY(SELECT DISTINCT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
|
|
326
|
+
}
|
|
327
|
+
return `ARRAY(SELECT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
276
331
|
// with joins, you should use DISTINCT
|
|
277
332
|
// or you can end up with duplicates
|
|
278
333
|
return {
|
|
@@ -283,6 +338,8 @@ export function buildWhereCosmosQuery3(
|
|
|
283
338
|
.map((s) =>
|
|
284
339
|
typeof s === "string"
|
|
285
340
|
? dottedToAccess(s === idKey ? "f.id" : `f.${s}`) // x["y"} vs x.y, helps with reserved keywords like "value"
|
|
341
|
+
: "computed" in s
|
|
342
|
+
? computedSelectExpr(s.key, s.computed)
|
|
286
343
|
: `ARRAY (SELECT ${s.subKeys.map((_) => dottedToAccess(`t.${_}`)).join(",")}
|
|
287
344
|
FROM t in ${dottedToAccess(`f.${s.key}`)}) AS ${s.key}`
|
|
288
345
|
)
|
|
@@ -291,7 +348,7 @@ export function buildWhereCosmosQuery3(
|
|
|
291
348
|
}
|
|
292
349
|
FROM ${name} f
|
|
293
350
|
|
|
294
|
-
${filter.length ? `WHERE (${print(filter,
|
|
351
|
+
${filter.length ? `WHERE (${print(filter, null, false)})` : ""}
|
|
295
352
|
${order ? `ORDER BY ${order.map((_) => `${dottedToAccess(`f.${_.key}`)} ${_.direction}`).join(", ")}` : ""}
|
|
296
353
|
${skip !== undefined || limit !== undefined ? `OFFSET ${skip ?? 0} LIMIT ${limit ?? 999999}` : ""}`,
|
|
297
354
|
parameters: values
|
package/src/Store/Cosmos.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { CosmosClient, CosmosClientLayer } from "../adapters/cosmos-client.js"
|
|
|
7
7
|
import { OptimisticConcurrencyException } from "../errors.js"
|
|
8
8
|
import { InfraLogger } from "../logger.js"
|
|
9
9
|
import type { FieldValues } from "../Model/filter/types.js"
|
|
10
|
-
import { type RawQuery } from "../Model/query.js"
|
|
10
|
+
import { type ComputedProjectionIrExpression, type RawQuery } from "../Model/query.js"
|
|
11
11
|
import { annotateCosmosResponse, annotateDb } from "../otel.js"
|
|
12
12
|
import { buildWhereCosmosQuery3, logQuery } from "./Cosmos/query.js"
|
|
13
13
|
import { storeId } from "./Memory.js"
|
|
@@ -451,7 +451,15 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
451
451
|
name,
|
|
452
452
|
defaultValues,
|
|
453
453
|
f.select as
|
|
454
|
-
| NonEmptyReadonlyArray<
|
|
454
|
+
| NonEmptyReadonlyArray<
|
|
455
|
+
string | {
|
|
456
|
+
key: string
|
|
457
|
+
subKeys: readonly string[]
|
|
458
|
+
} | {
|
|
459
|
+
key: string
|
|
460
|
+
computed: ComputedProjectionIrExpression
|
|
461
|
+
}
|
|
462
|
+
>
|
|
455
463
|
| undefined,
|
|
456
464
|
f.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
|
|
457
465
|
skip,
|