@reventlessdev/reventless-local 3.0.0-alpha.224 → 3.0.0-alpha.226
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 +22 -0
- package/package.json +11 -11
- package/src/Platform.res +1 -1
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +1 -1
- package/src/adapter/LocalBus.res +3 -0
- package/src/adapter/LocalStateChangeDescriptor.res +46 -2
- package/src/adapter/LocalStateChangeDescriptor.res.mjs +16 -4
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +1 -1
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +67 -6
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +45 -8
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +2 -0
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +2 -2
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +37 -0
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +14 -3
- package/src/reset/LocalSeedReset.res +23 -6
- package/src/reset/LocalSeedReset.res.mjs +2 -19
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +1 -1
- package/tests/adapter/DcbEventLogStorageTest.res +1 -1
- package/tests/adapter/EventLogSnapshotParityTest.res +1 -1
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +7 -4
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +10 -7
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res.mjs +2 -2
- package/tests/adapter/QueryDbListPushdownParityTest.res +82 -10
- package/tests/adapter/QueryDbListPushdownParityTest.res.mjs +96 -26
- package/tests/adapter/QueryDbListResolverTest.res +2 -1
- package/tests/adapter/QueryDbListResolverTest.res.mjs +3 -2
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.226 (2026-08-16)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.225 (2026-08-16)
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **seed:** let seed:reset take the yes it asks for ([1f646f6](https://github.com/ReventlessDev/reventless-core/commit/1f646f6ce13a4238e92b7047ee31650ccc4de4f3))
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **core:** exclude retired rows from reads a caller may not widen ([662f31a](https://github.com/ReventlessDev/reventless-core/commit/662f31abb717bda5154199d349da6dcf8e2d3e78))
|
|
22
|
+
* **core:** let [@retired](https://github.com/retired) name a lifecycle state, not only a boolean ([6bb346b](https://github.com/ReventlessDev/reventless-core/commit/6bb346b4f6a5f33826fc24537953482a76067177))
|
|
23
|
+
* **core:** mark the state that retires a row, and allow more than one ([cb1461f](https://github.com/ReventlessDev/reventless-core/commit/cb1461f024d3ca3b53fd9c8b010a054e3fcc4555))
|
|
24
|
+
* **core:** withhold a retired row's payload from the live channel ([82c62db](https://github.com/ReventlessDev/reventless-core/commit/82c62db00b87f7135378496e36601b544d4d2b62))
|
|
25
|
+
* **spec:** [@retired](https://github.com/retired) state-field annotation and its schema emission ([2d8234b](https://github.com/ReventlessDev/reventless-core/commit/2d8234b6b3dd8f479031a67eb5b4b47b5c0c2ff9))
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
# 3.0.0-alpha.224 (2026-08-15)
|
|
7
29
|
|
|
8
30
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.226",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -35,23 +35,23 @@
|
|
|
35
35
|
"sury": "11.0.0-alpha.4",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
38
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
38
39
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
|
|
39
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.7",
|
|
40
40
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
|
|
41
|
-
"@reventlessdev/rescript-
|
|
41
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.7",
|
|
42
42
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
43
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.86",
|
|
44
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.144",
|
|
45
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.185",
|
|
46
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.238",
|
|
47
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.102",
|
|
48
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.15",
|
|
49
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.116"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.0",
|
|
53
53
|
"sury-ppx": "11.0.0-alpha.2",
|
|
54
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
54
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.68"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"rescript": "12.3.0"
|
package/src/Platform.res
CHANGED
|
@@ -700,7 +700,7 @@ module MakeWithConfig = (
|
|
|
700
700
|
StateViewSliceMaker.Make(Spec, Projection)
|
|
701
701
|
}
|
|
702
702
|
|
|
703
|
-
// Admin UI-fragment registry (docs/plans/event-sourced-fragment-registries.md): the
|
|
703
|
+
// Admin UI-fragment registry (docs/plans/done/event-sourced-fragment-registries.md): the
|
|
704
704
|
// platform UI-fragment registry hosted as an admin DCB slice on the admin DcbEventLog.
|
|
705
705
|
// Built once and passed into Admin.construct below.
|
|
706
706
|
module UiFragmentRegistrySlice = StateChangeSlice.Make(
|
|
@@ -20,7 +20,7 @@ let ensureSchema = (db: SqliteDriver.t) => {
|
|
|
20
20
|
db->SqliteDriver.exec(
|
|
21
21
|
"CREATE TABLE IF NOT EXISTS event_log (log_name TEXT NOT NULL, aggregate_id TEXT NOT NULL, seq_nr INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY (log_name, aggregate_id, seq_nr))",
|
|
22
22
|
)
|
|
23
|
-
// Keep-one aggregate-state snapshots (docs/plans/aggregate-snapshotting.md):
|
|
23
|
+
// Keep-one aggregate-state snapshots (docs/plans/done/aggregate-snapshotting.md):
|
|
24
24
|
// one row per (log, aggregate), overwritten on every snapshot write. `state`
|
|
25
25
|
// is the JSON-encoded fold of events seq 0..seq_nr-1; `schema_hash` lets the
|
|
26
26
|
// consumer detect a state-schema drift and fall back to full replay.
|
package/src/adapter/LocalBus.res
CHANGED
|
@@ -133,6 +133,7 @@ module type T = {
|
|
|
133
133
|
~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
134
134
|
~labelField: string,
|
|
135
135
|
~ownerScope: (string, string)=?,
|
|
136
|
+
~retiredScope: Reventless.OwnerScope.retiredScope=?,
|
|
136
137
|
) => option<JSON.t>,
|
|
137
138
|
) => unit
|
|
138
139
|
let getQueryDbListPage: string => option<
|
|
@@ -141,6 +142,7 @@ module type T = {
|
|
|
141
142
|
~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
142
143
|
~labelField: string,
|
|
143
144
|
~ownerScope: (string, string)=?,
|
|
145
|
+
~retiredScope: Reventless.OwnerScope.retiredScope=?,
|
|
144
146
|
) => option<JSON.t>,
|
|
145
147
|
>
|
|
146
148
|
|
|
@@ -238,6 +240,7 @@ module Impl = (C: BusConfig): T => {
|
|
|
238
240
|
~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
239
241
|
~labelField: string,
|
|
240
242
|
~ownerScope: (string, string)=?,
|
|
243
|
+
~retiredScope: Reventless.OwnerScope.retiredScope=?,
|
|
241
244
|
) => option<JSON.t>,
|
|
242
245
|
>,
|
|
243
246
|
> = ref(Dict.make())
|
|
@@ -27,6 +27,19 @@
|
|
|
27
27
|
tell it nothing. */
|
|
28
28
|
let maxStateChars = 60 * 1024
|
|
29
29
|
|
|
30
|
+
/** The `@retired` field of a registered read model, or `None`.
|
|
31
|
+
|
|
32
|
+
Looked up by view name rather than passed down from the storage's
|
|
33
|
+
construction, for the reason the resolvers look it up per request: a storage
|
|
34
|
+
is built before every plugin's state schema is necessarily registered, and a
|
|
35
|
+
lookup that missed at construction would publish payloads for the rest of
|
|
36
|
+
the process. */
|
|
37
|
+
let retiredSpecFor = (name: string): option<Reventless.StateAnnotations.retiredSpec> =>
|
|
38
|
+
ReventlessCore.Plugin_Helpers.stateSchemaRegistry
|
|
39
|
+
->Dict.get(name)
|
|
40
|
+
->Option.flatMap(Reventless.StateAnnotations.getSpec)
|
|
41
|
+
->Option.flatMap(spec => spec.retired)
|
|
42
|
+
|
|
30
43
|
let pickSortKeyValue = (state: JSON.t): option<string> =>
|
|
31
44
|
switch state->JSON.Decode.object {
|
|
32
45
|
| Some(obj) =>
|
|
@@ -68,16 +81,47 @@ let nextSequence = (): string => {
|
|
|
68
81
|
- `state`: the resulting row for save(); `None` for delete(), which has no new
|
|
69
82
|
row — the descriptor then omits both `state` and `sortKeyValue`.
|
|
70
83
|
- `seq`: monotonic ordering token, from `nextSequence`. */
|
|
71
|
-
let make = (
|
|
84
|
+
let make = (
|
|
85
|
+
~changeKind: string,
|
|
86
|
+
~id: string,
|
|
87
|
+
~state: option<JSON.t>,
|
|
88
|
+
~seq: string,
|
|
89
|
+
~retiredField: option<string>=?,
|
|
90
|
+
~retiredValues: option<array<string>>=?,
|
|
91
|
+
): JSON.t => {
|
|
72
92
|
let descriptor = Dict.make()
|
|
73
93
|
descriptor->Dict.set("changeKind", JSON.Encode.string(changeKind))
|
|
74
94
|
descriptor->Dict.set("id", JSON.Encode.string(id))
|
|
75
|
-
|
|
95
|
+
// Whether the row this descriptor announces has been withdrawn from ordinary
|
|
96
|
+
// reads. A publish cannot be scoped per subscriber — the channel is keyed by
|
|
97
|
+
// list field and entity, and everyone watching the view receives it — so a
|
|
98
|
+
// payload here would hand the row to exactly the callers the resolvers just
|
|
99
|
+
// refused it to.
|
|
100
|
+
// Both forms of `@retired` are the one question `isRetiredValue` answers, so
|
|
101
|
+
// nothing here branches on which the view declared.
|
|
102
|
+
let isRetired = switch (retiredField, state) {
|
|
103
|
+
| (Some(field), Some(s)) =>
|
|
104
|
+
{Reventless.OwnerScope.field, values: retiredValues}->Reventless.OwnerScope.isRetiredValue(
|
|
105
|
+
s->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field)),
|
|
106
|
+
)
|
|
107
|
+
| _ => false
|
|
108
|
+
}
|
|
109
|
+
// Dropped along with the payload, not just the payload: the sort value is a
|
|
110
|
+
// timestamp off a row the subscriber may not read, and a subscriber without
|
|
111
|
+
// one is conservative rather than wrong — it refetches instead of deciding
|
|
112
|
+
// which page the row belongs on.
|
|
113
|
+
switch isRetired ? None : state->Option.flatMap(pickSortKeyValue) {
|
|
76
114
|
| Some(v) => descriptor->Dict.set("sortKeyValue", JSON.Encode.string(v))
|
|
77
115
|
| None => ()
|
|
78
116
|
}
|
|
79
117
|
descriptor->Dict.set("seq", JSON.Encode.string(seq))
|
|
80
118
|
switch state {
|
|
119
|
+
// `Updated` with no payload is the shape this already emits for an oversized
|
|
120
|
+
// row, and it means the same thing to a client: refetch and let the query
|
|
121
|
+
// layer answer. Deliberately not `Removed` — the row is not gone, and saying
|
|
122
|
+
// so would be false for an elevated subscriber reading with `includeRetired`
|
|
123
|
+
// and false again the moment the row is restored.
|
|
124
|
+
| Some(_) if isRetired => ()
|
|
81
125
|
| Some(s) =>
|
|
82
126
|
let encoded = s->JSON.stringify
|
|
83
127
|
if encoded->String.length <= maxStateChars {
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
6
|
+
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
7
|
+
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
8
|
+
|
|
9
|
+
function retiredSpecFor(name) {
|
|
10
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name], StateAnnotations$Reventless.getSpec), spec => spec.retired);
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
function pickSortKeyValue(state) {
|
|
7
14
|
let obj = Stdlib_JSON.Decode.object(state);
|
|
@@ -29,16 +36,20 @@ function nextSequence() {
|
|
|
29
36
|
return next.toString();
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
function make(changeKind, id, state, seq) {
|
|
39
|
+
function make(changeKind, id, state, seq, retiredField, retiredValues) {
|
|
33
40
|
let descriptor = {};
|
|
34
41
|
descriptor["changeKind"] = changeKind;
|
|
35
42
|
descriptor["id"] = id;
|
|
36
|
-
let
|
|
43
|
+
let isRetired = retiredField !== undefined && state !== undefined ? OwnerScope$Reventless.isRetiredValue({
|
|
44
|
+
field: retiredField,
|
|
45
|
+
values: retiredValues
|
|
46
|
+
}, Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(state), d => d[retiredField])) : false;
|
|
47
|
+
let v = isRetired ? undefined : Stdlib_Option.flatMap(state, pickSortKeyValue);
|
|
37
48
|
if (v !== undefined) {
|
|
38
49
|
descriptor["sortKeyValue"] = v;
|
|
39
50
|
}
|
|
40
51
|
descriptor["seq"] = seq;
|
|
41
|
-
if (state !== undefined) {
|
|
52
|
+
if (state !== undefined && !isRetired) {
|
|
42
53
|
let encoded = JSON.stringify(state);
|
|
43
54
|
if (encoded.length <= 61440) {
|
|
44
55
|
descriptor["state"] = state;
|
|
@@ -53,9 +64,10 @@ let maxStateChars = 61440;
|
|
|
53
64
|
|
|
54
65
|
export {
|
|
55
66
|
maxStateChars,
|
|
67
|
+
retiredSpecFor,
|
|
56
68
|
pickSortKeyValue,
|
|
57
69
|
lastSequence,
|
|
58
70
|
nextSequence,
|
|
59
71
|
make,
|
|
60
72
|
}
|
|
61
|
-
/*
|
|
73
|
+
/* OwnerScope-Reventless Not a pure module */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// refactor. So under Backend.Postgres the read-model live-query path uses the
|
|
10
10
|
// in-memory arm (getDb() is None for Postgres → Memory), rebuilt by replay from
|
|
11
11
|
// the durable pg event log; the deploy-usable ReventlessPostgres.QueryDbStorage_Postgres
|
|
12
|
-
// serves non-local compute layers. See docs/plans/postgres-storage-adapter.md §E.
|
|
12
|
+
// serves non-local compute layers. See docs/plans/done/postgres-storage-adapter.md §E.
|
|
13
13
|
|
|
14
14
|
module Make = (Bus: LocalBus.T) => {
|
|
15
15
|
module Mem = QueryDbStorage_InMemory.Make(Bus)
|
|
@@ -180,6 +180,54 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
180
180
|
->Option.mapOr(false, v => v == required)
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// ── Retirement narrowing ─────────────────────────────────────────────────
|
|
184
|
+
// The same two forms as owner scoping above, for the same reason: a door
|
|
185
|
+
// that narrows the page and a door that hands back one row both have to ask.
|
|
186
|
+
// Read per request for the same reason too.
|
|
187
|
+
let retiredSpecOf = () =>
|
|
188
|
+
Plugin_Helpers.stateSchemaRegistry
|
|
189
|
+
->Dict.get(name)
|
|
190
|
+
->Option.flatMap(Reventless.StateAnnotations.getSpec)
|
|
191
|
+
->Option.flatMap(spec => spec.retired)
|
|
192
|
+
|
|
193
|
+
// `includeRetired` is the caller asking; `decideRetired` decides whether the
|
|
194
|
+
// asking counts. Read here rather than inside the decision so the argument
|
|
195
|
+
// stays an argument — the door reports what was asked, the classifier says
|
|
196
|
+
// who may be answered.
|
|
197
|
+
let askedForRetired = (~args) =>
|
|
198
|
+
args
|
|
199
|
+
->JSON.Decode.object
|
|
200
|
+
->Option.flatMap(d => d->Dict.get("includeRetired"))
|
|
201
|
+
->Option.flatMap(JSON.Decode.bool)
|
|
202
|
+
->Option.getOr(false)
|
|
203
|
+
|
|
204
|
+
let retiredDecision = (~ctx, ~args) =>
|
|
205
|
+
extractIdentity(ctx)->Reventless.OwnerScope.decideRetired(
|
|
206
|
+
~retiredField=retiredSpecOf()->Option.map(r => r.field),
|
|
207
|
+
~retiredValues=?retiredSpecOf()->Option.flatMap(r => r.values),
|
|
208
|
+
~asked=askedForRetired(~args),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
let retiredScopeFor = (~ctx, ~args) =>
|
|
212
|
+
retiredDecision(~ctx, ~args)->Reventless.OwnerScope.retiredScopeOf
|
|
213
|
+
|
|
214
|
+
// Post-read form. The single-row and by-ids doors are where a filtered list
|
|
215
|
+
// would otherwise be walked around: a pasted URL, or a reference resolved
|
|
216
|
+
// from another row, reaches the entity directly.
|
|
217
|
+
let retiredAllows = (~ctx, ~args, item: JSON.t) =>
|
|
218
|
+
switch retiredScopeFor(~ctx, ~args) {
|
|
219
|
+
| None => true
|
|
220
|
+
// Absent keeps the row, matching `QueryDbListQuery`: a row written before
|
|
221
|
+
// the annotation existed is not retired. Which of the two forms decides
|
|
222
|
+
// that is `isRetiredValue`'s question, not this door's.
|
|
223
|
+
| Some(scope) =>
|
|
224
|
+
!(
|
|
225
|
+
scope->Reventless.OwnerScope.isRetiredValue(
|
|
226
|
+
item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(scope.field)),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
183
231
|
let cap = s => s->String.charAt(0)->String.toUpperCase ++ s->String.slice(~start=1)
|
|
184
232
|
|
|
185
233
|
// Resolve query field names: check registry first, fall back to safe defaults.
|
|
@@ -260,7 +308,8 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
260
308
|
// A row the caller does not own answers as though it were not there.
|
|
261
309
|
// Distinguishing "not yours" from "not found" here would turn this door
|
|
262
310
|
// into an oracle for which ids exist.
|
|
263
|
-
| Some(item) if !ownerAllows(~ctx, item) =>
|
|
311
|
+
| Some(item) if !ownerAllows(~ctx, item) || !retiredAllows(~ctx, ~args, item) =>
|
|
312
|
+
JSON.Encode.null
|
|
264
313
|
| Some(item) =>
|
|
265
314
|
if includeIdParam {
|
|
266
315
|
// Copied — `JSON.Decode.object` hands back the stored object itself,
|
|
@@ -327,7 +376,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
327
376
|
loaded
|
|
328
377
|
->Array.filterMap(((id, opt)) =>
|
|
329
378
|
opt
|
|
330
|
-
->Option.filter(item => ownerAllows(~ctx, item))
|
|
379
|
+
->Option.filter(item => ownerAllows(~ctx, item) && retiredAllows(~ctx, ~args, item))
|
|
331
380
|
->Option.map(item => {
|
|
332
381
|
let obj = item->JSON.Decode.object->Option.mapOr(Dict.make(), Dict.copy)
|
|
333
382
|
obj->Dict.set("id", JSON.Encode.string(id))
|
|
@@ -411,6 +460,11 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
411
460
|
| RefuseOwned => emptyConnection
|
|
412
461
|
| decision =>
|
|
413
462
|
let ownerScope = Reventless.OwnerScope.scopeOf(decision)
|
|
463
|
+
// Same both-arms rule as `ownerScope`, and the same reason it is
|
|
464
|
+
// spelled out: this predicate decides what the caller may see, so a
|
|
465
|
+
// push-down that did not receive it would serve the archive to
|
|
466
|
+
// everyone on the path that is hardest to notice.
|
|
467
|
+
let retiredScope = retiredScopeFor(~ctx, ~args)
|
|
414
468
|
let argsDict = args->JSON.Decode.object->Option.getOr(Dict.make())
|
|
415
469
|
// Prefer a backend list push-down (SQLite builds json_extract predicates
|
|
416
470
|
// + ORDER BY … LIMIT so it never materialises the whole read model). When
|
|
@@ -427,7 +481,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
427
481
|
DomainGraphQL_Server.decodeGlobalId(id)->Option.map(((_, lid)) => lid)
|
|
428
482
|
switch Bus.getQueryDbListPage(name) {
|
|
429
483
|
| Some(listPage) =>
|
|
430
|
-
switch listPage(~argsDict, ~capability, ~labelField, ~ownerScope?) {
|
|
484
|
+
switch listPage(~argsDict, ~capability, ~labelField, ~ownerScope?, ~retiredScope?) {
|
|
431
485
|
| Some(conn) => conn
|
|
432
486
|
| None =>
|
|
433
487
|
let items = await fetchAllItems()
|
|
@@ -438,6 +492,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
438
492
|
~labelField,
|
|
439
493
|
~decodeLocalId,
|
|
440
494
|
~ownerScope?,
|
|
495
|
+
~retiredScope?,
|
|
441
496
|
)
|
|
442
497
|
}
|
|
443
498
|
| None =>
|
|
@@ -449,6 +504,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
449
504
|
~labelField,
|
|
450
505
|
~decodeLocalId,
|
|
451
506
|
~ownerScope?,
|
|
507
|
+
~retiredScope?,
|
|
452
508
|
)
|
|
453
509
|
}
|
|
454
510
|
}
|
|
@@ -466,7 +522,8 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
466
522
|
// The legacy shape has no push-down to reach, so the narrowing is a plain
|
|
467
523
|
// filter here. `scannedCount` counts what is returned, not what was read:
|
|
468
524
|
// the pre-scoping total would tell a caller how many rows they may not see.
|
|
469
|
-
let items =
|
|
525
|
+
let items =
|
|
526
|
+
all->Array.filter(item => ownerAllows(~ctx, item) && retiredAllows(~ctx, ~args, item))
|
|
470
527
|
Obj.magic({"nextToken": Nullable.null, "scannedCount": items->Array.length, "items": items})
|
|
471
528
|
}
|
|
472
529
|
}
|
|
@@ -531,7 +588,10 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
531
588
|
// Narrowed here, before the cursor window and the sort-key filter, so
|
|
532
589
|
// every page this door emits is a page of rows the caller owns. Doing
|
|
533
590
|
// it after would hand back short pages with valid cursors.
|
|
534
|
-
let allItems =
|
|
591
|
+
let allItems =
|
|
592
|
+
loaded->Array.filter(item =>
|
|
593
|
+
ownerAllows(~ctx, item) && retiredAllows(~ctx, ~args, item)
|
|
594
|
+
)
|
|
535
595
|
|
|
536
596
|
// Cursor-keyed filtering: exclude items on the cursor side of the boundary
|
|
537
597
|
let cursorFiltered = if isBackward {
|
|
@@ -624,7 +684,8 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
624
684
|
// push-down and the scan are two ways to reach the same rows, and a
|
|
625
685
|
// narrowing that lives in only one is a hole that appears when a
|
|
626
686
|
// backend gains or loses an index.
|
|
627
|
-
let scoped = rows =>
|
|
687
|
+
let scoped = rows =>
|
|
688
|
+
rows->Array.filter(item => ownerAllows(~ctx, item) && retiredAllows(~ctx, ~args, item))
|
|
628
689
|
// Prefer the pushed-down equality lookup (SQLite rides the GSI index;
|
|
629
690
|
// in-memory reuses its lazy snapshot). Fall back to scan+filter only
|
|
630
691
|
// if no lookup is registered for this QueryDb.
|
|
@@ -11,6 +11,7 @@ import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/I
|
|
|
11
11
|
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
12
12
|
import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
|
|
13
13
|
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
14
|
+
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
14
15
|
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
15
16
|
import * as SortKey_Filter$ReventlessLocal from "./SortKey_Filter.res.mjs";
|
|
16
17
|
import * as QueryDbListQuery$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/QueryDbListQuery.res.mjs";
|
|
@@ -100,6 +101,17 @@ function Make(Bus) {
|
|
|
100
101
|
let field = match._0;
|
|
101
102
|
return Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]), Stdlib_JSON.Decode.string), false, v => v === required);
|
|
102
103
|
};
|
|
104
|
+
let retiredSpecOf = () => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name], StateAnnotations$Reventless.getSpec), spec => spec.retired);
|
|
105
|
+
let askedForRetired = args => Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d["includeRetired"]), Stdlib_JSON.Decode.bool), false);
|
|
106
|
+
let retiredDecision = (ctx, args) => OwnerScope$Reventless.decideRetired(extractIdentity(ctx), Stdlib_Option.map(retiredSpecOf(), r => r.field), Stdlib_Option.flatMap(retiredSpecOf(), r => r.values), askedForRetired(args), undefined);
|
|
107
|
+
let retiredAllows = (ctx, args, item) => {
|
|
108
|
+
let scope = OwnerScope$Reventless.retiredScopeOf(retiredDecision(ctx, args));
|
|
109
|
+
if (scope !== undefined) {
|
|
110
|
+
return !OwnerScope$Reventless.isRetiredValue(scope, Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[scope.field]));
|
|
111
|
+
} else {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
103
115
|
let cap = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
104
116
|
let registryEntry = Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[name];
|
|
105
117
|
let singleQueryName = registryEntry !== undefined ? registryEntry.singleFieldName : name.charAt(0).toLowerCase() + name.slice(1);
|
|
@@ -139,7 +151,7 @@ function Make(Bus) {
|
|
|
139
151
|
if (item === undefined) {
|
|
140
152
|
return null;
|
|
141
153
|
}
|
|
142
|
-
if (!ownerAllows(ctx, item)) {
|
|
154
|
+
if (!ownerAllows(ctx, item) || !retiredAllows(ctx, args, item)) {
|
|
143
155
|
return null;
|
|
144
156
|
}
|
|
145
157
|
if (!includeIdParam) {
|
|
@@ -186,7 +198,13 @@ function Make(Bus) {
|
|
|
186
198
|
}));
|
|
187
199
|
return Stdlib_Array.filterMap(loaded, param => {
|
|
188
200
|
let id = param[0];
|
|
189
|
-
return Stdlib_Option.map(Stdlib_Option.filter(param[1], item =>
|
|
201
|
+
return Stdlib_Option.map(Stdlib_Option.filter(param[1], item => {
|
|
202
|
+
if (ownerAllows(ctx, item)) {
|
|
203
|
+
return retiredAllows(ctx, args, item);
|
|
204
|
+
} else {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}), item => {
|
|
190
208
|
let obj = Stdlib_Option.mapOr(Stdlib_JSON.Decode.object(item), {}, prim => Object.assign({}, prim));
|
|
191
209
|
obj["id"] = id;
|
|
192
210
|
return obj;
|
|
@@ -242,19 +260,20 @@ function Make(Bus) {
|
|
|
242
260
|
return emptyConnection;
|
|
243
261
|
}
|
|
244
262
|
let ownerScope = OwnerScope$Reventless.scopeOf(decision);
|
|
263
|
+
let retiredScope = OwnerScope$Reventless.retiredScopeOf(retiredDecision(ctx, args));
|
|
245
264
|
let argsDict = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(args), {});
|
|
246
265
|
let decodeLocalId = id => Stdlib_Option.map(DomainGraphQL_Server$ReventlessLocal.decodeGlobalId(id), param => param[1]);
|
|
247
266
|
let listPage = Bus.getQueryDbListPage(name);
|
|
248
267
|
if (listPage !== undefined) {
|
|
249
|
-
let conn = listPage(argsDict, capability, labelField, ownerScope);
|
|
268
|
+
let conn = listPage(argsDict, capability, labelField, ownerScope, retiredScope);
|
|
250
269
|
if (conn !== undefined) {
|
|
251
270
|
return conn;
|
|
252
271
|
}
|
|
253
272
|
let items = await fetchAllItems();
|
|
254
|
-
return QueryDbListQuery$ReventlessCore.run(items, argsDict, capability, labelField, decodeLocalId, ownerScope);
|
|
273
|
+
return QueryDbListQuery$ReventlessCore.run(items, argsDict, capability, labelField, decodeLocalId, ownerScope, retiredScope);
|
|
255
274
|
}
|
|
256
275
|
let items$1 = await fetchAllItems();
|
|
257
|
-
return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, capability, labelField, decodeLocalId, ownerScope);
|
|
276
|
+
return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, capability, labelField, decodeLocalId, ownerScope, retiredScope);
|
|
258
277
|
};
|
|
259
278
|
match = [
|
|
260
279
|
sdl,
|
|
@@ -272,7 +291,13 @@ function Make(Bus) {
|
|
|
272
291
|
};
|
|
273
292
|
}
|
|
274
293
|
let all = await fetchAllItems();
|
|
275
|
-
let items = all.filter(item =>
|
|
294
|
+
let items = all.filter(item => {
|
|
295
|
+
if (ownerAllows(ctx, item)) {
|
|
296
|
+
return retiredAllows(ctx, args, item);
|
|
297
|
+
} else {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
276
301
|
return {
|
|
277
302
|
nextToken: null,
|
|
278
303
|
scannedCount: items.length,
|
|
@@ -331,7 +356,13 @@ function Make(Bus) {
|
|
|
331
356
|
return emptyConn;
|
|
332
357
|
}
|
|
333
358
|
let loaded = await Effect.runPromise(Effect.catchAll(Stream.runCollect(ops.loadStream(id)), param => Effect.succeed([])));
|
|
334
|
-
let allItems = loaded.filter(item =>
|
|
359
|
+
let allItems = loaded.filter(item => {
|
|
360
|
+
if (ownerAllows(ctx, item)) {
|
|
361
|
+
return retiredAllows(ctx, args, item);
|
|
362
|
+
} else {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
335
366
|
let cursorFiltered;
|
|
336
367
|
if (isBackward) {
|
|
337
368
|
let beforeKey = Stdlib_Option.map(before, decodeCursor);
|
|
@@ -408,7 +439,13 @@ function Make(Bus) {
|
|
|
408
439
|
return [];
|
|
409
440
|
}
|
|
410
441
|
let value = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[index]), Stdlib_JSON.Decode.string), "");
|
|
411
|
-
let scoped = rows => rows.filter(item =>
|
|
442
|
+
let scoped = rows => rows.filter(item => {
|
|
443
|
+
if (ownerAllows(ctx, item)) {
|
|
444
|
+
return retiredAllows(ctx, args, item);
|
|
445
|
+
} else {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
412
449
|
let lookup = Bus.getQueryDbIndexLookup(name);
|
|
413
450
|
if (lookup !== undefined) {
|
|
414
451
|
return scoped(lookup(filterField, value));
|
|
@@ -198,6 +198,8 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
198
198
|
~id=entityKeyFor(id, subKey),
|
|
199
199
|
~state=Some(state),
|
|
200
200
|
~seq=LocalStateChangeDescriptor.nextSequence(),
|
|
201
|
+
~retiredField=?LocalStateChangeDescriptor.retiredSpecFor(name)->Option.map(r => r.field),
|
|
202
|
+
~retiredValues=?LocalStateChangeDescriptor.retiredSpecFor(name)->Option.flatMap(r => r.values),
|
|
201
203
|
)
|
|
202
204
|
Bus.publishStateChange(~name, ~descriptor)
|
|
203
205
|
}
|
|
@@ -167,11 +167,11 @@ function Make(Bus) {
|
|
|
167
167
|
};
|
|
168
168
|
let publishSaved = (changeKind, id, state) => {
|
|
169
169
|
let subKey = getSubKey(state, subIdField);
|
|
170
|
-
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make(changeKind, entityKeyFor(id, subKey), state, LocalStateChangeDescriptor$ReventlessLocal.nextSequence());
|
|
170
|
+
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make(changeKind, entityKeyFor(id, subKey), state, LocalStateChangeDescriptor$ReventlessLocal.nextSequence(), Stdlib_Option.map(LocalStateChangeDescriptor$ReventlessLocal.retiredSpecFor(name), r => r.field), Stdlib_Option.flatMap(LocalStateChangeDescriptor$ReventlessLocal.retiredSpecFor(name), r => r.values));
|
|
171
171
|
Bus.publishStateChange(name, descriptor);
|
|
172
172
|
};
|
|
173
173
|
let publishRemoved = (id, subKey) => {
|
|
174
|
-
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make("Removed", entityKeyFor(id, subKey), undefined, LocalStateChangeDescriptor$ReventlessLocal.nextSequence());
|
|
174
|
+
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make("Removed", entityKeyFor(id, subKey), undefined, LocalStateChangeDescriptor$ReventlessLocal.nextSequence(), undefined, undefined);
|
|
175
175
|
Bus.publishStateChange(name, descriptor);
|
|
176
176
|
};
|
|
177
177
|
let save = async (id, state, _saveMode, ttl) => {
|
|
@@ -147,6 +147,7 @@ type busCallbacks = {
|
|
|
147
147
|
~capability: GraphQL_FragmentGenerator.serverCapability,
|
|
148
148
|
~labelField: string,
|
|
149
149
|
~ownerScope: (string, string)=?,
|
|
150
|
+
~retiredScope: Reventless.OwnerScope.retiredScope=?,
|
|
150
151
|
) => option<JSON.t>,
|
|
151
152
|
) => unit,
|
|
152
153
|
}
|
|
@@ -263,6 +264,8 @@ let makeStorage = (
|
|
|
263
264
|
~id=entityKeyFor(id, subKey),
|
|
264
265
|
~state=Some(state),
|
|
265
266
|
~seq=LocalStateChangeDescriptor.nextSequence(),
|
|
267
|
+
~retiredField=?LocalStateChangeDescriptor.retiredSpecFor(name)->Option.map(r => r.field),
|
|
268
|
+
~retiredValues=?LocalStateChangeDescriptor.retiredSpecFor(name)->Option.flatMap(r => r.values),
|
|
266
269
|
)
|
|
267
270
|
bus.publishStateChange(~name, ~descriptor)
|
|
268
271
|
}
|
|
@@ -428,6 +431,7 @@ let makeStorage = (
|
|
|
428
431
|
~capability: GraphQL_FragmentGenerator.serverCapability,
|
|
429
432
|
~labelField as _,
|
|
430
433
|
~ownerScope: option<(string, string)>=?,
|
|
434
|
+
~retiredScope: option<Reventless.OwnerScope.retiredScope>=?,
|
|
431
435
|
): option<JSON.t> => {
|
|
432
436
|
let filterDict =
|
|
433
437
|
argsDict->Dict.get("filter")->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
|
|
@@ -451,6 +455,39 @@ let makeStorage = (
|
|
|
451
455
|
params->Array.push(JSON.Encode.string(required))
|
|
452
456
|
| None => ()
|
|
453
457
|
}
|
|
458
|
+
// `IS NOT 1`, not `= 0`, and on the raw extraction rather than through
|
|
459
|
+
// `jsonText`. SQLite renders a JSON true as the integer 1 and a missing
|
|
460
|
+
// path as NULL, and `IS NOT` is the comparison that treats NULL as a value
|
|
461
|
+
// rather than poisoning the predicate. Three cases have to land the way the
|
|
462
|
+
// in-memory spec lands them: absent keeps the row (a row written before the
|
|
463
|
+
// annotation is not retired), false keeps it, and a value that is not a
|
|
464
|
+
// JSON boolean keeps it too — which `= 0` would get wrong, since a string
|
|
465
|
+
// `'true'` compares unequal to 0 and the row would vanish on this backend
|
|
466
|
+
// only. `QueryDbListPushdownParityTest` is what holds the two together.
|
|
467
|
+
//
|
|
468
|
+
// The state form compares against the states' text instead of 1, and the
|
|
469
|
+
// three cases land identically: absent is NULL, a state outside the set
|
|
470
|
+
// compares unequal to every member, and a non-string value is none of them
|
|
471
|
+
// either.
|
|
472
|
+
//
|
|
473
|
+
// Written as a conjunction of `IS NOT` rather than as `NOT IN`: `IN` is
|
|
474
|
+
// ordinary equality, so a NULL path makes the predicate NULL and the row
|
|
475
|
+
// vanishes — the absent case, which has to keep the row. `IS NOT` is the
|
|
476
|
+
// comparison that treats NULL as a value, and it does not have an `IN`
|
|
477
|
+
// spelling. A set naming nothing pushes no predicate down, which is the
|
|
478
|
+
// same answer `isRetiredValue` gives it.
|
|
479
|
+
switch retiredScope {
|
|
480
|
+
| Some(scope) =>
|
|
481
|
+
let path = `json_extract(item, '$.${scope.field->String.replaceAll("'", "''")}')`
|
|
482
|
+
switch scope.values {
|
|
483
|
+
| None => whereParts->Array.push(`${path} IS NOT 1`)
|
|
484
|
+
| Some(states) =>
|
|
485
|
+
states->Array.forEach(state =>
|
|
486
|
+
whereParts->Array.push(`${path} IS NOT '${state->String.replaceAll("'", "''")}'`)
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
| None => ()
|
|
490
|
+
}
|
|
454
491
|
let valString = v =>
|
|
455
492
|
switch v->JSON.Decode.string {
|
|
456
493
|
| Some(s) => Some(s)
|
|
@@ -194,7 +194,7 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
194
194
|
};
|
|
195
195
|
let publishSaved = (changeKind, id, state) => {
|
|
196
196
|
let subKey = computeSubKey(state, subIdField);
|
|
197
|
-
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make(changeKind, entityKeyFor(id, subKey), state, LocalStateChangeDescriptor$ReventlessLocal.nextSequence());
|
|
197
|
+
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make(changeKind, entityKeyFor(id, subKey), state, LocalStateChangeDescriptor$ReventlessLocal.nextSequence(), Stdlib_Option.map(LocalStateChangeDescriptor$ReventlessLocal.retiredSpecFor(name), r => r.field), Stdlib_Option.flatMap(LocalStateChangeDescriptor$ReventlessLocal.retiredSpecFor(name), r => r.values));
|
|
198
198
|
bus.publishStateChange(name, descriptor);
|
|
199
199
|
};
|
|
200
200
|
let saveKind = (id, state) => {
|
|
@@ -209,7 +209,7 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
209
209
|
}
|
|
210
210
|
};
|
|
211
211
|
let publishRemoved = (id, subKey) => {
|
|
212
|
-
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make("Removed", entityKeyFor(id, subKey), undefined, LocalStateChangeDescriptor$ReventlessLocal.nextSequence());
|
|
212
|
+
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make("Removed", entityKeyFor(id, subKey), undefined, LocalStateChangeDescriptor$ReventlessLocal.nextSequence(), undefined, undefined);
|
|
213
213
|
bus.publishStateChange(name, descriptor);
|
|
214
214
|
};
|
|
215
215
|
let rowKeysForPartition = id => SqliteDriver$ReventlessLocal.all(selectByPartitionStmt, [id]).map(row => {
|
|
@@ -337,7 +337,7 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
337
337
|
};
|
|
338
338
|
let jsonText = field => `CAST(json_extract(item, '$.` + field.replaceAll("'", "''") + `') AS TEXT)`;
|
|
339
339
|
let idExpr = "COALESCE(json_extract(item, '$.id'), partition_key)";
|
|
340
|
-
let listPage = (argsDict, capability, param, ownerScope) => {
|
|
340
|
+
let listPage = (argsDict, capability, param, ownerScope, retiredScope) => {
|
|
341
341
|
let filterDict = Stdlib_Option.getOr(Stdlib_Option.flatMap(argsDict["filter"], Stdlib_JSON.Decode.object), {});
|
|
342
342
|
let strNonEmpty = k => Stdlib_Option.mapOr(Stdlib_Option.flatMap(filterDict[k], Stdlib_JSON.Decode.string), false, s => s.length > 0);
|
|
343
343
|
let hasIds = Stdlib_Option.mapOr(Stdlib_Option.flatMap(filterDict["ids"], Stdlib_JSON.Decode.array), false, a => a.length !== 0);
|
|
@@ -352,6 +352,17 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
352
352
|
whereParts.push(jsonText(ownerScope[0]) + ` = ?`);
|
|
353
353
|
params.push(ownerScope[1]);
|
|
354
354
|
}
|
|
355
|
+
if (retiredScope !== undefined) {
|
|
356
|
+
let path = `json_extract(item, '$.` + retiredScope.field.replaceAll("'", "''") + `')`;
|
|
357
|
+
let states = retiredScope.values;
|
|
358
|
+
if (states !== undefined) {
|
|
359
|
+
states.forEach(state => {
|
|
360
|
+
whereParts.push(path + ` IS NOT '` + state.replaceAll("'", "''") + `'`);
|
|
361
|
+
});
|
|
362
|
+
} else {
|
|
363
|
+
whereParts.push(path + ` IS NOT 1`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
355
366
|
let valString = v => {
|
|
356
367
|
let s = Stdlib_JSON.Decode.string(v);
|
|
357
368
|
if (s !== undefined) {
|