@reventlessdev/reventless-local 3.0.0-alpha.115 → 3.0.0-alpha.116
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 +19 -0
- package/package.json +6 -6
- package/src/Platform.res +91 -17
- package/src/Platform.res.mjs +89 -30
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +102 -17
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +105 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +113 -39
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +87 -23
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +33 -0
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +44 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +23 -2
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +20 -3
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +132 -24
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +119 -37
- package/src/adapter/LocalBus.res +62 -0
- package/src/adapter/LocalBus.res.mjs +120 -0
- package/src/adapter/ProjectionCheckpoint.res +350 -0
- package/src/adapter/ProjectionCheckpoint.res.mjs +327 -0
- package/src/adapter/ProjectionPending.res +60 -0
- package/src/adapter/ProjectionPending.res.mjs +64 -0
- package/src/adapter/QueryDb/QueryDbListQuery.res +261 -0
- package/src/adapter/QueryDb/QueryDbListQuery.res.mjs +212 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +54 -286
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +28 -190
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +32 -6
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +30 -14
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +163 -11
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +101 -3
- package/src/adapter/SqliteDriver.res +11 -1
- package/src/adapter/SqliteDriver.res.mjs +5 -1
- package/src/components/ReadModel_Builder.res +3 -1
- package/src/components/ReadModel_Builder.res.mjs +13 -4
- package/src/components/StateViewSlice_Builder.res +4 -1
- package/src/components/StateViewSlice_Builder.res.mjs +9 -3
- package/src/test/Mocks/MockEventLogStorage.res +19 -2
- package/src/test/Mocks/MockEventLogStorage.res.mjs +21 -2
- package/tests/PluginEventDecodeTest.res +51 -0
- package/tests/PluginEventDecodeTest.res.mjs +63 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +73 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +86 -0
- package/tests/adapter/DcbEventLogStorageTest.res +46 -0
- package/tests/adapter/DcbEventLogStorageTest.res.mjs +63 -0
- package/tests/adapter/EventLogSnapshotParityTest.res +81 -0
- package/tests/adapter/EventLogSnapshotParityTest.res.mjs +141 -0
- package/tests/adapter/EventLogStorageSqliteTest.res +128 -0
- package/tests/adapter/EventLogStorageSqliteTest.res.mjs +189 -0
- package/tests/adapter/ProjectionCheckpointTest.res +422 -0
- package/tests/adapter/ProjectionCheckpointTest.res.mjs +401 -0
- package/tests/adapter/QueryDbGsiTtlTest.res +78 -0
- package/tests/adapter/QueryDbGsiTtlTest.res.mjs +77 -0
- package/tests/adapter/QueryDbListPushdownParityTest.res +197 -0
- package/tests/adapter/QueryDbListPushdownParityTest.res.mjs +292 -0
- package/tests/adapter/QueryDbListResolverTest.res +76 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +84 -0
- package/tests/components/aggregate/AggregateFixtures.res +4 -0
- package/tests/components/aggregate/AggregateFixtures.res.mjs +1 -0
- package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +4 -4
- package/tests/components/eventlog/EventLogStreamTest.res.mjs +8 -8
- package/tests/plugin/PluginBehavior_GWT.res.mjs +1 -0
|
@@ -25,18 +25,12 @@ type relaySupport = {
|
|
|
25
25
|
@val external btoa: string => string = "btoa"
|
|
26
26
|
@val external atob: string => string = "atob"
|
|
27
27
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// stays in lockstep.
|
|
28
|
+
// Keyset-cursor helpers for the `{name}Items` (sub-id) connection — base64 of
|
|
29
|
+
// the sub-key value. The main connection list resolver's cursor logic now lives
|
|
30
|
+
// in `QueryDbListQuery`; these remain for the items resolver below.
|
|
32
31
|
let encodeCursor = (value: string): string => btoa(value)
|
|
33
32
|
let decodeCursor = (cursor: string): string => atob(cursor)
|
|
34
33
|
|
|
35
|
-
// Default page size for the connection list resolver when neither `first` nor
|
|
36
|
-
// `last` is supplied. Matches the UI's `defaultPageSize` constant; a bound is
|
|
37
|
-
// necessary for the keyset model to report `pageInfo` correctly.
|
|
38
|
-
let defaultListPageSize = 50
|
|
39
|
-
|
|
40
34
|
module Make = (Bus: LocalBus.T) => {
|
|
41
35
|
open ReventlessCore
|
|
42
36
|
|
|
@@ -288,6 +282,19 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
288
282
|
| Some(s) => GraphQL_FragmentGenerator.deriveServerCapability(s)
|
|
289
283
|
| None => GraphQL_FragmentGenerator.emptyCapability
|
|
290
284
|
}
|
|
285
|
+
|
|
286
|
+
// Materialise the whole read model (stream preferred, scan fallback). The
|
|
287
|
+
// fallback path for list queries when no backend push-down is available.
|
|
288
|
+
let fetchAllItems = async (): array<JSON.t> =>
|
|
289
|
+
switch Bus.getQueryDbStream(name) {
|
|
290
|
+
| Some(makeStream) => await makeStream()->Stream.runCollect->Effect.runPromise
|
|
291
|
+
| None =>
|
|
292
|
+
switch Bus.getQueryDbScan(name) {
|
|
293
|
+
| Some(scanAll) => scanAll()
|
|
294
|
+
| None => []
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
291
298
|
let (listSdl, listResolver): (array<string>, GraphQL_ServerInstance.resolverFn) = if connectionSpec {
|
|
292
299
|
// Relay Connection spec format
|
|
293
300
|
let filterTypeName = returnTypeName ++ "Filter"
|
|
@@ -321,265 +328,27 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
321
328
|
},
|
|
322
329
|
})
|
|
323
330
|
| Allow =>
|
|
324
|
-
let items = switch Bus.getQueryDbStream(name) {
|
|
325
|
-
| Some(makeStream) =>
|
|
326
|
-
await makeStream()->Stream.runCollect->Effect.runPromise
|
|
327
|
-
| None =>
|
|
328
|
-
switch Bus.getQueryDbScan(name) {
|
|
329
|
-
| Some(scanAll) => scanAll()
|
|
330
|
-
| None => []
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
// Apply filter (search, searchPrefix, ids) before any pagination.
|
|
334
331
|
let argsDict = args->JSON.Decode.object->Option.getOr(Dict.make())
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
->Option.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
let getId = item =>
|
|
351
|
-
item
|
|
352
|
-
->JSON.Decode.object
|
|
353
|
-
->Option.flatMap(d => d->Dict.get("id"))
|
|
354
|
-
->Option.flatMap(JSON.Decode.string)
|
|
355
|
-
->Option.getOr("")
|
|
356
|
-
// Per-field eq / from / to filters derived from capability — applied
|
|
357
|
-
// alongside the legacy search/searchPrefix/ids block.
|
|
358
|
-
let getFieldString = (item, field) =>
|
|
359
|
-
item
|
|
360
|
-
->JSON.Decode.object
|
|
361
|
-
->Option.flatMap(d => d->Dict.get(field))
|
|
362
|
-
->Option.flatMap(v =>
|
|
363
|
-
switch v->JSON.Decode.string {
|
|
364
|
-
| Some(s) => Some(s)
|
|
365
|
-
| None =>
|
|
366
|
-
v
|
|
367
|
-
->JSON.Decode.float
|
|
368
|
-
->Option.map(f => Float.toString(f))
|
|
369
|
-
}
|
|
370
|
-
)
|
|
371
|
-
let perFieldChecks: array<JSON.t => bool> = capability.filterFields->Array.flatMap(f => {
|
|
372
|
-
let checks: array<JSON.t => bool> = []
|
|
373
|
-
switch filterDict->Dict.get(f.name ++ "Eq") {
|
|
374
|
-
| Some(v) when v != JSON.Encode.null =>
|
|
375
|
-
let expected = switch v->JSON.Decode.string {
|
|
376
|
-
| Some(s) => s
|
|
377
|
-
| None =>
|
|
378
|
-
v->JSON.Decode.float->Option.map(f => Float.toString(f))->Option.getOr("")
|
|
379
|
-
}
|
|
380
|
-
checks->Array.push(item =>
|
|
381
|
-
getFieldString(item, f.name)->Option.mapOr(false, v => v == expected)
|
|
382
|
-
)
|
|
383
|
-
| _ => ()
|
|
332
|
+
// Prefer a backend list push-down (SQLite builds json_extract predicates
|
|
333
|
+
// + ORDER BY … LIMIT so it never materialises the whole read model). When
|
|
334
|
+
// the backend can't serve this query shape it returns None and we fall
|
|
335
|
+
// back to materialising the full model and running the shared
|
|
336
|
+
// `QueryDbListQuery` spec over it (the same code the in-memory backend and
|
|
337
|
+
// the push-down are tested against).
|
|
338
|
+
let decodeLocalId = id =>
|
|
339
|
+
DomainGraphQL_Server.decodeGlobalId(id)->Option.map(((_, lid)) => lid)
|
|
340
|
+
switch Bus.getQueryDbListPage(name) {
|
|
341
|
+
| Some(listPage) =>
|
|
342
|
+
switch listPage(~argsDict, ~capability, ~labelField) {
|
|
343
|
+
| Some(conn) => conn
|
|
344
|
+
| None =>
|
|
345
|
+
let items = await fetchAllItems()
|
|
346
|
+
QueryDbListQuery.run(~items, ~argsDict, ~capability, ~labelField, ~decodeLocalId)
|
|
384
347
|
}
|
|
385
|
-
if f.range {
|
|
386
|
-
switch filterDict->Dict.get(f.name ++ "From") {
|
|
387
|
-
| Some(v) when v != JSON.Encode.null =>
|
|
388
|
-
let from =
|
|
389
|
-
v
|
|
390
|
-
->JSON.Decode.string
|
|
391
|
-
->Option.getOr(
|
|
392
|
-
v->JSON.Decode.float->Option.map(f => Float.toString(f))->Option.getOr(""),
|
|
393
|
-
)
|
|
394
|
-
checks->Array.push(item =>
|
|
395
|
-
getFieldString(item, f.name)->Option.mapOr(false, v => v >= from)
|
|
396
|
-
)
|
|
397
|
-
| _ => ()
|
|
398
|
-
}
|
|
399
|
-
switch filterDict->Dict.get(f.name ++ "To") {
|
|
400
|
-
| Some(v) when v != JSON.Encode.null =>
|
|
401
|
-
let to_ =
|
|
402
|
-
v
|
|
403
|
-
->JSON.Decode.string
|
|
404
|
-
->Option.getOr(
|
|
405
|
-
v->JSON.Decode.float->Option.map(f => Float.toString(f))->Option.getOr(""),
|
|
406
|
-
)
|
|
407
|
-
checks->Array.push(item =>
|
|
408
|
-
getFieldString(item, f.name)->Option.mapOr(false, v => v <= to_)
|
|
409
|
-
)
|
|
410
|
-
| _ => ()
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
checks
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
let filtered = items->Array.filter(item => {
|
|
417
|
-
let passSearch = switch search {
|
|
418
|
-
| Some(s) if s->String.length > 0 =>
|
|
419
|
-
getLabel(item)->String.toLowerCase->String.includes(s->String.toLowerCase)
|
|
420
|
-
| _ => true
|
|
421
|
-
}
|
|
422
|
-
let passPrefix = switch searchPrefix {
|
|
423
|
-
| Some(p) if p->String.length > 0 =>
|
|
424
|
-
getLabel(item)->String.toLowerCase->String.startsWith(p->String.toLowerCase)
|
|
425
|
-
| _ => true
|
|
426
|
-
}
|
|
427
|
-
// Filter `ids` accepts either the Relay-encoded global ID (the
|
|
428
|
-
// form returned by node.id) or the entity's raw local ID — so
|
|
429
|
-
// callers that hold a foreign-key value like `customerId =
|
|
430
|
-
// "cust-1"` can hydrate labels without first encoding to
|
|
431
|
-
// `Ordering_Customer:cust-1` base64.
|
|
432
|
-
let passIds = switch ids {
|
|
433
|
-
| Some(idList) if idList->Array.length > 0 =>
|
|
434
|
-
let itemId = getId(item)
|
|
435
|
-
let itemLocalId =
|
|
436
|
-
DomainGraphQL_Server.decodeGlobalId(itemId)->Option.map(((_, lid)) => lid)
|
|
437
|
-
idList->Array.some(i => i == itemId || itemLocalId == Some(i))
|
|
438
|
-
| _ => true
|
|
439
|
-
}
|
|
440
|
-
let passPerField = perFieldChecks->Array.every(check => check(item))
|
|
441
|
-
passSearch && passPrefix && passIds && passPerField
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
// Apply orderBy when provided. Sorts on the requested field; ties
|
|
445
|
-
// broken by id so keyset-style cursors stay stable across requests
|
|
446
|
-
// that share a sort field. When orderBy is omitted, items are sorted
|
|
447
|
-
// by id ascending so the natural order is deterministic and pagination
|
|
448
|
-
// cursors remain stable.
|
|
449
|
-
let orderByDict =
|
|
450
|
-
argsDict
|
|
451
|
-
->Dict.get("orderBy")
|
|
452
|
-
->Option.flatMap(JSON.Decode.object)
|
|
453
|
-
let orderByField =
|
|
454
|
-
orderByDict
|
|
455
|
-
->Option.flatMap(ob => ob->Dict.get("field"))
|
|
456
|
-
->Option.flatMap(JSON.Decode.string)
|
|
457
|
-
let direction =
|
|
458
|
-
orderByDict
|
|
459
|
-
->Option.flatMap(ob => ob->Dict.get("direction"))
|
|
460
|
-
->Option.flatMap(JSON.Decode.string)
|
|
461
|
-
->Option.getOr("ASC")
|
|
462
|
-
let isDesc = direction == "DESC"
|
|
463
|
-
let sorted = switch orderByField {
|
|
464
|
-
| Some(f) =>
|
|
465
|
-
let cmp = (a, b) => {
|
|
466
|
-
let av = getFieldString(a, f)->Option.getOr("")
|
|
467
|
-
let bv = getFieldString(b, f)->Option.getOr("")
|
|
468
|
-
let primary = if av < bv {
|
|
469
|
-
-1
|
|
470
|
-
} else if av > bv {
|
|
471
|
-
1
|
|
472
|
-
} else {
|
|
473
|
-
0
|
|
474
|
-
}
|
|
475
|
-
let primary = isDesc ? -primary : primary
|
|
476
|
-
if primary != 0 {
|
|
477
|
-
primary
|
|
478
|
-
} else {
|
|
479
|
-
let aid = getId(a)
|
|
480
|
-
let bid = getId(b)
|
|
481
|
-
if aid < bid {
|
|
482
|
-
-1
|
|
483
|
-
} else if aid > bid {
|
|
484
|
-
1
|
|
485
|
-
} else {
|
|
486
|
-
0
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
filtered->Array.toSorted((a, b) => cmp(a, b)->Int.toFloat)
|
|
491
348
|
| None =>
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
let aid = getId(a)
|
|
495
|
-
let bid = getId(b)
|
|
496
|
-
if aid < bid {
|
|
497
|
-
-1.
|
|
498
|
-
} else if aid > bid {
|
|
499
|
-
1.
|
|
500
|
-
} else {
|
|
501
|
-
0.
|
|
502
|
-
}
|
|
503
|
-
})
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Keyset pagination. Cursor encodes the row's value for the active
|
|
507
|
-
// sort field — orderBy.field when supplied, "id" otherwise. Boundary
|
|
508
|
-
// is applied as a value comparison against the sorted array; in DESC
|
|
509
|
-
// sort the comparison flips. Note: when the cursor field has duplicate
|
|
510
|
-
// values, the boundary excludes all rows with the cursor's value, not
|
|
511
|
-
// just the row at the cursor position. Acceptable for in-memory dev;
|
|
512
|
-
// in practice common sort fields (id, createdAt) are unique.
|
|
513
|
-
let first =
|
|
514
|
-
argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
|
|
515
|
-
let after = argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string)
|
|
516
|
-
let last =
|
|
517
|
-
argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
|
|
518
|
-
let before = argsDict->Dict.get("before")->Option.flatMap(JSON.Decode.string)
|
|
519
|
-
let isBackward = last->Option.isSome
|
|
520
|
-
let cursorField = orderByField->Option.getOr("id")
|
|
521
|
-
let getCursorValue = item =>
|
|
522
|
-
getFieldString(item, cursorField)->Option.getOr(getId(item))
|
|
523
|
-
|
|
524
|
-
let cursorBounded = switch (isBackward, after, before) {
|
|
525
|
-
| (false, Some(c), _) =>
|
|
526
|
-
let cv = decodeCursor(c)
|
|
527
|
-
sorted->Array.filter(item => {
|
|
528
|
-
let v = getCursorValue(item)
|
|
529
|
-
isDesc ? v < cv : v > cv
|
|
530
|
-
})
|
|
531
|
-
| (true, _, Some(c)) =>
|
|
532
|
-
let cv = decodeCursor(c)
|
|
533
|
-
sorted->Array.filter(item => {
|
|
534
|
-
let v = getCursorValue(item)
|
|
535
|
-
isDesc ? v > cv : v < cv
|
|
536
|
-
})
|
|
537
|
-
| _ => sorted
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
let pageSize = if isBackward {
|
|
541
|
-
last->Option.getOr(defaultListPageSize)
|
|
542
|
-
} else {
|
|
543
|
-
first->Option.getOr(defaultListPageSize)
|
|
544
|
-
}
|
|
545
|
-
let take = pageSize + 1
|
|
546
|
-
let (pageItems, hasMore) = if isBackward {
|
|
547
|
-
// Take the last `take` items from the cursor-bounded slice. If we
|
|
548
|
-
// grabbed an extra, drop the leading entry (the boundary marker).
|
|
549
|
-
let len = cursorBounded->Array.length
|
|
550
|
-
let startIdx = len > take ? len - take : 0
|
|
551
|
-
let arr = cursorBounded->Array.slice(~start=startIdx, ~end=len)
|
|
552
|
-
let hasMore = arr->Array.length > pageSize
|
|
553
|
-
let result = if hasMore {
|
|
554
|
-
arr->Array.slice(~start=1, ~end=arr->Array.length)
|
|
555
|
-
} else {
|
|
556
|
-
arr
|
|
557
|
-
}
|
|
558
|
-
(result, hasMore)
|
|
559
|
-
} else {
|
|
560
|
-
let arr = cursorBounded->Array.slice(~start=0, ~end=take)
|
|
561
|
-
let hasMore = arr->Array.length > pageSize
|
|
562
|
-
(arr->Array.slice(~start=0, ~end=pageSize), hasMore)
|
|
349
|
+
let items = await fetchAllItems()
|
|
350
|
+
QueryDbListQuery.run(~items, ~argsDict, ~capability, ~labelField, ~decodeLocalId)
|
|
563
351
|
}
|
|
564
|
-
|
|
565
|
-
let edges = pageItems->Array.map(item =>
|
|
566
|
-
Obj.magic({"node": item, "cursor": encodeCursor(getCursorValue(item))})
|
|
567
|
-
)
|
|
568
|
-
let startCursor =
|
|
569
|
-
pageItems->Array.get(0)->Option.map(item => encodeCursor(getCursorValue(item)))
|
|
570
|
-
let endCursor =
|
|
571
|
-
pageItems
|
|
572
|
-
->Array.get(pageItems->Array.length - 1)
|
|
573
|
-
->Option.map(item => encodeCursor(getCursorValue(item)))
|
|
574
|
-
Obj.magic({
|
|
575
|
-
"edges": edges,
|
|
576
|
-
"pageInfo": {
|
|
577
|
-
"hasNextPage": !isBackward && hasMore,
|
|
578
|
-
"hasPreviousPage": isBackward && hasMore,
|
|
579
|
-
"startCursor": startCursor->Nullable.fromOption,
|
|
580
|
-
"endCursor": endCursor->Nullable.fromOption,
|
|
581
|
-
},
|
|
582
|
-
})
|
|
583
352
|
}
|
|
584
353
|
}
|
|
585
354
|
(sdl, resolver)
|
|
@@ -590,15 +359,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
590
359
|
switch await runInterceptor(~ctx, ~args) {
|
|
591
360
|
| Deny(_) => Obj.magic({"nextToken": Nullable.null, "scannedCount": 0, "items": []})
|
|
592
361
|
| Allow =>
|
|
593
|
-
let items =
|
|
594
|
-
| Some(makeStream) =>
|
|
595
|
-
await makeStream()->Stream.runCollect->Effect.runPromise
|
|
596
|
-
| None =>
|
|
597
|
-
switch Bus.getQueryDbScan(name) {
|
|
598
|
-
| Some(scanAll) => scanAll()
|
|
599
|
-
| None => []
|
|
600
|
-
}
|
|
601
|
-
}
|
|
362
|
+
let items = await fetchAllItems()
|
|
602
363
|
Obj.magic({"nextToken": Nullable.null, "scannedCount": items->Array.length, "items": items})
|
|
603
364
|
}
|
|
604
365
|
}
|
|
@@ -748,19 +509,26 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
748
509
|
| Allow =>
|
|
749
510
|
let value =
|
|
750
511
|
args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(index))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
->
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
512
|
+
// Prefer the pushed-down equality lookup (SQLite rides the GSI index;
|
|
513
|
+
// in-memory reuses its lazy snapshot). Fall back to scan+filter only
|
|
514
|
+
// if no lookup is registered for this QueryDb.
|
|
515
|
+
switch Bus.getQueryDbIndexLookup(name) {
|
|
516
|
+
| Some(lookup) => lookup(filterField, value)->JSON.Encode.array
|
|
517
|
+
| None =>
|
|
518
|
+
switch Bus.getQueryDbScan(name) {
|
|
519
|
+
| Some(scanAll) =>
|
|
520
|
+
scanAll()
|
|
521
|
+
->Array.filter(item =>
|
|
522
|
+
item
|
|
523
|
+
->JSON.Decode.object
|
|
524
|
+
->Option.flatMap(d => d->Dict.get(filterField))
|
|
525
|
+
->Option.flatMap(JSON.Decode.string)
|
|
526
|
+
->Option.map(v => v == value)
|
|
527
|
+
->Option.getOr(false)
|
|
528
|
+
)
|
|
529
|
+
->JSON.Encode.array
|
|
530
|
+
| None => []->JSON.Encode.array
|
|
531
|
+
}
|
|
764
532
|
}
|
|
765
533
|
}
|
|
766
534
|
}
|
|
@@ -6,12 +6,12 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
6
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
|
|
9
|
-
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
10
9
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
11
10
|
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
12
11
|
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
13
12
|
import * as SortKey_Filter$ReventlessLocal from "./SortKey_Filter.res.mjs";
|
|
14
13
|
import * as QueryDb_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Callback.res.mjs";
|
|
14
|
+
import * as QueryDbListQuery$ReventlessLocal from "./QueryDbListQuery.res.mjs";
|
|
15
15
|
import * as DomainGraphQL_Server$ReventlessLocal from "../DomainGraphQL_Server.res.mjs";
|
|
16
16
|
import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
17
17
|
|
|
@@ -157,6 +157,18 @@ function Make(Bus) {
|
|
|
157
157
|
let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
|
|
158
158
|
let stateSchemaOpt = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name];
|
|
159
159
|
let capability = stateSchemaOpt !== undefined ? GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(stateSchemaOpt) : GraphQL_FragmentGenerator$ReventlessCore.emptyCapability;
|
|
160
|
+
let fetchAllItems = async () => {
|
|
161
|
+
let makeStream = Bus.getQueryDbStream(name);
|
|
162
|
+
if (makeStream !== undefined) {
|
|
163
|
+
return await Effect.runPromise(Stream.runCollect(makeStream()));
|
|
164
|
+
}
|
|
165
|
+
let scanAll = Bus.getQueryDbScan(name);
|
|
166
|
+
if (scanAll !== undefined) {
|
|
167
|
+
return scanAll();
|
|
168
|
+
} else {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
};
|
|
160
172
|
let match;
|
|
161
173
|
if (connectionSpec) {
|
|
162
174
|
let filterTypeName = returnTypeName + "Filter";
|
|
@@ -178,187 +190,19 @@ function Make(Bus) {
|
|
|
178
190
|
}
|
|
179
191
|
};
|
|
180
192
|
}
|
|
181
|
-
let makeStream = Bus.getQueryDbStream(name);
|
|
182
|
-
let items;
|
|
183
|
-
if (makeStream !== undefined) {
|
|
184
|
-
items = await Effect.runPromise(Stream.runCollect(makeStream()));
|
|
185
|
-
} else {
|
|
186
|
-
let scanAll = Bus.getQueryDbScan(name);
|
|
187
|
-
items = scanAll !== undefined ? scanAll() : [];
|
|
188
|
-
}
|
|
189
193
|
let argsDict = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(args), {});
|
|
190
|
-
let
|
|
191
|
-
let
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
let getFieldString = (item, field) => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]), v => {
|
|
197
|
-
let s = Stdlib_JSON.Decode.string(v);
|
|
198
|
-
if (s !== undefined) {
|
|
199
|
-
return s;
|
|
200
|
-
} else {
|
|
201
|
-
return Stdlib_Option.map(Stdlib_JSON.Decode.float(v), f => f.toString());
|
|
194
|
+
let decodeLocalId = id => Stdlib_Option.map(DomainGraphQL_Server$ReventlessLocal.decodeGlobalId(id), param => param[1]);
|
|
195
|
+
let listPage = Bus.getQueryDbListPage(name);
|
|
196
|
+
if (listPage !== undefined) {
|
|
197
|
+
let conn = listPage(argsDict, capability, labelField);
|
|
198
|
+
if (conn !== undefined) {
|
|
199
|
+
return conn;
|
|
202
200
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
let checks = [];
|
|
206
|
-
let v = filterDict[f.name + "Eq"];
|
|
207
|
-
if (v !== undefined && v !== null) {
|
|
208
|
-
let s = Stdlib_JSON.Decode.string(v);
|
|
209
|
-
let expected = s !== undefined ? s : Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_JSON.Decode.float(v), f => f.toString()), "");
|
|
210
|
-
checks.push(item => Stdlib_Option.mapOr(getFieldString(item, f.name), false, v => v === expected));
|
|
211
|
-
}
|
|
212
|
-
if (f.range) {
|
|
213
|
-
let v$1 = filterDict[f.name + "From"];
|
|
214
|
-
if (v$1 !== undefined && v$1 !== null) {
|
|
215
|
-
let from = Stdlib_Option.getOr(Stdlib_JSON.Decode.string(v$1), Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_JSON.Decode.float(v$1), f => f.toString()), ""));
|
|
216
|
-
checks.push(item => Stdlib_Option.mapOr(getFieldString(item, f.name), false, v => v >= from));
|
|
217
|
-
}
|
|
218
|
-
let v$2 = filterDict[f.name + "To"];
|
|
219
|
-
if (v$2 !== undefined && v$2 !== null) {
|
|
220
|
-
let to_ = Stdlib_Option.getOr(Stdlib_JSON.Decode.string(v$2), Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_JSON.Decode.float(v$2), f => f.toString()), ""));
|
|
221
|
-
checks.push(item => Stdlib_Option.mapOr(getFieldString(item, f.name), false, v => v <= to_));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return checks;
|
|
225
|
-
});
|
|
226
|
-
let filtered = items.filter(item => {
|
|
227
|
-
let passSearch = search !== undefined && search.length > 0 ? getLabel(item).toLowerCase().includes(search.toLowerCase()) : true;
|
|
228
|
-
let passPrefix = searchPrefix !== undefined && searchPrefix.length > 0 ? getLabel(item).toLowerCase().startsWith(searchPrefix.toLowerCase()) : true;
|
|
229
|
-
let passIds;
|
|
230
|
-
if (ids !== undefined && ids.length !== 0) {
|
|
231
|
-
let itemId = getId(item);
|
|
232
|
-
let itemLocalId = Stdlib_Option.map(DomainGraphQL_Server$ReventlessLocal.decodeGlobalId(itemId), param => param[1]);
|
|
233
|
-
passIds = ids.some(i => {
|
|
234
|
-
if (i === itemId) {
|
|
235
|
-
return true;
|
|
236
|
-
} else {
|
|
237
|
-
return Primitive_object.equal(itemLocalId, i);
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
} else {
|
|
241
|
-
passIds = true;
|
|
242
|
-
}
|
|
243
|
-
let passPerField = perFieldChecks.every(check => check(item));
|
|
244
|
-
if (passSearch && passPrefix && passIds) {
|
|
245
|
-
return passPerField;
|
|
246
|
-
} else {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
let orderByDict = Stdlib_Option.flatMap(argsDict["orderBy"], Stdlib_JSON.Decode.object);
|
|
251
|
-
let orderByField = Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["field"]), Stdlib_JSON.Decode.string);
|
|
252
|
-
let direction = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["direction"]), Stdlib_JSON.Decode.string), "ASC");
|
|
253
|
-
let isDesc = direction === "DESC";
|
|
254
|
-
let sorted;
|
|
255
|
-
if (orderByField !== undefined) {
|
|
256
|
-
sorted = filtered.toSorted((a, b) => {
|
|
257
|
-
let av = Stdlib_Option.getOr(getFieldString(a, orderByField), "");
|
|
258
|
-
let bv = Stdlib_Option.getOr(getFieldString(b, orderByField), "");
|
|
259
|
-
let primary = av < bv ? -1 : (
|
|
260
|
-
av > bv ? 1 : 0
|
|
261
|
-
);
|
|
262
|
-
let primary$1 = isDesc ? -primary | 0 : primary;
|
|
263
|
-
if (primary$1 !== 0) {
|
|
264
|
-
return primary$1;
|
|
265
|
-
}
|
|
266
|
-
let aid = getId(a);
|
|
267
|
-
let bid = getId(b);
|
|
268
|
-
if (aid < bid) {
|
|
269
|
-
return -1;
|
|
270
|
-
} else if (aid > bid) {
|
|
271
|
-
return 1;
|
|
272
|
-
} else {
|
|
273
|
-
return 0;
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
} else {
|
|
277
|
-
sorted = filtered.toSorted((a, b) => {
|
|
278
|
-
let aid = getId(a);
|
|
279
|
-
let bid = getId(b);
|
|
280
|
-
if (aid < bid) {
|
|
281
|
-
return -1;
|
|
282
|
-
} else if (aid > bid) {
|
|
283
|
-
return 1;
|
|
284
|
-
} else {
|
|
285
|
-
return 0;
|
|
286
|
-
}
|
|
287
|
-
});
|
|
201
|
+
let items = await fetchAllItems();
|
|
202
|
+
return QueryDbListQuery$ReventlessLocal.run(items, argsDict, capability, labelField, decodeLocalId);
|
|
288
203
|
}
|
|
289
|
-
let
|
|
290
|
-
|
|
291
|
-
let last = Stdlib_Option.map(Stdlib_Option.flatMap(argsDict["last"], Stdlib_JSON.Decode.float), prim => prim | 0);
|
|
292
|
-
let before = Stdlib_Option.flatMap(argsDict["before"], Stdlib_JSON.Decode.string);
|
|
293
|
-
let isBackward = Stdlib_Option.isSome(last);
|
|
294
|
-
let cursorField = Stdlib_Option.getOr(orderByField, "id");
|
|
295
|
-
let getCursorValue = item => Stdlib_Option.getOr(getFieldString(item, cursorField), getId(item));
|
|
296
|
-
let cursorBounded;
|
|
297
|
-
if (isBackward) {
|
|
298
|
-
if (before !== undefined) {
|
|
299
|
-
let cv = atob(before);
|
|
300
|
-
cursorBounded = sorted.filter(item => {
|
|
301
|
-
let v = getCursorValue(item);
|
|
302
|
-
if (isDesc) {
|
|
303
|
-
return v > cv;
|
|
304
|
-
} else {
|
|
305
|
-
return v < cv;
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
} else {
|
|
309
|
-
cursorBounded = sorted;
|
|
310
|
-
}
|
|
311
|
-
} else if (after !== undefined) {
|
|
312
|
-
let cv$1 = atob(after);
|
|
313
|
-
cursorBounded = sorted.filter(item => {
|
|
314
|
-
let v = getCursorValue(item);
|
|
315
|
-
if (isDesc) {
|
|
316
|
-
return v < cv$1;
|
|
317
|
-
} else {
|
|
318
|
-
return v > cv$1;
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
} else {
|
|
322
|
-
cursorBounded = sorted;
|
|
323
|
-
}
|
|
324
|
-
let pageSize = isBackward ? Stdlib_Option.getOr(last, 50) : Stdlib_Option.getOr(first, 50);
|
|
325
|
-
let take = pageSize + 1 | 0;
|
|
326
|
-
let match$1;
|
|
327
|
-
if (isBackward) {
|
|
328
|
-
let len = cursorBounded.length;
|
|
329
|
-
let startIdx = len > take ? len - take | 0 : 0;
|
|
330
|
-
let arr = cursorBounded.slice(startIdx, len);
|
|
331
|
-
let hasMore = arr.length > pageSize;
|
|
332
|
-
let result = hasMore ? arr.slice(1, arr.length) : arr;
|
|
333
|
-
match$1 = [
|
|
334
|
-
result,
|
|
335
|
-
hasMore
|
|
336
|
-
];
|
|
337
|
-
} else {
|
|
338
|
-
let arr$1 = cursorBounded.slice(0, take);
|
|
339
|
-
let hasMore$1 = arr$1.length > pageSize;
|
|
340
|
-
match$1 = [
|
|
341
|
-
arr$1.slice(0, pageSize),
|
|
342
|
-
hasMore$1
|
|
343
|
-
];
|
|
344
|
-
}
|
|
345
|
-
let hasMore$2 = match$1[1];
|
|
346
|
-
let pageItems = match$1[0];
|
|
347
|
-
let edges = pageItems.map(item => ({
|
|
348
|
-
node: item,
|
|
349
|
-
cursor: btoa(getCursorValue(item))
|
|
350
|
-
}));
|
|
351
|
-
let startCursor = Stdlib_Option.map(pageItems[0], item => btoa(getCursorValue(item)));
|
|
352
|
-
let endCursor = Stdlib_Option.map(pageItems[pageItems.length - 1 | 0], item => btoa(getCursorValue(item)));
|
|
353
|
-
return {
|
|
354
|
-
edges: edges,
|
|
355
|
-
pageInfo: {
|
|
356
|
-
hasNextPage: !isBackward && hasMore$2,
|
|
357
|
-
hasPreviousPage: isBackward && hasMore$2,
|
|
358
|
-
startCursor: Stdlib_Nullable.fromOption(startCursor),
|
|
359
|
-
endCursor: Stdlib_Nullable.fromOption(endCursor)
|
|
360
|
-
}
|
|
361
|
-
};
|
|
204
|
+
let items$1 = await fetchAllItems();
|
|
205
|
+
return QueryDbListQuery$ReventlessLocal.run(items$1, argsDict, capability, labelField, decodeLocalId);
|
|
362
206
|
};
|
|
363
207
|
match = [
|
|
364
208
|
sdl,
|
|
@@ -375,14 +219,7 @@ function Make(Bus) {
|
|
|
375
219
|
items: []
|
|
376
220
|
};
|
|
377
221
|
}
|
|
378
|
-
let
|
|
379
|
-
let items;
|
|
380
|
-
if (makeStream !== undefined) {
|
|
381
|
-
items = await Effect.runPromise(Stream.runCollect(makeStream()));
|
|
382
|
-
} else {
|
|
383
|
-
let scanAll = Bus.getQueryDbScan(name);
|
|
384
|
-
items = scanAll !== undefined ? scanAll() : [];
|
|
385
|
-
}
|
|
222
|
+
let items = await fetchAllItems();
|
|
386
223
|
return {
|
|
387
224
|
nextToken: null,
|
|
388
225
|
scannedCount: items.length,
|
|
@@ -517,6 +354,10 @@ function Make(Bus) {
|
|
|
517
354
|
return [];
|
|
518
355
|
}
|
|
519
356
|
let value = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[index]), Stdlib_JSON.Decode.string), "");
|
|
357
|
+
let lookup = Bus.getQueryDbIndexLookup(name);
|
|
358
|
+
if (lookup !== undefined) {
|
|
359
|
+
return lookup(filterField, value);
|
|
360
|
+
}
|
|
520
361
|
let scanAll = Bus.getQueryDbScan(name);
|
|
521
362
|
if (scanAll !== undefined) {
|
|
522
363
|
return scanAll().filter(item => Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[filterField]), Stdlib_JSON.Decode.string), v => v === value), false));
|
|
@@ -558,12 +399,9 @@ function Make(Bus) {
|
|
|
558
399
|
};
|
|
559
400
|
}
|
|
560
401
|
|
|
561
|
-
let defaultListPageSize = 50;
|
|
562
|
-
|
|
563
402
|
export {
|
|
564
403
|
encodeCursor,
|
|
565
404
|
decodeCursor,
|
|
566
|
-
defaultListPageSize,
|
|
567
405
|
Make,
|
|
568
406
|
}
|
|
569
407
|
/* Stream Not a pure module */
|