@reventlessdev/reventless-aws 3.0.0-alpha.301 → 3.0.0-alpha.303
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 +20 -0
- package/package.json +7 -7
- package/src/Platform.res +18 -4
- package/src/Platform.res.mjs +24 -4
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +39 -4
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +34 -13
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +108 -20
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +99 -17
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +41 -3
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +27 -6
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +34 -4
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +16 -5
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +10 -0
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +4 -1
- package/src/adapter/Runtime/StateTopicPublish.mjs +38 -5
- package/src/adapter/StateTopic/StateTopic_AppSync.res +49 -1
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +28 -3
- package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +73 -2
- package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +39 -4
- package/src/util/Util_DynamoDbStream.res +15 -4
- package/src/util/Util_DynamoDbStream.res.mjs +15 -5
- package/src/util/Util_ShellConfig.res +22 -2
- package/src/util/Util_ShellConfig.res.mjs +11 -0
- package/tests/PgQueryResolver_LambdaTest.res +16 -1
- package/tests/PgQueryResolver_LambdaTest.res.mjs +46 -32
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res +111 -0
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res.mjs +67 -0
- package/tests/QueryDbResolvers_AppSyncTest.res.mjs +3 -3
- package/tests/StateChangeDescriptorParityTest.res +82 -1
- package/tests/StateChangeDescriptorParityTest.res.mjs +97 -1
- package/tests/Util_DynamoDbStreamTest.res +43 -0
- package/tests/Util_DynamoDbStreamTest.res.mjs +41 -0
- package/tests/Util_ShellConfigTest.res +107 -0
- package/tests/Util_ShellConfigTest.res.mjs +100 -0
- package/tests/stateChangeDescriptorParity.mjs +72 -3
|
@@ -68,6 +68,7 @@ type pushdowns = {
|
|
|
68
68
|
~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
69
69
|
~labelField: string,
|
|
70
70
|
~ownerScope: (string, string)=?,
|
|
71
|
+
~retiredScope: Reventless.OwnerScope.retiredScope=?,
|
|
71
72
|
) => promise<option<JSON.t>>,
|
|
72
73
|
// Sub-id connection ({single}Items) — keyset over sub_key within a partition.
|
|
73
74
|
itemsPage: (
|
|
@@ -76,6 +77,7 @@ type pushdowns = {
|
|
|
76
77
|
~id: string,
|
|
77
78
|
~argsDict: dict<JSON.t>,
|
|
78
79
|
~ownerScope: (string, string)=?,
|
|
80
|
+
~retiredScope: Reventless.OwnerScope.retiredScope=?,
|
|
79
81
|
) => promise<JSON.t>,
|
|
80
82
|
// Full materialisation for the list fallback (shapes listPage declines).
|
|
81
83
|
scanAll: (~readModelName: string) => promise<array<JSON.t>>,
|
|
@@ -95,6 +97,8 @@ type binding = {
|
|
|
95
97
|
from the same schema `capability` comes from, so the two cannot disagree
|
|
96
98
|
about which fields this read model has. */
|
|
97
99
|
ownerField: option<string>,
|
|
100
|
+
retiredField: option<string>,
|
|
101
|
+
retiredValues: option<array<string>>,
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
// -- arg helpers -------------------------------------------------------------
|
|
@@ -178,6 +182,35 @@ let dispatch = async (
|
|
|
178
182
|
| ScopeTo(field, required) =>
|
|
179
183
|
item->argStr(field)->Option.mapOr(false, v => v == required)
|
|
180
184
|
}
|
|
185
|
+
// The caller's request to see the archive, honoured only where the
|
|
186
|
+
// classification says it counts.
|
|
187
|
+
let askedForRetired =
|
|
188
|
+
payload.arguments
|
|
189
|
+
->argObj
|
|
190
|
+
->Dict.get("includeRetired")
|
|
191
|
+
->Option.flatMap(JSON.Decode.bool)
|
|
192
|
+
->Option.getOr(false)
|
|
193
|
+
let retiredScope =
|
|
194
|
+
payload.identity
|
|
195
|
+
->Reventless.OwnerScope.decideRetired(
|
|
196
|
+
~retiredField=binding.retiredField,
|
|
197
|
+
~retiredValues=?binding.retiredValues,
|
|
198
|
+
~asked=askedForRetired,
|
|
199
|
+
)
|
|
200
|
+
->Reventless.OwnerScope.retiredScopeOf
|
|
201
|
+
// Absent keeps the row, as in `QueryDbListQuery`. The two forms of `@retired`
|
|
202
|
+
// differ only inside `isRetiredValue`, so nothing here branches on which one
|
|
203
|
+
// the view declared.
|
|
204
|
+
let retiredAllows = (item: JSON.t) =>
|
|
205
|
+
switch retiredScope {
|
|
206
|
+
| None => true
|
|
207
|
+
| Some(scope) =>
|
|
208
|
+
!(
|
|
209
|
+
scope->Reventless.OwnerScope.isRetiredValue(
|
|
210
|
+
item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(scope.field)),
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
}
|
|
181
214
|
switch payload.kind {
|
|
182
215
|
| "getById" =>
|
|
183
216
|
let id = payload.arguments->argStr("id")->Option.getOr("")
|
|
@@ -199,7 +232,7 @@ let dispatch = async (
|
|
|
199
232
|
switch found {
|
|
200
233
|
// A row the caller does not own answers as "not found". Saying "not yours"
|
|
201
234
|
// instead would make this door an oracle for which ids exist.
|
|
202
|
-
| Some(item) if !ownerAllows(item) => JSON.Encode.null
|
|
235
|
+
| Some(item) if !ownerAllows(item) || !retiredAllows(item) => JSON.Encode.null
|
|
203
236
|
| Some(item) => binding.includeIdParam ? withId(item, resolvedKey) : item
|
|
204
237
|
| None => JSON.Encode.null
|
|
205
238
|
}
|
|
@@ -216,7 +249,9 @@ let dispatch = async (
|
|
|
216
249
|
let extra = missing->Array.length > 0
|
|
217
250
|
? await binding.pushdowns.byIds(~readModelName=rm, missing)
|
|
218
251
|
: []
|
|
219
|
-
JSON.Encode.array(
|
|
252
|
+
JSON.Encode.array(
|
|
253
|
+
Array.concat(found, extra)->Array.filter(item => ownerAllows(item) && retiredAllows(item)),
|
|
254
|
+
)
|
|
220
255
|
|
|
221
256
|
| "items" =>
|
|
222
257
|
// Sub-id connection: {single}Items(id, filter, first/after/last/before).
|
|
@@ -233,6 +268,7 @@ let dispatch = async (
|
|
|
233
268
|
~id,
|
|
234
269
|
~argsDict=payload.arguments->argObj,
|
|
235
270
|
~ownerScope=?Reventless.OwnerScope.scopeOf(decision),
|
|
271
|
+
~retiredScope?,
|
|
236
272
|
)
|
|
237
273
|
}
|
|
238
274
|
| None => emptyConnection()
|
|
@@ -274,7 +310,7 @@ let dispatch = async (
|
|
|
274
310
|
if authorized {
|
|
275
311
|
JSON.Encode.array(
|
|
276
312
|
(await binding.pushdowns.indexLookup(~readModelName=rm, field, value))->Array.filter(
|
|
277
|
-
ownerAllows,
|
|
313
|
+
item => ownerAllows(item) && retiredAllows(item),
|
|
278
314
|
),
|
|
279
315
|
)
|
|
280
316
|
} else {
|
|
@@ -293,6 +329,7 @@ let dispatch = async (
|
|
|
293
329
|
~capability=binding.capability,
|
|
294
330
|
~labelField=binding.labelField,
|
|
295
331
|
~ownerScope?,
|
|
332
|
+
~retiredScope?,
|
|
296
333
|
) {
|
|
297
334
|
| Some(conn) => conn
|
|
298
335
|
| None =>
|
|
@@ -306,6 +343,7 @@ let dispatch = async (
|
|
|
306
343
|
~capability=binding.capability,
|
|
307
344
|
~labelField=binding.labelField,
|
|
308
345
|
~ownerScope?,
|
|
346
|
+
~retiredScope?,
|
|
309
347
|
)
|
|
310
348
|
}
|
|
311
349
|
}
|
|
@@ -80,6 +80,15 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
80
80
|
let required = match._1;
|
|
81
81
|
return Stdlib_Option.mapOr(argStr(item, match._0), false, v => v === required);
|
|
82
82
|
};
|
|
83
|
+
let askedForRetired = Stdlib_Option.getOr(Stdlib_Option.flatMap(argObj(payload.arguments)["includeRetired"], Stdlib_JSON.Decode.bool), false);
|
|
84
|
+
let retiredScope = OwnerScope$Reventless.retiredScopeOf(OwnerScope$Reventless.decideRetired(payload.identity, binding.retiredField, binding.retiredValues, askedForRetired, undefined));
|
|
85
|
+
let retiredAllows = item => {
|
|
86
|
+
if (retiredScope !== undefined) {
|
|
87
|
+
return !OwnerScope$Reventless.isRetiredValue(retiredScope, Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[retiredScope.field]));
|
|
88
|
+
} else {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
83
92
|
let other = payload.kind;
|
|
84
93
|
switch (other) {
|
|
85
94
|
case "byIds" :
|
|
@@ -87,7 +96,13 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
87
96
|
let found = await binding.pushdowns.byIds(rm, ids);
|
|
88
97
|
let missing = found.length < ids.length ? Stdlib_Array.filterMap(ids, Api_Ids$ReventlessCore.alternateKey) : [];
|
|
89
98
|
let extra = missing.length !== 0 ? await binding.pushdowns.byIds(rm, missing) : [];
|
|
90
|
-
return found.concat(extra).filter(
|
|
99
|
+
return found.concat(extra).filter(item => {
|
|
100
|
+
if (ownerAllows(item)) {
|
|
101
|
+
return retiredAllows(item);
|
|
102
|
+
} else {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
91
106
|
case "getById" :
|
|
92
107
|
let id = Stdlib_Option.getOr(argStr(payload.arguments, "id"), "");
|
|
93
108
|
let loadKey = async key => {
|
|
@@ -106,7 +121,7 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
106
121
|
await loadKey(match$2)
|
|
107
122
|
];
|
|
108
123
|
let found$1 = match$3[1];
|
|
109
|
-
if (found$1 !== undefined && ownerAllows(found$1)) {
|
|
124
|
+
if (found$1 !== undefined && ownerAllows(found$1) && retiredAllows(found$1)) {
|
|
110
125
|
if (binding.includeIdParam) {
|
|
111
126
|
return withId(found$1, match$3[0]);
|
|
112
127
|
} else {
|
|
@@ -134,7 +149,13 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
134
149
|
authorized = true;
|
|
135
150
|
}
|
|
136
151
|
if (authorized) {
|
|
137
|
-
return (await binding.pushdowns.indexLookup(rm, field, value)).filter(
|
|
152
|
+
return (await binding.pushdowns.indexLookup(rm, field, value)).filter(item => {
|
|
153
|
+
if (ownerAllows(item)) {
|
|
154
|
+
return retiredAllows(item);
|
|
155
|
+
} else {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
138
159
|
} else {
|
|
139
160
|
return [];
|
|
140
161
|
}
|
|
@@ -148,7 +169,7 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
148
169
|
if (typeof decision !== "object" && decision !== "Unscoped") {
|
|
149
170
|
return emptyConnection();
|
|
150
171
|
}
|
|
151
|
-
return await binding.pushdowns.itemsPage(rm, subIdField, id$1, argObj(payload.arguments), OwnerScope$Reventless.scopeOf(decision));
|
|
172
|
+
return await binding.pushdowns.itemsPage(rm, subIdField, id$1, argObj(payload.arguments), OwnerScope$Reventless.scopeOf(decision), retiredScope);
|
|
152
173
|
break;
|
|
153
174
|
case "list" :
|
|
154
175
|
let argsDict = argObj(payload.arguments);
|
|
@@ -157,12 +178,12 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
157
178
|
return emptyConnection();
|
|
158
179
|
}
|
|
159
180
|
let ownerScope = OwnerScope$Reventless.scopeOf(decision$1);
|
|
160
|
-
let conn = await binding.pushdowns.listPage(rm, argsDict, binding.capability, binding.labelField, ownerScope);
|
|
181
|
+
let conn = await binding.pushdowns.listPage(rm, argsDict, binding.capability, binding.labelField, ownerScope, retiredScope);
|
|
161
182
|
if (conn !== undefined) {
|
|
162
183
|
return conn;
|
|
163
184
|
}
|
|
164
185
|
let items$1 = await binding.pushdowns.scanAll(rm);
|
|
165
|
-
return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, binding.capability, binding.labelField, undefined, ownerScope);
|
|
186
|
+
return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, binding.capability, binding.labelField, undefined, ownerScope, retiredScope);
|
|
166
187
|
break;
|
|
167
188
|
case "resolveMany" :
|
|
168
189
|
let target = Stdlib_Option.getOr(payload.target, rm);
|
|
@@ -256,12 +256,12 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
256
256
|
~view=name,
|
|
257
257
|
~ownerField,
|
|
258
258
|
)
|
|
259
|
+
let isIndexed = f =>
|
|
260
|
+
indexes->Array.some(ic => ic.idField->Option.getOr(ic.index) == f) ||
|
|
261
|
+
subIdField->Option.getOr("") == f
|
|
259
262
|
switch ownerField {
|
|
260
263
|
| Some(f) =>
|
|
261
|
-
|
|
262
|
-
indexes->Array.some(ic => ic.idField->Option.getOr(ic.index) == f) ||
|
|
263
|
-
subIdField->Option.getOr("") == f
|
|
264
|
-
if !indexed {
|
|
264
|
+
if !isIndexed(f) {
|
|
265
265
|
log.warn(
|
|
266
266
|
~comp="QueryDbResolvers_AppSync",
|
|
267
267
|
`${name}: @owner field "${f}" is not the key of any index on this table. ` ++
|
|
@@ -271,6 +271,34 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
271
271
|
}
|
|
272
272
|
| None => ()
|
|
273
273
|
}
|
|
274
|
+
let retiredSpec =
|
|
275
|
+
stateSchemaOpt
|
|
276
|
+
->Option.flatMap(Reventless.StateAnnotations.getSpec)
|
|
277
|
+
->Option.flatMap(spec => spec.retired)
|
|
278
|
+
let retiredField = retiredSpec->Option.map(r => r.field)
|
|
279
|
+
let retiredValues = retiredSpec->Option.flatMap(r => r.values)
|
|
280
|
+
// The same class of "works, but scans" mistake as the owner warning above,
|
|
281
|
+
// and the retirement case degrades the same way: the FilterExpression is
|
|
282
|
+
// applied after the page is read, so pages shrink as the archive's share of
|
|
283
|
+
// the table grows.
|
|
284
|
+
//
|
|
285
|
+
// `@scan` deliberately does not satisfy this. It adds no index and removes
|
|
286
|
+
// no read unit — it only widens the client's filter surface — so accepting it
|
|
287
|
+
// here would make the warning dismissible by an annotation that changes
|
|
288
|
+
// nothing about the cost being warned about.
|
|
289
|
+
switch retiredField {
|
|
290
|
+
| Some(f) =>
|
|
291
|
+
if !isIndexed(f) {
|
|
292
|
+
log.warn(
|
|
293
|
+
~comp="QueryDbResolvers_AppSync",
|
|
294
|
+
`${name}: @retired field "${f}" is not the key of any index on this table. ` ++
|
|
295
|
+
"Reads that exclude retired rows will Scan and filter, so pages shrink as " ++
|
|
296
|
+
"the archive's share of the rows grows. Add an @index on that field before " ++
|
|
297
|
+
"this read model grows.",
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
| None => ()
|
|
301
|
+
}
|
|
274
302
|
let resolverAll = makeQueryResolver(
|
|
275
303
|
~resolverName=fieldNameForAll->String.capitalize,
|
|
276
304
|
~field=fieldNameForAll->Pulumi.Input.make,
|
|
@@ -283,6 +311,8 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
283
311
|
~requireAttribute?,
|
|
284
312
|
~ownerField?,
|
|
285
313
|
~elevatedGroups=Reventless.OwnerScope.elevatedGroups(),
|
|
314
|
+
~retiredField?,
|
|
315
|
+
~retiredValues?,
|
|
286
316
|
)
|
|
287
317
|
} else {
|
|
288
318
|
Resolver.Functions.listAllItems
|
|
@@ -9,6 +9,7 @@ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types
|
|
|
9
9
|
import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
|
|
10
10
|
import * as AppSync_Function$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Function.res.mjs";
|
|
11
11
|
import * as Util_AppSync$ReventlessAws from "../../util/Util_AppSync.res.mjs";
|
|
12
|
+
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
12
13
|
import * as Util_DynamoDb$ReventlessAws from "../../util/Util_DynamoDb.res.mjs";
|
|
13
14
|
import * as Util_QueryDb$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_QueryDb.res.mjs";
|
|
14
15
|
import * as AppSync_DataSource$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_DataSource.res.mjs";
|
|
@@ -111,13 +112,23 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
111
112
|
let requireAttribute = internalRowRequiredAttr(name$1);
|
|
112
113
|
let ownerField = stateSchemaOpt !== undefined ? Owner$Reventless.fieldNames(stateSchemaOpt)[0] : undefined;
|
|
113
114
|
OwnerScopeDiagnostics$ReventlessCore.warnIfNoElevatedGroups("QueryDbResolvers_AppSync", name$1, ownerField);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
let isIndexed = f => {
|
|
116
|
+
if (indexes.some(ic => Stdlib_Option.getOr(ic.idField, ic.index) === f)) {
|
|
117
|
+
return true;
|
|
118
|
+
} else {
|
|
119
|
+
return Stdlib_Option.getOr(subIdField, "") === f;
|
|
118
120
|
}
|
|
121
|
+
};
|
|
122
|
+
if (ownerField !== undefined && !isIndexed(ownerField)) {
|
|
123
|
+
log.warn("QueryDbResolvers_AppSync", undefined, name$1 + `: @owner field "` + ownerField + `" is not the key of any index on this table. ` + "Owner-scoped reads will Scan and filter, so pages shrink as the caller's share of the rows falls. Add an @index on that field before this read model grows.");
|
|
124
|
+
}
|
|
125
|
+
let retiredSpec = Stdlib_Option.flatMap(Stdlib_Option.flatMap(stateSchemaOpt, StateAnnotations$Reventless.getSpec), spec => spec.retired);
|
|
126
|
+
let retiredField = Stdlib_Option.map(retiredSpec, r => r.field);
|
|
127
|
+
let retiredValues = Stdlib_Option.flatMap(retiredSpec, r => r.values);
|
|
128
|
+
if (retiredField !== undefined && !isIndexed(retiredField)) {
|
|
129
|
+
log.warn("QueryDbResolvers_AppSync", undefined, name$1 + `: @retired field "` + retiredField + `" is not the key of any index on this table. ` + "Reads that exclude retired rows will Scan and filter, so pages shrink as the archive's share of the rows grows. Add an @index on that field before this read model grows.");
|
|
119
130
|
}
|
|
120
|
-
let resolverAll = makeQueryResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, connectionSpec ? AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection(labelField, filterFieldNames, rangeFieldNames, sortFieldNames, requireAttribute, ownerField, OwnerScope$Reventless.elevatedGroups()) : AppSync_Resolver_Functions$PulumiAws.listAllItems);
|
|
131
|
+
let resolverAll = makeQueryResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, connectionSpec ? AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection(labelField, filterFieldNames, rangeFieldNames, sortFieldNames, requireAttribute, ownerField, OwnerScope$Reventless.elevatedGroups(), retiredField, retiredValues) : AppSync_Resolver_Functions$PulumiAws.listAllItems);
|
|
121
132
|
let resolversByIndex = indexes.map(indexConfig => {
|
|
122
133
|
let index = indexConfig.index;
|
|
123
134
|
let stripLeadingBy = s => {
|
|
@@ -153,6 +153,16 @@ let registerBinding = (
|
|
|
153
153
|
// From the same schema `capability` is derived from, one line above, so the
|
|
154
154
|
// two cannot end up disagreeing about this read model's fields.
|
|
155
155
|
ownerField: Reventless.Owner.fieldNames(spec.stateSchema)->Array.get(0),
|
|
156
|
+
retiredField: spec.stateSchema
|
|
157
|
+
->Reventless.StateAnnotations.getSpec
|
|
158
|
+
->Option.flatMap(a => a.retired)
|
|
159
|
+
->Option.map(r => r.field),
|
|
160
|
+
// The state form's other half. Carried beside the field rather than
|
|
161
|
+
// re-derived at predicate time, so one place decides which rows are retired.
|
|
162
|
+
retiredValues: spec.stateSchema
|
|
163
|
+
->Reventless.StateAnnotations.getSpec
|
|
164
|
+
->Option.flatMap(a => a.retired)
|
|
165
|
+
->Option.flatMap(r => r.values),
|
|
156
166
|
},
|
|
157
167
|
)
|
|
158
168
|
logDebug("registered resolver binding for " ++ entry.readModelName, {comp: "PgQueryResolver"})
|
|
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
6
6
|
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
7
7
|
import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
|
|
8
8
|
import * as HandlerFactoryHelpersMjs from "./HandlerFactoryHelpers.mjs";
|
|
9
|
+
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
9
10
|
import * as PgQueryResolver_Lambda$ReventlessAws from "../QueryDb/PgQueryResolver_Lambda.res.mjs";
|
|
10
11
|
import * as ProjectionEntryPoint_Ops$ReventlessAws from "./ProjectionEntryPoint_Ops.res.mjs";
|
|
11
12
|
import * as QueryEnginePostgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/QueryEnginePostgres.res.mjs";
|
|
@@ -74,7 +75,9 @@ function registerBinding(pushdowns, pgConnection, entry, spec) {
|
|
|
74
75
|
labelField: entry.labelField,
|
|
75
76
|
includeIdParam: entry.includeIdParam,
|
|
76
77
|
authorization: spec.authorization,
|
|
77
|
-
ownerField: Owner$Reventless.fieldNames(spec.stateSchema)[0]
|
|
78
|
+
ownerField: Owner$Reventless.fieldNames(spec.stateSchema)[0],
|
|
79
|
+
retiredField: Stdlib_Option.map(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.field),
|
|
80
|
+
retiredValues: Stdlib_Option.flatMap(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.values)
|
|
78
81
|
});
|
|
79
82
|
HandlerFactoryHelpersMjs.log.debug("registered resolver binding for " + entry.readModelName, {
|
|
80
83
|
comp: "PgQueryResolver"
|
|
@@ -98,14 +98,43 @@ export function nextSequence() {
|
|
|
98
98
|
// implementations. Over the size cap the state is dropped and the downgrade logged:
|
|
99
99
|
// a metadata-only descriptor still tells the client to refetch, where a publish
|
|
100
100
|
// rejected for size would tell it nothing.
|
|
101
|
-
|
|
101
|
+
//
|
|
102
|
+
// `retiredField` names the row's `@retired` flag, when the read model declares
|
|
103
|
+
// one. A row whose flag is true publishes as metadata only — no state, and no
|
|
104
|
+
// sortKeyValue either, since that is a timestamp off a row the subscriber may
|
|
105
|
+
// not read. The channel is shared by every subscriber of the view and cannot be
|
|
106
|
+
// scoped per caller, so a payload here would hand the row to exactly the callers
|
|
107
|
+
// the resolvers refuse it to. `Updated` with no state is the shape an oversized
|
|
108
|
+
// row already takes, and it asks the client to do the one thing that enforces
|
|
109
|
+
// the rule: refetch, and let the query layer answer.
|
|
110
|
+
//
|
|
111
|
+
// `retiredValues` is the state form: the row is retired when the field holds any
|
|
112
|
+
// of those states. Absent is the boolean form, where the flag is `true`. This
|
|
113
|
+
// mirrors `OwnerScope.isRetiredValue`, which the other two implementations call
|
|
114
|
+
// — this one cannot, being hand-written JS outside the ReScript graph, so the
|
|
115
|
+
// three are held together by StateChangeDescriptorParityTest instead. The strict
|
|
116
|
+
// readings matter and are the ones that file asserts: a non-boolean is not
|
|
117
|
+
// `true`, a non-string is in no set, and an absent field is neither.
|
|
118
|
+
export function makeDescriptor({ changeKind, entityKey, state, seq, retiredField, retiredValues }) {
|
|
102
119
|
const descriptor = { changeKind, id: entityKey };
|
|
103
|
-
|
|
120
|
+
const cell =
|
|
121
|
+
retiredField !== undefined &&
|
|
122
|
+
retiredField !== null &&
|
|
123
|
+
state !== undefined &&
|
|
124
|
+
state !== null &&
|
|
125
|
+
typeof state === "object"
|
|
126
|
+
? state[retiredField]
|
|
127
|
+
: undefined;
|
|
128
|
+
const retired =
|
|
129
|
+
retiredValues === undefined || retiredValues === null
|
|
130
|
+
? cell === true
|
|
131
|
+
: typeof cell === "string" && retiredValues.indexOf(cell) >= 0;
|
|
132
|
+
if (state !== undefined && !retired) {
|
|
104
133
|
const sortKeyValue = pickSortKeyValue(state);
|
|
105
134
|
if (sortKeyValue !== undefined) descriptor.sortKeyValue = sortKeyValue;
|
|
106
135
|
}
|
|
107
136
|
descriptor.seq = seq;
|
|
108
|
-
if (state !== undefined) {
|
|
137
|
+
if (state !== undefined && !retired) {
|
|
109
138
|
const encoded = JSON.stringify(state);
|
|
110
139
|
if (encoded.length <= MAX_STATE_CHARS) {
|
|
111
140
|
descriptor.state = state;
|
|
@@ -126,12 +155,16 @@ export function makeDescriptor({ changeKind, entityKey, state, seq }) {
|
|
|
126
155
|
// - changeKind: "Updated" (save) | "Removed" (delete).
|
|
127
156
|
// - state: the full new row for a save; omitted for a delete.
|
|
128
157
|
// - dedupeId: AppSync publish `id` (idempotency hint).
|
|
129
|
-
|
|
158
|
+
// - retiredField: the row's `@retired` flag name, when the read model declares
|
|
159
|
+
// one; a retired row publishes metadata-only.
|
|
160
|
+
// - retiredValues: the states that retire the row, for the state form. Absent is
|
|
161
|
+
// the boolean form, where the flag is `true`.
|
|
162
|
+
export async function publishStateChange({ endpoint, region, topicName, entityKey, changeKind, state, dedupeId, retiredField, retiredValues }) {
|
|
130
163
|
if (!endpoint || !topicName) return; // not live-enabled — no-op
|
|
131
164
|
try {
|
|
132
165
|
const url = new URL(endpoint);
|
|
133
166
|
const channel = "/default/" + pathSegment(topicName) + "/" + pathSegment(entityKey);
|
|
134
|
-
const descriptor = makeDescriptor({ changeKind, entityKey, state, seq: nextSequence() });
|
|
167
|
+
const descriptor = makeDescriptor({ changeKind, entityKey, state, seq: nextSequence(), retiredField, retiredValues });
|
|
135
168
|
const body = JSON.stringify({
|
|
136
169
|
id: dedupeId || (topicName + ":" + entityKey + ":" + changeKind),
|
|
137
170
|
channel,
|
|
@@ -92,6 +92,10 @@ type streamEntry = {
|
|
|
92
92
|
tableName: Pulumi.Output.t<string>,
|
|
93
93
|
streamArn: Pulumi.Output.t<string>,
|
|
94
94
|
topicName: string,
|
|
95
|
+
// The read model's `@retired` field, resolved at deploy time because the relay
|
|
96
|
+
// Lambda has no plugin registry in process to resolve it at request time.
|
|
97
|
+
retiredField: option<string>,
|
|
98
|
+
retiredValues: option<array<string>>,
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
// One registry per events API, keyed by `eventsApi.name` (the static Pulumi
|
|
@@ -134,6 +138,8 @@ let makeForTable = (
|
|
|
134
138
|
~streamArn: Pulumi.Output.t<string>,
|
|
135
139
|
~partitionKeyName: Pulumi.Output.t<string>,
|
|
136
140
|
~topicName: string,
|
|
141
|
+
~retiredField: option<string>=?,
|
|
142
|
+
~retiredValues: option<array<string>>=?,
|
|
137
143
|
~eventsApi: AppSync_EventsApi.t,
|
|
138
144
|
~opts as _: Pulumi.CustomResourceOptions.t,
|
|
139
145
|
) => {
|
|
@@ -150,7 +156,12 @@ let makeForTable = (
|
|
|
150
156
|
|
|
151
157
|
let key = eventsApi.name
|
|
152
158
|
let entries = registry->Dict.get(key)->Option.getOr([])
|
|
153
|
-
registry->Dict.set(
|
|
159
|
+
registry->Dict.set(
|
|
160
|
+
key,
|
|
161
|
+
entries->Array.concat([
|
|
162
|
+
{tableName: checkedTableName, streamArn, topicName, retiredField, retiredValues},
|
|
163
|
+
]),
|
|
164
|
+
)
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
let make = (
|
|
@@ -176,6 +187,18 @@ let make = (
|
|
|
176
187
|
// `makeForTable` and can be keyed anything.
|
|
177
188
|
~partitionKeyName=StateTopic_AppSync_Helpers.entityKeyPartitionAttribute->Pulumi.Output.make,
|
|
178
189
|
~topicName,
|
|
190
|
+
// Resolved here, where the read model's name is known and its spec is
|
|
191
|
+
// registered in this process. The relay Lambda has neither.
|
|
192
|
+
~retiredField=?ReventlessCore.Plugin_Helpers.stateSchemaRegistry
|
|
193
|
+
->Dict.get(readModelName)
|
|
194
|
+
->Option.flatMap(Reventless.StateAnnotations.getSpec)
|
|
195
|
+
->Option.flatMap(spec => spec.retired)
|
|
196
|
+
->Option.map(r => r.field),
|
|
197
|
+
~retiredValues=?ReventlessCore.Plugin_Helpers.stateSchemaRegistry
|
|
198
|
+
->Dict.get(readModelName)
|
|
199
|
+
->Option.flatMap(Reventless.StateAnnotations.getSpec)
|
|
200
|
+
->Option.flatMap(spec => spec.retired)
|
|
201
|
+
->Option.flatMap(r => r.values),
|
|
179
202
|
~eventsApi,
|
|
180
203
|
~opts,
|
|
181
204
|
)
|
|
@@ -270,6 +293,30 @@ let finish = (
|
|
|
270
293
|
dict->JSON.Encode.object->JSON.stringify
|
|
271
294
|
})
|
|
272
295
|
|
|
296
|
+
// STATE_RETIRED_MAP env var — `{ <tableName>: {field, values?} }`, carrying
|
|
297
|
+
// only the tables that declare retirement. Built from the same awaited table
|
|
298
|
+
// names as the topic map so the two cannot describe different sets of tables.
|
|
299
|
+
// The whole predicate travels: the relay Lambda has no plugin registry, so a
|
|
300
|
+
// field without its states would leave it unable to tell the two forms apart.
|
|
301
|
+
let retiredMapJson =
|
|
302
|
+
entries
|
|
303
|
+
->Array.map(e => e.tableName)
|
|
304
|
+
->Pulumi.Output.all
|
|
305
|
+
->Pulumi.Output.apply(tableNames => {
|
|
306
|
+
let dict = Dict.make()
|
|
307
|
+
tableNames->Array.forEachWithIndex((tableName, i) => {
|
|
308
|
+
let entry = entries->Array.getUnsafe(i)
|
|
309
|
+
entry.retiredField->Option.forEach(f => {
|
|
310
|
+
let obj = Dict.fromArray([("field", f->JSON.Encode.string)])
|
|
311
|
+
entry.retiredValues->Option.forEach(vs =>
|
|
312
|
+
obj->Dict.set("values", vs->Array.map(JSON.Encode.string)->JSON.Encode.array)
|
|
313
|
+
)
|
|
314
|
+
dict->Dict.set(tableName, obj->JSON.Encode.object)
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
dict->JSON.Encode.object->JSON.stringify
|
|
318
|
+
})
|
|
319
|
+
|
|
273
320
|
// Shared Lambda — the compiled `_Ops` handler is identical for every
|
|
274
321
|
// stream-enabled RM; routing is per-record via STATE_TOPIC_MAP env var.
|
|
275
322
|
// Bundle reventless-aws (the handler + its node:crypto signer + the
|
|
@@ -325,6 +372,7 @@ let finish = (
|
|
|
325
372
|
("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
|
|
326
373
|
("APPSYNC_ENDPOINT", appsyncEndpoint->Pulumi.Output.asInput),
|
|
327
374
|
("STATE_TOPIC_MAP", topicMapJson->Pulumi.Output.asInput),
|
|
375
|
+
("STATE_RETIRED_MAP", retiredMapJson->Pulumi.Output.asInput),
|
|
328
376
|
("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
|
|
329
377
|
("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
|
|
330
378
|
Util_LambdaLogging.logLevelEntry(),
|
|
@@ -10,6 +10,8 @@ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
|
10
10
|
import * as QueryDb$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb.res.mjs";
|
|
11
11
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
12
12
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
13
|
+
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
14
|
+
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
13
15
|
import * as Util_ReadModel$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_ReadModel.res.mjs";
|
|
14
16
|
import * as AppSync_EventsApi$ReventlessAws from "../Api/AppSync_EventsApi.res.mjs";
|
|
15
17
|
import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
|
|
@@ -18,7 +20,7 @@ import * as StateTopic_AppSync_Helpers$ReventlessAws from "./StateTopic_AppSync_
|
|
|
18
20
|
|
|
19
21
|
let registry = {};
|
|
20
22
|
|
|
21
|
-
function makeForTable(tableName, streamArn, partitionKeyName, topicName, eventsApi, param) {
|
|
23
|
+
function makeForTable(tableName, streamArn, partitionKeyName, topicName, retiredField, retiredValues, eventsApi, param) {
|
|
22
24
|
let checkedTableName = Pulumi.all([
|
|
23
25
|
tableName,
|
|
24
26
|
partitionKeyName
|
|
@@ -32,13 +34,15 @@ function makeForTable(tableName, streamArn, partitionKeyName, topicName, eventsA
|
|
|
32
34
|
registry[key] = entries.concat([{
|
|
33
35
|
tableName: checkedTableName,
|
|
34
36
|
streamArn: streamArn,
|
|
35
|
-
topicName: topicName
|
|
37
|
+
topicName: topicName,
|
|
38
|
+
retiredField: retiredField,
|
|
39
|
+
retiredValues: retiredValues
|
|
36
40
|
}]);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
function make(readModelName, topicName, allQueryDbs, eventsApi, opts) {
|
|
40
44
|
let streamResource = Util_DynamoDbStream$ReventlessAws.findResource(Util_ReadModel$ReventlessCore.queryDbStorageResources(allQueryDbs, readModelName));
|
|
41
|
-
makeForTable(streamResource.name, Util_DynamoDbStream$ReventlessAws.streamArnFromDynamoDbTableResource(streamResource), Pulumi.output(StateTopic_AppSync_Helpers$ReventlessAws.entityKeyPartitionAttribute), topicName, eventsApi, opts);
|
|
45
|
+
makeForTable(streamResource.name, Util_DynamoDbStream$ReventlessAws.streamArnFromDynamoDbTableResource(streamResource), Pulumi.output(StateTopic_AppSync_Helpers$ReventlessAws.entityKeyPartitionAttribute), topicName, Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Plugin_Helpers$ReventlessCore.stateSchemaRegistry[readModelName], StateAnnotations$Reventless.getSpec), spec => spec.retired), r => r.field), Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Plugin_Helpers$ReventlessCore.stateSchemaRegistry[readModelName], StateAnnotations$Reventless.getSpec), spec => spec.retired), r => r.values), eventsApi, opts);
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
function finish(eventsApi, opts) {
|
|
@@ -94,6 +98,23 @@ function finish(eventsApi, opts) {
|
|
|
94
98
|
});
|
|
95
99
|
return JSON.stringify(dict);
|
|
96
100
|
});
|
|
101
|
+
let retiredMapJson = Pulumi.all(entries.map(e => e.tableName)).apply(tableNames => {
|
|
102
|
+
let dict = {};
|
|
103
|
+
tableNames.forEach((tableName, i) => {
|
|
104
|
+
let entry = entries[i];
|
|
105
|
+
Stdlib_Option.forEach(entry.retiredField, f => {
|
|
106
|
+
let obj = Object.fromEntries([[
|
|
107
|
+
"field",
|
|
108
|
+
f
|
|
109
|
+
]]);
|
|
110
|
+
Stdlib_Option.forEach(entry.retiredValues, vs => {
|
|
111
|
+
obj["values"] = vs.map(prim => prim);
|
|
112
|
+
});
|
|
113
|
+
dict[tableName] = obj;
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
return JSON.stringify(dict);
|
|
117
|
+
});
|
|
97
118
|
let packageDirs = Object.fromEntries([[
|
|
98
119
|
"@reventlessdev/reventless-aws",
|
|
99
120
|
Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
|
|
@@ -125,6 +146,10 @@ function finish(eventsApi, opts) {
|
|
|
125
146
|
"STATE_TOPIC_MAP",
|
|
126
147
|
topicMapJson
|
|
127
148
|
],
|
|
149
|
+
[
|
|
150
|
+
"STATE_RETIRED_MAP",
|
|
151
|
+
retiredMapJson
|
|
152
|
+
],
|
|
128
153
|
[
|
|
129
154
|
"NODE_OPTIONS",
|
|
130
155
|
Util_Bundle$ReventlessAws.esmLoaderNodeOptions
|
|
@@ -53,6 +53,55 @@ type event = {@as("Records") records: array<record>}
|
|
|
53
53
|
|
|
54
54
|
// ── Per-record derivations (ports of the former inline JS helpers) ───────────
|
|
55
55
|
|
|
56
|
+
// STATE_RETIRED_MAP: `{ <tableName>: {field, values?} }`, injected at deploy time
|
|
57
|
+
// beside STATE_TOPIC_MAP. This handler runs with no plugin registry in process —
|
|
58
|
+
// there is nothing here that could read a state schema — so the whole predicate
|
|
59
|
+
// has to arrive the same way the topic routing does. A table absent from the map
|
|
60
|
+
// declares no retirement, which is every table until one does.
|
|
61
|
+
//
|
|
62
|
+
// `values` absent is the boolean form. Carried as an object rather than as an
|
|
63
|
+
// encoded string because a `field=Value` convention is a parser this side and a
|
|
64
|
+
// writer the other, and the two drift the first time a state name contains the
|
|
65
|
+
// separator — an argument that only gets stronger now that a lifecycle can name
|
|
66
|
+
// several states.
|
|
67
|
+
let retiredMap: dict<Reventless.OwnerScope.retiredScope> =
|
|
68
|
+
switch NodeProcess.env
|
|
69
|
+
->Dict.get("STATE_RETIRED_MAP")
|
|
70
|
+
->Option.getOr("{}")
|
|
71
|
+
->JSON.parseOrThrow
|
|
72
|
+
->JSON.Decode.object {
|
|
73
|
+
| Some(d) =>
|
|
74
|
+
let out = Dict.make()
|
|
75
|
+
d->Dict.forEachWithKey((v, k) =>
|
|
76
|
+
v
|
|
77
|
+
->JSON.Decode.object
|
|
78
|
+
->Option.flatMap(o =>
|
|
79
|
+
o
|
|
80
|
+
->Dict.get("field")
|
|
81
|
+
->Option.flatMap(JSON.Decode.string)
|
|
82
|
+
->Option.map(field => {
|
|
83
|
+
Reventless.OwnerScope.field,
|
|
84
|
+
values: o
|
|
85
|
+
->Dict.get("values")
|
|
86
|
+
->Option.flatMap(JSON.Decode.array)
|
|
87
|
+
->Option.map(vs => vs->Array.filterMap(JSON.Decode.string)),
|
|
88
|
+
})
|
|
89
|
+
)
|
|
90
|
+
->Option.forEach(scope => out->Dict.set(k, scope))
|
|
91
|
+
)
|
|
92
|
+
out
|
|
93
|
+
| None => Dict.make()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let tableNameFromEventSourceArn = (arn: string): option<string> => {
|
|
97
|
+
let parts = arn->String.split("/")
|
|
98
|
+
switch (parts->Array.get(0), parts->Array.get(1), parts->Array.get(2)) {
|
|
99
|
+
| (Some(prefix), Some(tableName), Some("stream")) if prefix->String.endsWith(":table") =>
|
|
100
|
+
Some(tableName)
|
|
101
|
+
| _ => None
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
// Map record.eventSourceARN → topicRoot via STATE_TOPIC_MAP. Stream ARNs look
|
|
57
106
|
// like arn:aws:dynamodb:<region>:<acct>:table/<TableName>/stream/<ts>; split on
|
|
58
107
|
// "/" gives [ "...:table", "<TableName>", "stream", "<ts>" ].
|
|
@@ -131,18 +180,34 @@ let makeDescriptor = (
|
|
|
131
180
|
~entityKey: string,
|
|
132
181
|
~image: dict<JSON.t>,
|
|
133
182
|
~seq: option<string>,
|
|
183
|
+
~retiredField: option<string>=?,
|
|
184
|
+
~retiredValues: option<array<string>>=?,
|
|
134
185
|
): JSON.t => {
|
|
135
186
|
let removed = changeKind == "Removed"
|
|
187
|
+
// A retired row publishes as metadata only, for the reason the other two
|
|
188
|
+
// implementations do it: this channel reaches every subscriber of the view,
|
|
189
|
+
// and a payload would deliver the row to the callers the resolvers refuse it
|
|
190
|
+
// to. `Updated` with no state is the shape an oversized row already takes.
|
|
191
|
+
// Both forms are the one question `isRetiredValue` answers, so nothing here
|
|
192
|
+
// branches on which the view declared.
|
|
193
|
+
let retired = switch retiredField {
|
|
194
|
+
| Some(field) =>
|
|
195
|
+
!removed &&
|
|
196
|
+
{Reventless.OwnerScope.field, values: retiredValues}->Reventless.OwnerScope.isRetiredValue(
|
|
197
|
+
image->Dict.get(field),
|
|
198
|
+
)
|
|
199
|
+
| None => false
|
|
200
|
+
}
|
|
136
201
|
let descriptor = Dict.make()
|
|
137
202
|
descriptor->Dict.set("changeKind", JSON.Encode.string(changeKind))
|
|
138
203
|
descriptor->Dict.set("id", JSON.Encode.string(entityKey))
|
|
139
|
-
if !removed {
|
|
204
|
+
if !removed && !retired {
|
|
140
205
|
pickSortKeyValue(image)->Option.forEach(v =>
|
|
141
206
|
descriptor->Dict.set("sortKeyValue", JSON.Encode.string(v))
|
|
142
207
|
)
|
|
143
208
|
}
|
|
144
209
|
seq->Option.forEach(s => descriptor->Dict.set("seq", JSON.Encode.string(s)))
|
|
145
|
-
if !removed {
|
|
210
|
+
if !removed && !retired {
|
|
146
211
|
let state = image->JSON.Encode.object
|
|
147
212
|
let encoded = state->JSON.stringify
|
|
148
213
|
if encoded->String.length <= maxStateChars {
|
|
@@ -191,6 +256,12 @@ let processRecord = async (
|
|
|
191
256
|
~entityKey,
|
|
192
257
|
~image=unmarshalled,
|
|
193
258
|
~seq=dynamodb.sequenceNumber,
|
|
259
|
+
~retiredField=?tableNameFromEventSourceArn(record.eventSourceARN)
|
|
260
|
+
->Option.flatMap(t => retiredMap->Dict.get(t))
|
|
261
|
+
->Option.map(scope => scope.Reventless.OwnerScope.field),
|
|
262
|
+
~retiredValues=?tableNameFromEventSourceArn(record.eventSourceARN)
|
|
263
|
+
->Option.flatMap(t => retiredMap->Dict.get(t))
|
|
264
|
+
->Option.flatMap(scope => scope.Reventless.OwnerScope.values),
|
|
194
265
|
)
|
|
195
266
|
let body =
|
|
196
267
|
Dict.fromArray([
|