@reventlessdev/reventless-aws 3.0.0-alpha.302 → 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.
@@ -113,6 +113,71 @@ describe("toEntryWith heals a structure persisted before a required list existed
113
113
  })
114
114
  })
115
115
 
116
+ // The `statusField` → `lifecycleField` rename has the same shape of problem one
117
+ // severity down: the SDL field is a nullable `String`, so an absent key answers
118
+ // null rather than blackholing the query. That is worse to find, not better —
119
+ // lists keep rendering while command-menu filtering, board columns, group
120
+ // sections, progress trackers and state diagrams all go quiet. Asserted because
121
+ // the shim is the one part of the rename with nothing to fail at compile time.
122
+ describe("toEntryWith reads a pre-rename structure's lifecycle field", () => {
123
+ let legacyStructure = Dict.fromArray([
124
+ (
125
+ "readModels",
126
+ JSON.Encode.array([
127
+ JSON.parseOrThrow(`{"name": "Customers", "statusField": "locationStatus"}`),
128
+ ]),
129
+ ),
130
+ (
131
+ "stateViewSlices",
132
+ JSON.Encode.array([JSON.parseOrThrow(`{"name": "Orders", "statusField": "status"}`)]),
133
+ ),
134
+ ])->JSON.Encode.object
135
+
136
+ let fieldOf = (~collection) =>
137
+ entry(~filter=false, legacyStructure)
138
+ ->Option.map(members(_, collection))
139
+ ->Option.flatMap(Array.get(_, 0))
140
+ ->Option.flatMap(c => c->Dict.get("lifecycleField"))
141
+
142
+ testSync("a read model's legacy key answers on the new name", () =>
143
+ expect(fieldOf(~collection="readModels"))->toEqual(
144
+ Some(JSON.Encode.string("locationStatus")),
145
+ )
146
+ )
147
+
148
+ testSync("so does a state view slice's", () =>
149
+ expect(fieldOf(~collection="stateViewSlices"))->toEqual(Some(JSON.Encode.string("status")))
150
+ )
151
+
152
+ testSync("a post-rename structure is left alone", () => {
153
+ let current = Dict.fromArray([
154
+ (
155
+ "readModels",
156
+ JSON.Encode.array([
157
+ JSON.parseOrThrow(`{"name": "Orders", "lifecycleField": "lifecycle", "statusField": "stale"}`),
158
+ ]),
159
+ ),
160
+ ])->JSON.Encode.object
161
+ let field =
162
+ entry(~filter=false, current)
163
+ ->Option.map(members(_, "readModels"))
164
+ ->Option.flatMap(Array.get(_, 0))
165
+ ->Option.flatMap(c => c->Dict.get("lifecycleField"))
166
+ expect(field)->toEqual(Some(JSON.Encode.string("lifecycle")))
167
+ })
168
+
169
+ testSync("a structure declaring neither still answers nothing", () => {
170
+ let neither = Dict.fromArray([
171
+ ("readModels", JSON.Encode.array([JSON.parseOrThrow(`{"name": "Products"}`)])),
172
+ ])->JSON.Encode.object
173
+ let component =
174
+ entry(~filter=false, neither)
175
+ ->Option.map(members(_, "readModels"))
176
+ ->Option.flatMap(Array.get(_, 0))
177
+ expect(component->Option.flatMap(c => c->Dict.get("lifecycleField")))->toEqual(None)
178
+ })
179
+ })
180
+
116
181
  // The persisted structure is pre-filter, so this handler is a twin of the
117
182
  // in-memory `encodePluginStructureEntry` rather than a consequence of it. A shell
118
183
  // reading `internalQueryables` against a platform that emits it on only one
@@ -67,6 +67,42 @@ globalThis.describe("toEntryWith heals a structure persisted before a required l
67
67
  });
68
68
  });
69
69
 
70
+ globalThis.describe("toEntryWith reads a pre-rename structure's lifecycle field", () => {
71
+ let legacyStructure = Object.fromEntries([
72
+ [
73
+ "readModels",
74
+ [JSON.parse(`{"name": "Customers", "statusField": "locationStatus"}`)]
75
+ ],
76
+ [
77
+ "stateViewSlices",
78
+ [JSON.parse(`{"name": "Orders", "statusField": "status"}`)]
79
+ ]
80
+ ]);
81
+ let fieldOf = collection => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.map(entry(false, legacyStructure), __x => members(__x, collection)), __x => __x[0]), c => c["lifecycleField"]);
82
+ globalThis.test("a read model's legacy key answers on the new name", () => {
83
+ globalThis.expect(fieldOf("readModels")).toEqual("locationStatus");
84
+ });
85
+ globalThis.test("so does a state view slice's", () => {
86
+ globalThis.expect(fieldOf("stateViewSlices")).toEqual("status");
87
+ });
88
+ globalThis.test("a post-rename structure is left alone", () => {
89
+ let current = Object.fromEntries([[
90
+ "readModels",
91
+ [JSON.parse(`{"name": "Orders", "lifecycleField": "lifecycle", "statusField": "stale"}`)]
92
+ ]]);
93
+ let field = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.map(entry(false, current), __x => members(__x, "readModels")), __x => __x[0]), c => c["lifecycleField"]);
94
+ globalThis.expect(field).toEqual("lifecycle");
95
+ });
96
+ globalThis.test("a structure declaring neither still answers nothing", () => {
97
+ let neither = Object.fromEntries([[
98
+ "readModels",
99
+ [JSON.parse(`{"name": "Products"}`)]
100
+ ]]);
101
+ let component = Stdlib_Option.flatMap(Stdlib_Option.map(entry(false, neither), __x => members(__x, "readModels")), __x => __x[0]);
102
+ globalThis.expect(Stdlib_Option.flatMap(component, c => c["lifecycleField"])).toEqual(undefined);
103
+ });
104
+ });
105
+
70
106
  globalThis.describe("toEntryWith carries Internal queryables on the filtered field", () => {
71
107
  let mixed = Object.fromEntries([
72
108
  [
@@ -19,18 +19,18 @@ globalThis.describe("QueryDbResolvers_AppSync.internalRowRequiredAttr", () => {
19
19
 
20
20
  globalThis.describe("AppSync_Resolver_Retrying.Functions.listAllItemsConnection", () => {
21
21
  globalThis.test("with ~requireAttribute emits an attribute_exists filter that excludes internal rows", () => {
22
- let code = AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection("name", undefined, undefined, undefined, "name", undefined, undefined);
22
+ let code = AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection("name", undefined, undefined, undefined, "name", undefined, undefined, undefined, undefined);
23
23
  globalThis.expect(code.includes("attribute_exists(#name)")).toBe(true);
24
24
  globalThis.expect(code.includes("names['#name'] = 'name'")).toBe(true);
25
25
  });
26
26
  globalThis.test("without ~requireAttribute emits no attribute_exists filter (default read models unchanged)", () => {
27
- let code = AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection("name", undefined, undefined, undefined, undefined, undefined, undefined);
27
+ let code = AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection("name", undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
28
28
  globalThis.expect(code.includes("attribute_exists")).toBe(false);
29
29
  });
30
30
  });
31
31
 
32
32
  globalThis.describe("listAllItemsConnection — Scan cursor round-trip (paging fixes 1–3)", () => {
33
- let code = AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection("name", undefined, undefined, undefined, undefined, undefined, undefined);
33
+ let code = AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection("name", undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
34
34
  globalThis.test("Fix 1: response encodes the real nextToken as the cursor", () => {
35
35
  globalThis.expect(code.includes("const next = ctx.result?.nextToken ?? null;")).toBe(true);
36
36
  globalThis.expect(code.includes("util.base64Encode(JSON.stringify({ token: next, index: i }))")).toBe(true);
@@ -29,7 +29,7 @@ describe("state-change descriptor parity", () => {
29
29
  testAsync("every implementation builds the same descriptor", async () => {
30
30
  let cases = await buildAll()
31
31
  // A silent zero-case run would pass while asserting nothing.
32
- expect(cases->Array.length)->toBe(6)
32
+ expect(cases->Array.length)->toBe(11)
33
33
  cases->Array.forEach(c => {
34
34
  expect((c.name, c.relay))->toEqual((c.name, c.local))
35
35
  expect((c.name, c.postgres))->toEqual((c.name, c.local))
@@ -55,4 +55,85 @@ describe("state-change descriptor parity", () => {
55
55
  | None => expect("oversized case missing")->toBe("present")
56
56
  }
57
57
  })
58
+
59
+ // The live channel is the third place a retired row could reach a caller the
60
+ // resolvers refuse it to, and the only one that cannot be scoped per
61
+ // subscriber — the channel is keyed by view and entity, and everyone watching
62
+ // receives the frame. So the payload has to go, in all three implementations.
63
+ testAsync("a retired row publishes as metadata only, everywhere", async () => {
64
+ let cases = await buildAll()
65
+ let keyOf = (d: JSON.t, k) => d->JSON.Decode.object->Option.flatMap(o => o->Dict.get(k))
66
+ let byName = name => cases->Array.find(c => c.name == name)
67
+
68
+ switch byName("retired row degrades to metadata only") {
69
+ | Some(c) =>
70
+ // Not just the payload: the sort value is a timestamp off a row the
71
+ // subscriber may not read, and this row carries one.
72
+ expect((keyOf(c.local, "state"), keyOf(c.local, "sortKeyValue")))->toEqual((None, None))
73
+ expect((keyOf(c.relay, "state"), keyOf(c.relay, "sortKeyValue")))->toEqual((None, None))
74
+ expect((keyOf(c.postgres, "state"), keyOf(c.postgres, "sortKeyValue")))->toEqual((None, None))
75
+ // `Updated`, never `Removed`: the row is not gone. Saying so would be
76
+ // false for an elevated subscriber reading with `includeRetired`, and
77
+ // false again the moment the row is restored.
78
+ expect(keyOf(c.local, "changeKind"))->toEqual(Some(JSON.Encode.string("Updated")))
79
+ | None => expect("retired case missing")->toBe("present")
80
+ }
81
+
82
+ // The control that separates "drops a retired row" from "drops any row on a
83
+ // view that declares the flag".
84
+ switch byName("live row with a retirement flag carries the row") {
85
+ | Some(c) =>
86
+ expect(keyOf(c.local, "state")->Option.isSome)->toBe(true)
87
+ expect(keyOf(c.relay, "state")->Option.isSome)->toBe(true)
88
+ expect(keyOf(c.postgres, "state")->Option.isSome)->toBe(true)
89
+ | None => expect("live-with-flag case missing")->toBe("present")
90
+ }
91
+ })
92
+
93
+ // The state form, where the predicate is membership rather than equality. Both
94
+ // states are asserted, so an implementation comparing against only the first
95
+ // member of the set fails on the second — which is the shape of mistake a set
96
+ // invites and a single value could not.
97
+ testAsync("a row in any retired state publishes as metadata only", async () => {
98
+ let cases = await buildAll()
99
+ let keyOf = (d: JSON.t, k) => d->JSON.Decode.object->Option.flatMap(o => o->Dict.get(k))
100
+ let byName = name => cases->Array.find(c => c.name == name)
101
+
102
+ [
103
+ "row in the first retired state degrades to metadata only",
104
+ "row in the second retired state degrades to metadata only",
105
+ ]->Array.forEach(name =>
106
+ switch byName(name) {
107
+ | Some(c) =>
108
+ expect((name, keyOf(c.local, "state"), keyOf(c.local, "sortKeyValue")))->toEqual((
109
+ name,
110
+ None,
111
+ None,
112
+ ))
113
+ expect((name, keyOf(c.relay, "state"), keyOf(c.relay, "sortKeyValue")))->toEqual((
114
+ name,
115
+ None,
116
+ None,
117
+ ))
118
+ expect((name, keyOf(c.postgres, "state"), keyOf(c.postgres, "sortKeyValue")))->toEqual((
119
+ name,
120
+ None,
121
+ None,
122
+ ))
123
+ | None => expect(name ++ " missing")->toBe("present")
124
+ }
125
+ )
126
+
127
+ // The control for the state form: a live state on the same lifecycle, so an
128
+ // implementation dropping every row of a view that declares states fails.
129
+ switch byName("row in a live state carries the row") {
130
+ | Some(c) =>
131
+ expect((
132
+ keyOf(c.local, "state")->Option.isSome,
133
+ keyOf(c.relay, "state")->Option.isSome,
134
+ keyOf(c.postgres, "state")->Option.isSome,
135
+ ))->toEqual((true, true, true))
136
+ | None => expect("live-state case missing")->toBe("present")
137
+ }
138
+ })
58
139
  })
@@ -11,7 +11,7 @@ function buildAll(prim) {
11
11
  globalThis.describe("state-change descriptor parity", () => {
12
12
  globalThis.test("every implementation builds the same descriptor", async () => {
13
13
  let cases = await StateChangeDescriptorParityMjs.buildAll();
14
- globalThis.expect(cases.length).toBe(6);
14
+ globalThis.expect(cases.length).toBe(11);
15
15
  cases.forEach(c => {
16
16
  globalThis.expect([
17
17
  c.name,
@@ -52,6 +52,102 @@ globalThis.describe("state-change descriptor parity", () => {
52
52
  globalThis.expect("oversized case missing").toBe("present");
53
53
  }
54
54
  });
55
+ globalThis.test("a retired row publishes as metadata only, everywhere", async () => {
56
+ let cases = await StateChangeDescriptorParityMjs.buildAll();
57
+ let keyOf = (d, k) => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(d), o => o[k]);
58
+ let byName = name => cases.find(c => c.name === name);
59
+ let c = byName("retired row degrades to metadata only");
60
+ if (c !== undefined) {
61
+ globalThis.expect([
62
+ keyOf(c.local, "state"),
63
+ keyOf(c.local, "sortKeyValue")
64
+ ]).toEqual([
65
+ undefined,
66
+ undefined
67
+ ]);
68
+ globalThis.expect([
69
+ keyOf(c.relay, "state"),
70
+ keyOf(c.relay, "sortKeyValue")
71
+ ]).toEqual([
72
+ undefined,
73
+ undefined
74
+ ]);
75
+ globalThis.expect([
76
+ keyOf(c.postgres, "state"),
77
+ keyOf(c.postgres, "sortKeyValue")
78
+ ]).toEqual([
79
+ undefined,
80
+ undefined
81
+ ]);
82
+ globalThis.expect(keyOf(c.local, "changeKind")).toEqual("Updated");
83
+ } else {
84
+ globalThis.expect("retired case missing").toBe("present");
85
+ }
86
+ let c$1 = byName("live row with a retirement flag carries the row");
87
+ if (c$1 !== undefined) {
88
+ globalThis.expect(Stdlib_Option.isSome(keyOf(c$1.local, "state"))).toBe(true);
89
+ globalThis.expect(Stdlib_Option.isSome(keyOf(c$1.relay, "state"))).toBe(true);
90
+ globalThis.expect(Stdlib_Option.isSome(keyOf(c$1.postgres, "state"))).toBe(true);
91
+ } else {
92
+ globalThis.expect("live-with-flag case missing").toBe("present");
93
+ }
94
+ });
95
+ globalThis.test("a row in any retired state publishes as metadata only", async () => {
96
+ let cases = await StateChangeDescriptorParityMjs.buildAll();
97
+ let keyOf = (d, k) => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(d), o => o[k]);
98
+ let byName = name => cases.find(c => c.name === name);
99
+ [
100
+ "row in the first retired state degrades to metadata only",
101
+ "row in the second retired state degrades to metadata only"
102
+ ].forEach(name => {
103
+ let c = byName(name);
104
+ if (c !== undefined) {
105
+ globalThis.expect([
106
+ name,
107
+ keyOf(c.local, "state"),
108
+ keyOf(c.local, "sortKeyValue")
109
+ ]).toEqual([
110
+ name,
111
+ undefined,
112
+ undefined
113
+ ]);
114
+ globalThis.expect([
115
+ name,
116
+ keyOf(c.relay, "state"),
117
+ keyOf(c.relay, "sortKeyValue")
118
+ ]).toEqual([
119
+ name,
120
+ undefined,
121
+ undefined
122
+ ]);
123
+ globalThis.expect([
124
+ name,
125
+ keyOf(c.postgres, "state"),
126
+ keyOf(c.postgres, "sortKeyValue")
127
+ ]).toEqual([
128
+ name,
129
+ undefined,
130
+ undefined
131
+ ]);
132
+ } else {
133
+ globalThis.expect(name + " missing").toBe("present");
134
+ }
135
+ });
136
+ let c = byName("row in a live state carries the row");
137
+ if (c !== undefined) {
138
+ globalThis.expect([
139
+ Stdlib_Option.isSome(keyOf(c.local, "state")),
140
+ Stdlib_Option.isSome(keyOf(c.relay, "state")),
141
+ Stdlib_Option.isSome(keyOf(c.postgres, "state"))
142
+ ]).toEqual([
143
+ true,
144
+ true,
145
+ true
146
+ ]);
147
+ } else {
148
+ globalThis.expect("live-state case missing").toBe("present");
149
+ }
150
+ });
55
151
  });
56
152
 
57
153
  export {
@@ -49,6 +49,59 @@ const CASES = [
49
49
  entityKey: "p3",
50
50
  state: { id: "p3", blob: "x".repeat(70 * 1024), updatedAt: "2026-05-19T12:00:00Z" },
51
51
  },
52
+ // Retirement: the row is withdrawn, so every implementation must publish the
53
+ // metadata-only shape — no state, and no sortKeyValue either, even though this
54
+ // row carries an updatedAt that the same row would publish while live.
55
+ {
56
+ name: "retired row degrades to metadata only",
57
+ changeKind: "Updated",
58
+ entityKey: "p4",
59
+ state: { id: "p4", name: "Widget", archived: true, updatedAt: "2026-05-19T12:00:00Z" },
60
+ retiredField: "archived",
61
+ },
62
+ // The control: same field declared, flag false, so nothing changes. Without
63
+ // this, an implementation that dropped the payload unconditionally whenever a
64
+ // retiredField was configured would still pass the case above.
65
+ {
66
+ name: "live row with a retirement flag carries the row",
67
+ changeKind: "Updated",
68
+ entityKey: "p5",
69
+ state: { id: "p5", name: "Widget", archived: false, updatedAt: "2026-05-19T12:00:00Z" },
70
+ retiredField: "archived",
71
+ },
72
+ // The state form, and the reason it is a set: a lifecycle may end in more than
73
+ // one way, and both ways withdraw the row identically. Two cases rather than
74
+ // one so an implementation comparing against only the first member fails.
75
+ {
76
+ name: "row in the first retired state degrades to metadata only",
77
+ changeKind: "Updated",
78
+ entityKey: "p6",
79
+ state: { id: "p6", name: "Widget", shelfStatus: "Archived", updatedAt: "2026-05-19T12:00:00Z" },
80
+ retiredField: "shelfStatus",
81
+ retiredValues: ["Archived", "Discontinued"],
82
+ },
83
+ {
84
+ name: "row in the second retired state degrades to metadata only",
85
+ changeKind: "Updated",
86
+ entityKey: "p7",
87
+ state: {
88
+ id: "p7",
89
+ name: "Widget",
90
+ shelfStatus: "Discontinued",
91
+ updatedAt: "2026-05-19T12:00:00Z",
92
+ },
93
+ retiredField: "shelfStatus",
94
+ retiredValues: ["Archived", "Discontinued"],
95
+ },
96
+ // The state-form control: a live state on the same declared lifecycle.
97
+ {
98
+ name: "row in a live state carries the row",
99
+ changeKind: "Updated",
100
+ entityKey: "p8",
101
+ state: { id: "p8", name: "Widget", shelfStatus: "Listed", updatedAt: "2026-05-19T12:00:00Z" },
102
+ retiredField: "shelfStatus",
103
+ retiredValues: ["Archived", "Discontinued"],
104
+ },
52
105
  {
53
106
  name: "delete carries no row",
54
107
  changeKind: "Removed",
@@ -79,10 +132,17 @@ export async function buildAll() {
79
132
  return { ...descriptor, seq: "<seq>" };
80
133
  };
81
134
 
82
- return CASES.map(({ name, changeKind, entityKey, state }) => ({
135
+ return CASES.map(({ name, changeKind, entityKey, state, retiredField, retiredValues }) => ({
83
136
  name,
84
137
  local: normalise(
85
- local.make(changeKind, entityKey, state === undefined ? undefined : state, local.nextSequence()),
138
+ local.make(
139
+ changeKind,
140
+ entityKey,
141
+ state === undefined ? undefined : state,
142
+ local.nextSequence(),
143
+ retiredField,
144
+ retiredValues,
145
+ ),
86
146
  ),
87
147
  relay: normalise(
88
148
  relay.makeDescriptor(
@@ -93,10 +153,19 @@ export async function buildAll() {
93
153
  // OldImage, which makeDescriptor then drops.
94
154
  state === undefined ? { id: entityKey } : state,
95
155
  "49590300000000016818000000",
156
+ retiredField,
157
+ retiredValues,
96
158
  ),
97
159
  ),
98
160
  postgres: normalise(
99
- postgres.makeDescriptor({ changeKind, entityKey, state, seq: postgres.nextSequence() }),
161
+ postgres.makeDescriptor({
162
+ changeKind,
163
+ entityKey,
164
+ state,
165
+ seq: postgres.nextSequence(),
166
+ retiredField,
167
+ retiredValues,
168
+ }),
100
169
  ),
101
170
  }));
102
171
  }