@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +6 -6
  3. package/src/Platform.res +91 -17
  4. package/src/Platform.res.mjs +89 -30
  5. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +102 -17
  6. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +105 -22
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +113 -39
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +87 -23
  9. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +33 -0
  10. package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +44 -0
  11. package/src/adapter/EventLog/EventLogStorage_InMemory.res +23 -2
  12. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +20 -3
  13. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +132 -24
  14. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +119 -37
  15. package/src/adapter/LocalBus.res +62 -0
  16. package/src/adapter/LocalBus.res.mjs +120 -0
  17. package/src/adapter/ProjectionCheckpoint.res +350 -0
  18. package/src/adapter/ProjectionCheckpoint.res.mjs +327 -0
  19. package/src/adapter/ProjectionPending.res +60 -0
  20. package/src/adapter/ProjectionPending.res.mjs +64 -0
  21. package/src/adapter/QueryDb/QueryDbListQuery.res +261 -0
  22. package/src/adapter/QueryDb/QueryDbListQuery.res.mjs +212 -0
  23. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +54 -286
  24. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +28 -190
  25. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +32 -6
  26. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +30 -14
  27. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +163 -11
  28. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +101 -3
  29. package/src/adapter/SqliteDriver.res +11 -1
  30. package/src/adapter/SqliteDriver.res.mjs +5 -1
  31. package/src/components/ReadModel_Builder.res +3 -1
  32. package/src/components/ReadModel_Builder.res.mjs +13 -4
  33. package/src/components/StateViewSlice_Builder.res +4 -1
  34. package/src/components/StateViewSlice_Builder.res.mjs +9 -3
  35. package/src/test/Mocks/MockEventLogStorage.res +19 -2
  36. package/src/test/Mocks/MockEventLogStorage.res.mjs +21 -2
  37. package/tests/PluginEventDecodeTest.res +51 -0
  38. package/tests/PluginEventDecodeTest.res.mjs +63 -0
  39. package/tests/adapter/DcbEventLogStorageSqliteTest.res +73 -0
  40. package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +86 -0
  41. package/tests/adapter/DcbEventLogStorageTest.res +46 -0
  42. package/tests/adapter/DcbEventLogStorageTest.res.mjs +63 -0
  43. package/tests/adapter/EventLogSnapshotParityTest.res +81 -0
  44. package/tests/adapter/EventLogSnapshotParityTest.res.mjs +141 -0
  45. package/tests/adapter/EventLogStorageSqliteTest.res +128 -0
  46. package/tests/adapter/EventLogStorageSqliteTest.res.mjs +189 -0
  47. package/tests/adapter/ProjectionCheckpointTest.res +422 -0
  48. package/tests/adapter/ProjectionCheckpointTest.res.mjs +401 -0
  49. package/tests/adapter/QueryDbGsiTtlTest.res +78 -0
  50. package/tests/adapter/QueryDbGsiTtlTest.res.mjs +77 -0
  51. package/tests/adapter/QueryDbListPushdownParityTest.res +197 -0
  52. package/tests/adapter/QueryDbListPushdownParityTest.res.mjs +292 -0
  53. package/tests/adapter/QueryDbListResolverTest.res +76 -0
  54. package/tests/adapter/QueryDbListResolverTest.res.mjs +84 -0
  55. package/tests/components/aggregate/AggregateFixtures.res +4 -0
  56. package/tests/components/aggregate/AggregateFixtures.res.mjs +1 -0
  57. package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +4 -4
  58. package/tests/components/eventlog/EventLogStreamTest.res.mjs +8 -8
  59. package/tests/plugin/PluginBehavior_GWT.res.mjs +1 -0
@@ -0,0 +1,60 @@
1
+ // In-memory "appended but not yet fully published" tracker backing the SQLite
2
+ // projection checkpoint (see ProjectionCheckpoint.res).
3
+ //
4
+ // The storage adapters register each appended batch here (msgId → global
5
+ // position) the moment the insert transaction commits; Platform's afterPublish
6
+ // hook resolves the batch once `Bus.publishEvent` has returned — i.e. after
7
+ // every subscriber (including all projections) has processed the events. The
8
+ // gap between the two is exactly the crash window a checkpoint must not
9
+ // advance over: `minPending(axis) - 1` is the highest position provably fully
10
+ // projected on that axis.
11
+ //
12
+ // Two independent position axes exist, because the two persisted logs have
13
+ // unrelated global sequences (each uses its own table's rowid):
14
+ // Aggregate — event_log (aggregate EventLogs → ReadModel projections)
15
+ // Dcb — dcb_event (DCB slice events → StateViewSlice projections)
16
+ // msgIds are globally unique, so resolution needs no axis; each entry just
17
+ // remembers which axis its position belongs to.
18
+ //
19
+ // Tracking is off by default so bare storage construction (tests, tools) never
20
+ // accumulates entries nobody resolves; Platform enables it for the SQLite
21
+ // backend only. State is module-level — one platform per process, same as the
22
+ // LocalBus registries.
23
+
24
+ type axis =
25
+ | Aggregate
26
+ | Dcb
27
+
28
+ let enabled = ref(false)
29
+ let pendingByMsgId: dict<(axis, int)> = Dict.make()
30
+
31
+ let enableTracking = () => enabled := true
32
+
33
+ let trackAppended = (~axis: axis, entries: array<(string, int)>) =>
34
+ if enabled.contents {
35
+ entries->Array.forEach(((msgId, position)) =>
36
+ pendingByMsgId->Dict.set(msgId, (axis, position))
37
+ )
38
+ }
39
+
40
+ let resolve = (msgIds: array<string>) =>
41
+ msgIds->Array.forEach(msgId => pendingByMsgId->Dict.delete(msgId))
42
+
43
+ // Lowest still-pending position on the given axis, or None when every tracked
44
+ // append on that axis has completed its publish cycle.
45
+ let minPending = (axis: axis): option<int> => {
46
+ let positions =
47
+ pendingByMsgId
48
+ ->Dict.valuesToArray
49
+ ->Array.filterMap(((a, p)) => a == axis ? Some(p) : None)
50
+ switch positions->Array.get(0) {
51
+ | None => None
52
+ | Some(first) => Some(positions->Array.reduce(first, (a, b) => a < b ? a : b))
53
+ }
54
+ }
55
+
56
+ // Test isolation: clears state AND disables tracking (the default).
57
+ let reset = () => {
58
+ enabled := false
59
+ pendingByMsgId->Dict.keysToArray->Array.forEach(k => pendingByMsgId->Dict.delete(k))
60
+ }
@@ -0,0 +1,64 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+
6
+ let enabled = {
7
+ contents: false
8
+ };
9
+
10
+ let pendingByMsgId = {};
11
+
12
+ function enableTracking() {
13
+ enabled.contents = true;
14
+ }
15
+
16
+ function trackAppended(axis, entries) {
17
+ if (enabled.contents) {
18
+ entries.forEach(param => {
19
+ pendingByMsgId[param[0]] = [
20
+ axis,
21
+ param[1]
22
+ ];
23
+ });
24
+ return;
25
+ }
26
+ }
27
+
28
+ function resolve(msgIds) {
29
+ msgIds.forEach(msgId => Stdlib_Dict.$$delete(pendingByMsgId, msgId));
30
+ }
31
+
32
+ function minPending(axis) {
33
+ let positions = Stdlib_Array.filterMap(Object.values(pendingByMsgId), param => {
34
+ if (param[0] === axis) {
35
+ return param[1];
36
+ }
37
+ });
38
+ let first = positions[0];
39
+ if (first !== undefined) {
40
+ return Stdlib_Array.reduce(positions, first, (a, b) => {
41
+ if (a < b) {
42
+ return a;
43
+ } else {
44
+ return b;
45
+ }
46
+ });
47
+ }
48
+ }
49
+
50
+ function reset() {
51
+ enabled.contents = false;
52
+ Object.keys(pendingByMsgId).forEach(k => Stdlib_Dict.$$delete(pendingByMsgId, k));
53
+ }
54
+
55
+ export {
56
+ enabled,
57
+ pendingByMsgId,
58
+ enableTracking,
59
+ trackAppended,
60
+ resolve,
61
+ minPending,
62
+ reset,
63
+ }
64
+ /* No side effect */
@@ -0,0 +1,261 @@
1
+ // The connection-list filter / sort / keyset-paginate logic, extracted verbatim
2
+ // from QueryDbResolvers_GraphQL so there is ONE source of truth for the
3
+ // semantics. The resolver calls `run` over the materialised items (in-memory
4
+ // path); the SQLite backend's list push-down must reproduce these exact
5
+ // semantics, and the parity harness asserts it does. Keeping this pure (no
6
+ // server / storage dependencies — the one server helper it needs, global-id
7
+ // decoding for the `ids` filter, is passed in) is what lets it be the shared
8
+ // spec both paths are tested against.
9
+
10
+ open ReventlessCore
11
+
12
+ // base64 cursor of the active sort field's value. Shared with the resolver's
13
+ // items (sub-id) connection so cursor encoding stays in lockstep.
14
+ @val external btoa: string => string = "btoa"
15
+ @val external atob: string => string = "atob"
16
+ let encodeCursor = (value: string): string => btoa(value)
17
+ let decodeCursor = (cursor: string): string => atob(cursor)
18
+
19
+ // Default page size when neither `first` nor `last` is supplied — a bound is
20
+ // required for keyset `pageInfo` to be reported correctly.
21
+ let defaultListPageSize = 50
22
+
23
+ // The effective id of an item (its "id" field). Shared with the SQLite
24
+ // push-down so both compute the tiebreak / default-cursor value identically.
25
+ let getId = (item: JSON.t): string =>
26
+ item
27
+ ->JSON.Decode.object
28
+ ->Option.flatMap(d => d->Dict.get("id"))
29
+ ->Option.flatMap(JSON.Decode.string)
30
+ ->Option.getOr("")
31
+
32
+ // String form of a field for comparison / cursor purposes: the string itself,
33
+ // or a number rendered via Float.toString, else None. The push-down must render
34
+ // the same string in SQL (CAST … AS TEXT) for cursor/order parity.
35
+ let getFieldString = (item: JSON.t, field: string): option<string> =>
36
+ item
37
+ ->JSON.Decode.object
38
+ ->Option.flatMap(d => d->Dict.get(field))
39
+ ->Option.flatMap(v =>
40
+ switch v->JSON.Decode.string {
41
+ | Some(s) => Some(s)
42
+ | None => v->JSON.Decode.float->Option.map(f => Float.toString(f))
43
+ }
44
+ )
45
+
46
+ // Build the Relay connection object from an already-ordered page. Shared by
47
+ // `run` and the SQLite push-down so edges / cursors / pageInfo have one shape.
48
+ let buildConnection = (
49
+ ~pageItems: array<JSON.t>,
50
+ ~hasNextPage: bool,
51
+ ~hasPreviousPage: bool,
52
+ ~cursorValueOf: JSON.t => string,
53
+ ): JSON.t => {
54
+ let edges =
55
+ pageItems->Array.map(item => Obj.magic({"node": item, "cursor": encodeCursor(cursorValueOf(item))}))
56
+ let startCursor = pageItems->Array.get(0)->Option.map(item => encodeCursor(cursorValueOf(item)))
57
+ let endCursor =
58
+ pageItems->Array.get(pageItems->Array.length - 1)->Option.map(item => encodeCursor(cursorValueOf(item)))
59
+ Obj.magic({
60
+ "edges": edges,
61
+ "pageInfo": {
62
+ "hasNextPage": hasNextPage,
63
+ "hasPreviousPage": hasPreviousPage,
64
+ "startCursor": startCursor->Nullable.fromOption,
65
+ "endCursor": endCursor->Nullable.fromOption,
66
+ },
67
+ })
68
+ }
69
+
70
+ // Compute the Relay connection response (edges + pageInfo) for `items` under the
71
+ // GraphQL args. `decodeLocalId` maps a possibly-Relay-encoded id to its local id
72
+ // (the resolver passes DomainGraphQL_Server.decodeGlobalId); everything else is
73
+ // pure over the item array.
74
+ let run = (
75
+ ~items: array<JSON.t>,
76
+ ~argsDict: Dict.t<JSON.t>,
77
+ ~capability: GraphQL_FragmentGenerator.serverCapability,
78
+ ~labelField: string,
79
+ ~decodeLocalId: string => option<string>,
80
+ ): JSON.t => {
81
+ let filterDict =
82
+ argsDict->Dict.get("filter")->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
83
+ let search = filterDict->Dict.get("search")->Option.flatMap(JSON.Decode.string)
84
+ let searchPrefix = filterDict->Dict.get("searchPrefix")->Option.flatMap(JSON.Decode.string)
85
+ let ids =
86
+ filterDict
87
+ ->Dict.get("ids")
88
+ ->Option.flatMap(JSON.Decode.array)
89
+ ->Option.map(arr => arr->Array.filterMap(JSON.Decode.string))
90
+ let getLabel = item =>
91
+ item
92
+ ->JSON.Decode.object
93
+ ->Option.flatMap(d => d->Dict.get(labelField))
94
+ ->Option.flatMap(JSON.Decode.string)
95
+ ->Option.getOr("")
96
+ // Per-field eq / from / to filters derived from capability — applied alongside
97
+ // the legacy search/searchPrefix/ids block.
98
+ let perFieldChecks: array<JSON.t => bool> = capability.filterFields->Array.flatMap(f => {
99
+ let checks: array<JSON.t => bool> = []
100
+ switch filterDict->Dict.get(f.name ++ "Eq") {
101
+ | Some(v) when v != JSON.Encode.null =>
102
+ let expected = switch v->JSON.Decode.string {
103
+ | Some(s) => s
104
+ | None => v->JSON.Decode.float->Option.map(f => Float.toString(f))->Option.getOr("")
105
+ }
106
+ checks->Array.push(item =>
107
+ getFieldString(item, f.name)->Option.mapOr(false, v => v == expected)
108
+ )
109
+ | _ => ()
110
+ }
111
+ if f.range {
112
+ switch filterDict->Dict.get(f.name ++ "From") {
113
+ | Some(v) when v != JSON.Encode.null =>
114
+ let from =
115
+ v
116
+ ->JSON.Decode.string
117
+ ->Option.getOr(v->JSON.Decode.float->Option.map(f => Float.toString(f))->Option.getOr(""))
118
+ checks->Array.push(item => getFieldString(item, f.name)->Option.mapOr(false, v => v >= from))
119
+ | _ => ()
120
+ }
121
+ switch filterDict->Dict.get(f.name ++ "To") {
122
+ | Some(v) when v != JSON.Encode.null =>
123
+ let to_ =
124
+ v
125
+ ->JSON.Decode.string
126
+ ->Option.getOr(v->JSON.Decode.float->Option.map(f => Float.toString(f))->Option.getOr(""))
127
+ checks->Array.push(item => getFieldString(item, f.name)->Option.mapOr(false, v => v <= to_))
128
+ | _ => ()
129
+ }
130
+ }
131
+ checks
132
+ })
133
+
134
+ let filtered = items->Array.filter(item => {
135
+ let passSearch = switch search {
136
+ | Some(s) if s->String.length > 0 =>
137
+ getLabel(item)->String.toLowerCase->String.includes(s->String.toLowerCase)
138
+ | _ => true
139
+ }
140
+ let passPrefix = switch searchPrefix {
141
+ | Some(p) if p->String.length > 0 =>
142
+ getLabel(item)->String.toLowerCase->String.startsWith(p->String.toLowerCase)
143
+ | _ => true
144
+ }
145
+ let passIds = switch ids {
146
+ | Some(idList) if idList->Array.length > 0 =>
147
+ let itemId = getId(item)
148
+ let itemLocalId = decodeLocalId(itemId)
149
+ idList->Array.some(i => i == itemId || itemLocalId == Some(i))
150
+ | _ => true
151
+ }
152
+ let passPerField = perFieldChecks->Array.every(check => check(item))
153
+ passSearch && passPrefix && passIds && passPerField
154
+ })
155
+
156
+ let orderByDict = argsDict->Dict.get("orderBy")->Option.flatMap(JSON.Decode.object)
157
+ let orderByField =
158
+ orderByDict->Option.flatMap(ob => ob->Dict.get("field"))->Option.flatMap(JSON.Decode.string)
159
+ let direction =
160
+ orderByDict
161
+ ->Option.flatMap(ob => ob->Dict.get("direction"))
162
+ ->Option.flatMap(JSON.Decode.string)
163
+ ->Option.getOr("ASC")
164
+ let isDesc = direction == "DESC"
165
+ let sorted = switch orderByField {
166
+ | Some(f) =>
167
+ let cmp = (a, b) => {
168
+ let av = getFieldString(a, f)->Option.getOr("")
169
+ let bv = getFieldString(b, f)->Option.getOr("")
170
+ let primary = if av < bv {
171
+ -1
172
+ } else if av > bv {
173
+ 1
174
+ } else {
175
+ 0
176
+ }
177
+ let primary = isDesc ? -primary : primary
178
+ if primary != 0 {
179
+ primary
180
+ } else {
181
+ let aid = getId(a)
182
+ let bid = getId(b)
183
+ if aid < bid {
184
+ -1
185
+ } else if aid > bid {
186
+ 1
187
+ } else {
188
+ 0
189
+ }
190
+ }
191
+ }
192
+ filtered->Array.toSorted((a, b) => cmp(a, b)->Int.toFloat)
193
+ | None =>
194
+ filtered->Array.toSorted((a, b) => {
195
+ let aid = getId(a)
196
+ let bid = getId(b)
197
+ if aid < bid {
198
+ -1.
199
+ } else if aid > bid {
200
+ 1.
201
+ } else {
202
+ 0.
203
+ }
204
+ })
205
+ }
206
+
207
+ let first =
208
+ argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
209
+ let after = argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string)
210
+ let last = argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
211
+ let before = argsDict->Dict.get("before")->Option.flatMap(JSON.Decode.string)
212
+ let isBackward = last->Option.isSome
213
+ let cursorField = orderByField->Option.getOr("id")
214
+ let getCursorValue = item => getFieldString(item, cursorField)->Option.getOr(getId(item))
215
+
216
+ let cursorBounded = switch (isBackward, after, before) {
217
+ | (false, Some(c), _) =>
218
+ let cv = decodeCursor(c)
219
+ sorted->Array.filter(item => {
220
+ let v = getCursorValue(item)
221
+ isDesc ? v < cv : v > cv
222
+ })
223
+ | (true, _, Some(c)) =>
224
+ let cv = decodeCursor(c)
225
+ sorted->Array.filter(item => {
226
+ let v = getCursorValue(item)
227
+ isDesc ? v > cv : v < cv
228
+ })
229
+ | _ => sorted
230
+ }
231
+
232
+ let pageSize = if isBackward {
233
+ last->Option.getOr(defaultListPageSize)
234
+ } else {
235
+ first->Option.getOr(defaultListPageSize)
236
+ }
237
+ let take = pageSize + 1
238
+ let (pageItems, hasMore) = if isBackward {
239
+ let len = cursorBounded->Array.length
240
+ let startIdx = len > take ? len - take : 0
241
+ let arr = cursorBounded->Array.slice(~start=startIdx, ~end=len)
242
+ let hasMore = arr->Array.length > pageSize
243
+ let result = if hasMore {
244
+ arr->Array.slice(~start=1, ~end=arr->Array.length)
245
+ } else {
246
+ arr
247
+ }
248
+ (result, hasMore)
249
+ } else {
250
+ let arr = cursorBounded->Array.slice(~start=0, ~end=take)
251
+ let hasMore = arr->Array.length > pageSize
252
+ (arr->Array.slice(~start=0, ~end=pageSize), hasMore)
253
+ }
254
+
255
+ buildConnection(
256
+ ~pageItems,
257
+ ~hasNextPage=!isBackward && hasMore,
258
+ ~hasPreviousPage=isBackward && hasMore,
259
+ ~cursorValueOf=getCursorValue,
260
+ )
261
+ }
@@ -0,0 +1,212 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
7
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
+
9
+ function encodeCursor(value) {
10
+ return btoa(value);
11
+ }
12
+
13
+ function decodeCursor(cursor) {
14
+ return atob(cursor);
15
+ }
16
+
17
+ function getId(item) {
18
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d["id"]), Stdlib_JSON.Decode.string), "");
19
+ }
20
+
21
+ function getFieldString(item, field) {
22
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]), v => {
23
+ let s = Stdlib_JSON.Decode.string(v);
24
+ if (s !== undefined) {
25
+ return s;
26
+ } else {
27
+ return Stdlib_Option.map(Stdlib_JSON.Decode.float(v), f => f.toString());
28
+ }
29
+ });
30
+ }
31
+
32
+ function buildConnection(pageItems, hasNextPage, hasPreviousPage, cursorValueOf) {
33
+ let edges = pageItems.map(item => ({
34
+ node: item,
35
+ cursor: btoa(cursorValueOf(item))
36
+ }));
37
+ let startCursor = Stdlib_Option.map(pageItems[0], item => btoa(cursorValueOf(item)));
38
+ let endCursor = Stdlib_Option.map(pageItems[pageItems.length - 1 | 0], item => btoa(cursorValueOf(item)));
39
+ return {
40
+ edges: edges,
41
+ pageInfo: {
42
+ hasNextPage: hasNextPage,
43
+ hasPreviousPage: hasPreviousPage,
44
+ startCursor: Stdlib_Nullable.fromOption(startCursor),
45
+ endCursor: Stdlib_Nullable.fromOption(endCursor)
46
+ }
47
+ };
48
+ }
49
+
50
+ function run(items, argsDict, capability, labelField, decodeLocalId) {
51
+ let filterDict = Stdlib_Option.getOr(Stdlib_Option.flatMap(argsDict["filter"], Stdlib_JSON.Decode.object), {});
52
+ let search = Stdlib_Option.flatMap(filterDict["search"], Stdlib_JSON.Decode.string);
53
+ let searchPrefix = Stdlib_Option.flatMap(filterDict["searchPrefix"], Stdlib_JSON.Decode.string);
54
+ let ids = Stdlib_Option.map(Stdlib_Option.flatMap(filterDict["ids"], Stdlib_JSON.Decode.array), arr => Stdlib_Array.filterMap(arr, Stdlib_JSON.Decode.string));
55
+ let getLabel = item => Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[labelField]), Stdlib_JSON.Decode.string), "");
56
+ let perFieldChecks = capability.filterFields.flatMap(f => {
57
+ let checks = [];
58
+ let v = filterDict[f.name + "Eq"];
59
+ if (v !== undefined && v !== null) {
60
+ let s = Stdlib_JSON.Decode.string(v);
61
+ let expected = s !== undefined ? s : Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_JSON.Decode.float(v), f => f.toString()), "");
62
+ checks.push(item => Stdlib_Option.mapOr(getFieldString(item, f.name), false, v => v === expected));
63
+ }
64
+ if (f.range) {
65
+ let v$1 = filterDict[f.name + "From"];
66
+ if (v$1 !== undefined && v$1 !== null) {
67
+ 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()), ""));
68
+ checks.push(item => Stdlib_Option.mapOr(getFieldString(item, f.name), false, v => v >= from));
69
+ }
70
+ let v$2 = filterDict[f.name + "To"];
71
+ if (v$2 !== undefined && v$2 !== null) {
72
+ 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()), ""));
73
+ checks.push(item => Stdlib_Option.mapOr(getFieldString(item, f.name), false, v => v <= to_));
74
+ }
75
+ }
76
+ return checks;
77
+ });
78
+ let filtered = items.filter(item => {
79
+ let passSearch = search !== undefined && search.length > 0 ? getLabel(item).toLowerCase().includes(search.toLowerCase()) : true;
80
+ let passPrefix = searchPrefix !== undefined && searchPrefix.length > 0 ? getLabel(item).toLowerCase().startsWith(searchPrefix.toLowerCase()) : true;
81
+ let passIds;
82
+ if (ids !== undefined && ids.length !== 0) {
83
+ let itemId = getId(item);
84
+ let itemLocalId = decodeLocalId(itemId);
85
+ passIds = ids.some(i => {
86
+ if (i === itemId) {
87
+ return true;
88
+ } else {
89
+ return Primitive_object.equal(itemLocalId, i);
90
+ }
91
+ });
92
+ } else {
93
+ passIds = true;
94
+ }
95
+ let passPerField = perFieldChecks.every(check => check(item));
96
+ if (passSearch && passPrefix && passIds) {
97
+ return passPerField;
98
+ } else {
99
+ return false;
100
+ }
101
+ });
102
+ let orderByDict = Stdlib_Option.flatMap(argsDict["orderBy"], Stdlib_JSON.Decode.object);
103
+ let orderByField = Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["field"]), Stdlib_JSON.Decode.string);
104
+ let direction = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["direction"]), Stdlib_JSON.Decode.string), "ASC");
105
+ let isDesc = direction === "DESC";
106
+ let sorted;
107
+ if (orderByField !== undefined) {
108
+ sorted = filtered.toSorted((a, b) => {
109
+ let av = Stdlib_Option.getOr(getFieldString(a, orderByField), "");
110
+ let bv = Stdlib_Option.getOr(getFieldString(b, orderByField), "");
111
+ let primary = av < bv ? -1 : (
112
+ av > bv ? 1 : 0
113
+ );
114
+ let primary$1 = isDesc ? -primary | 0 : primary;
115
+ if (primary$1 !== 0) {
116
+ return primary$1;
117
+ }
118
+ let aid = getId(a);
119
+ let bid = getId(b);
120
+ if (aid < bid) {
121
+ return -1;
122
+ } else if (aid > bid) {
123
+ return 1;
124
+ } else {
125
+ return 0;
126
+ }
127
+ });
128
+ } else {
129
+ sorted = filtered.toSorted((a, b) => {
130
+ let aid = getId(a);
131
+ let bid = getId(b);
132
+ if (aid < bid) {
133
+ return -1;
134
+ } else if (aid > bid) {
135
+ return 1;
136
+ } else {
137
+ return 0;
138
+ }
139
+ });
140
+ }
141
+ let first = Stdlib_Option.map(Stdlib_Option.flatMap(argsDict["first"], Stdlib_JSON.Decode.float), prim => prim | 0);
142
+ let after = Stdlib_Option.flatMap(argsDict["after"], Stdlib_JSON.Decode.string);
143
+ let last = Stdlib_Option.map(Stdlib_Option.flatMap(argsDict["last"], Stdlib_JSON.Decode.float), prim => prim | 0);
144
+ let before = Stdlib_Option.flatMap(argsDict["before"], Stdlib_JSON.Decode.string);
145
+ let isBackward = Stdlib_Option.isSome(last);
146
+ let cursorField = Stdlib_Option.getOr(orderByField, "id");
147
+ let getCursorValue = item => Stdlib_Option.getOr(getFieldString(item, cursorField), getId(item));
148
+ let cursorBounded;
149
+ if (isBackward) {
150
+ if (before !== undefined) {
151
+ let cv = atob(before);
152
+ cursorBounded = sorted.filter(item => {
153
+ let v = getCursorValue(item);
154
+ if (isDesc) {
155
+ return v > cv;
156
+ } else {
157
+ return v < cv;
158
+ }
159
+ });
160
+ } else {
161
+ cursorBounded = sorted;
162
+ }
163
+ } else if (after !== undefined) {
164
+ let cv$1 = atob(after);
165
+ cursorBounded = sorted.filter(item => {
166
+ let v = getCursorValue(item);
167
+ if (isDesc) {
168
+ return v < cv$1;
169
+ } else {
170
+ return v > cv$1;
171
+ }
172
+ });
173
+ } else {
174
+ cursorBounded = sorted;
175
+ }
176
+ let pageSize = isBackward ? Stdlib_Option.getOr(last, 50) : Stdlib_Option.getOr(first, 50);
177
+ let take = pageSize + 1 | 0;
178
+ let match;
179
+ if (isBackward) {
180
+ let len = cursorBounded.length;
181
+ let startIdx = len > take ? len - take | 0 : 0;
182
+ let arr = cursorBounded.slice(startIdx, len);
183
+ let hasMore = arr.length > pageSize;
184
+ let result = hasMore ? arr.slice(1, arr.length) : arr;
185
+ match = [
186
+ result,
187
+ hasMore
188
+ ];
189
+ } else {
190
+ let arr$1 = cursorBounded.slice(0, take);
191
+ let hasMore$1 = arr$1.length > pageSize;
192
+ match = [
193
+ arr$1.slice(0, pageSize),
194
+ hasMore$1
195
+ ];
196
+ }
197
+ let hasMore$2 = match[1];
198
+ return buildConnection(match[0], !isBackward && hasMore$2, isBackward && hasMore$2, getCursorValue);
199
+ }
200
+
201
+ let defaultListPageSize = 50;
202
+
203
+ export {
204
+ encodeCursor,
205
+ decodeCursor,
206
+ defaultListPageSize,
207
+ getId,
208
+ getFieldString,
209
+ buildConnection,
210
+ run,
211
+ }
212
+ /* No side effect */