@reventlessdev/reventless-aws 3.0.0-alpha.279 → 3.0.0-alpha.281

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 CHANGED
@@ -3,6 +3,20 @@
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.281 (2026-08-10)
7
+
8
+ ### Features
9
+
10
+ * **admin:** carry Internal queryables on their own manifest field ([f5aa03d](https://github.com/ReventlessDev/reventless-core/commit/f5aa03d60b0b2554d116fe9b3784d30f2c4ef356))
11
+
12
+
13
+ # 3.0.0-alpha.280 (2026-08-10)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **aws:** keep the interceptor data source out of an option ([d908126](https://github.com/ReventlessDev/reventless-core/commit/d90812626d64b5c408c8185ac58a63db448e4082))
18
+
19
+
6
20
  # 3.0.0-alpha.279 (2026-08-10)
7
21
 
8
22
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.279",
3
+ "version": "3.0.0-alpha.281",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -15,15 +15,15 @@
15
15
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.6",
16
16
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
17
  "@reventlessdev/rescript-node": "2.0.0-alpha.3",
18
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
19
18
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.69",
19
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
20
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
21
21
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
- "@reventlessdev/reventless-core": "3.0.0-alpha.222",
22
+ "@reventlessdev/reventless-core": "3.0.0-alpha.223",
23
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.133",
23
24
  "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
24
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.86",
25
- "@reventlessdev/reventless-spec": "3.0.0-alpha.106",
26
- "@reventlessdev/reventless-infra": "3.0.0-alpha.132"
25
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.87",
26
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.107"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
@@ -114,19 +114,33 @@ let isPublicQueryable = (q: JSON.t): bool =>
114
114
  | _ => true
115
115
  }
116
116
 
117
- // Drop Internal ReadModels / StateViewSlices from a persisted structure. A
118
- // missing/non-array field defaults to `[]` (mirrors the JS `?? []`).
117
+ // Drop Internal ReadModels / StateViewSlices from a persisted structure, and
118
+ // carry what was dropped on `internalQueryables` the complement, so a client
119
+ // can resolve a reference to an Internal view without any enumeration site
120
+ // being able to see it. A missing/non-array field defaults to `[]` (mirrors the
121
+ // JS `?? []`), which also covers a structure persisted before this field
122
+ // existed: the complement is recomputed here, never read off the structure.
123
+ //
124
+ // The `~filter=false` path (Platform_PluginStructures) deliberately does not go
125
+ // through here — it serves Internal components inline and must not grow a
126
+ // duplicate list.
119
127
  let filterStructure = (structure: dict<JSON.t>): dict<JSON.t> => {
120
128
  let out = Dict.fromArray(structure->Dict.toArray)
121
- let filterArr = key =>
122
- out
123
- ->Dict.get(key)
124
- ->Option.flatMap(JSON.Decode.array)
125
- ->Option.getOr([])
126
- ->Array.filter(isPublicQueryable)
127
- ->JSON.Encode.array
128
- out->Dict.set("readModels", filterArr("readModels"))
129
- out->Dict.set("stateViewSlices", filterArr("stateViewSlices"))
129
+ let arrAt = key =>
130
+ out->Dict.get(key)->Option.flatMap(JSON.Decode.array)->Option.getOr([])
131
+ let readModels = arrAt("readModels")
132
+ let stateViewSlices = arrAt("stateViewSlices")
133
+ out->Dict.set("readModels", readModels->Array.filter(isPublicQueryable)->JSON.Encode.array)
134
+ out->Dict.set(
135
+ "stateViewSlices",
136
+ stateViewSlices->Array.filter(isPublicQueryable)->JSON.Encode.array,
137
+ )
138
+ out->Dict.set(
139
+ "internalQueryables",
140
+ Array.concat(readModels, stateViewSlices)
141
+ ->Array.filter(q => !isPublicQueryable(q))
142
+ ->JSON.Encode.array,
143
+ )
130
144
  out
131
145
  }
132
146
 
@@ -166,9 +166,12 @@ function isPublicQueryable(q) {
166
166
 
167
167
  function filterStructure(structure) {
168
168
  let out = Object.fromEntries(Object.entries(structure));
169
- let filterArr = key => Stdlib_Option.getOr(Stdlib_Option.flatMap(out[key], Stdlib_JSON.Decode.array), []).filter(isPublicQueryable);
170
- out["readModels"] = filterArr("readModels");
171
- out["stateViewSlices"] = filterArr("stateViewSlices");
169
+ let arrAt = key => Stdlib_Option.getOr(Stdlib_Option.flatMap(out[key], Stdlib_JSON.Decode.array), []);
170
+ let readModels = arrAt("readModels");
171
+ let stateViewSlices = arrAt("stateViewSlices");
172
+ out["readModels"] = readModels.filter(isPublicQueryable);
173
+ out["stateViewSlices"] = stateViewSlices.filter(isPublicQueryable);
174
+ out["internalQueryables"] = readModels.concat(stateViewSlices).filter(q => !isPublicQueryable(q));
172
175
  return out;
173
176
  }
174
177
 
@@ -109,15 +109,19 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
109
109
  // totals because one is queried through an index. A partial pipeline here would
110
110
  // make that skew invisible rather than absent.
111
111
  let interceptorFunction = (~resolverName) =>
112
- QueryInterceptor_Provisioning.dataSourceName(~api, ~opts)->Option.map(interceptorDsName =>
113
- Function.makeJs(
114
- ~name=resolverName ++ "Interceptor",
115
- ~api,
116
- ~dataSource=interceptorDsName,
117
- ~code=interceptorCode(name),
118
- ~opts,
112
+ switch QueryInterceptor_Provisioning.dataSourceName(~api, ~opts) {
113
+ | Off => None
114
+ | On(interceptorDsName) =>
115
+ Some(
116
+ Function.makeJs(
117
+ ~name=resolverName ++ "Interceptor",
118
+ ~api,
119
+ ~dataSource=interceptorDsName,
120
+ ~code=interceptorCode(name),
121
+ ~opts,
122
+ ),
119
123
  )
120
- )
124
+ }
121
125
 
122
126
  // Creates either a unit resolver (no interceptor) or a pipeline resolver
123
127
  // (interceptor Lambda → DynamoDB query), for the Query fields that have no
@@ -66,7 +66,14 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
66
66
  let fieldNameForSingle = registryEntry !== undefined ? registryEntry.singleFieldName : AppSync_Resolver_Functions$PulumiAws.uncapitalize(name$1);
67
67
  let includeIdParam = registryEntry !== undefined ? registryEntry.includeIdParam : true;
68
68
  let connectionSpec = registryEntry !== undefined ? registryEntry.connectionSpec : true;
69
- let interceptorFunction = resolverName => Stdlib_Option.map(QueryInterceptor_Provisioning$ReventlessAws.dataSourceName(api, opts), interceptorDsName => AppSync_Function$PulumiAws.makeJs(resolverName + "Interceptor", api, interceptorDsName, interceptorCode(name$1), opts));
69
+ let interceptorFunction = resolverName => {
70
+ let interceptorDsName = QueryInterceptor_Provisioning$ReventlessAws.dataSourceName(api, opts);
71
+ if (typeof interceptorDsName !== "object") {
72
+ return;
73
+ } else {
74
+ return AppSync_Function$PulumiAws.makeJs(resolverName + "Interceptor", api, interceptorDsName._0, interceptorCode(name$1), opts);
75
+ }
76
+ };
70
77
  let makeQueryResolver = (resolverName, field, code) => {
71
78
  let interceptorFn = interceptorFunction(resolverName);
72
79
  if (interceptorFn === undefined) {
@@ -138,18 +138,34 @@ let buildInterceptor = (~api: Types.AppSync.api, ~opts) => {
138
138
  dataSource.name->Pulumi.Output.asInput
139
139
  }
140
140
 
141
- /** The data source a Query resolver should put in front of its DynamoDB read, or
142
- `None` when interception is off in which case the caller builds the unit
143
- resolver it always did and nothing here is provisioned. */
144
- let dataSourceName = (~api: Types.AppSync.api, ~opts): option<Pulumi.Input.t<string>> =>
141
+ /** Whether a Query resolver fronts its DynamoDB read with the interceptor, and
142
+ the data source to front it with. `Off` means interception is switched off
143
+ the caller builds the unit resolver it always did and nothing here is
144
+ provisioned.
145
+
146
+ A variant rather than an `option`, and that is load-bearing. The payload is a
147
+ Pulumi `Output`, which is a JS `Proxy` answering *every* property read with a
148
+ derived `Output`. The generic option runtime unwraps by probing the payload
149
+ for `BS_PRIVATE_NESTED_SOME_NONE`; on a `Proxy` that probe never reads
150
+ `undefined`, so it takes the nested-option branch and yields a wrapper object
151
+ where a data-source name belongs. AppSync then rejects the resource with
152
+ "Attribute must be a single value, not a map" — at deploy time, naming a
153
+ field the source never mentions. A variant keeps that runtime off the path
154
+ entirely, so no caller can reintroduce the fault by reaching for
155
+ `Option.map`. */
156
+ type t =
157
+ | Off
158
+ | On(Pulumi.Input.t<string>)
159
+
160
+ let dataSourceName = (~api: Types.AppSync.api, ~opts): t =>
145
161
  if !ReventlessCore.QueryInterception.isEnabled() {
146
- None
162
+ Off
147
163
  } else {
148
164
  switch byApi->Map.get(api) {
149
- | Some(existing) => Some(existing)
165
+ | Some(existing) => On(existing)
150
166
  | None =>
151
167
  let created = buildInterceptor(~api, ~opts)
152
168
  byApi->Map.set(api, created)
153
- Some(created)
169
+ On(created)
154
170
  }
155
171
  }
@@ -71,15 +71,21 @@ function buildInterceptor(api, opts) {
71
71
 
72
72
  function dataSourceName(api, opts) {
73
73
  if (!QueryInterception$ReventlessCore.isEnabled()) {
74
- return;
74
+ return "Off";
75
75
  }
76
76
  let existing = byApi.get(api);
77
77
  if (existing !== undefined) {
78
- return existing;
78
+ return {
79
+ TAG: "On",
80
+ _0: existing
81
+ };
79
82
  }
80
83
  let created = buildInterceptor(api, opts);
81
84
  byApi.set(api, created);
82
- return created;
85
+ return {
86
+ TAG: "On",
87
+ _0: created
88
+ };
83
89
  }
84
90
 
85
91
  export {
@@ -112,3 +112,67 @@ describe("toEntryWith heals a structure persisted before a required list existed
112
112
  expect(names)->toEqual(["Products"])
113
113
  })
114
114
  })
115
+
116
+ // The persisted structure is pre-filter, so this handler is a twin of the
117
+ // in-memory `encodePluginStructureEntry` rather than a consequence of it. A shell
118
+ // reading `internalQueryables` against a platform that emits it on only one
119
+ // transport gets working pickers locally and silent text inputs when deployed —
120
+ // which is why the split is asserted here as well as in core.
121
+ describe("toEntryWith carries Internal queryables on the filtered field", () => {
122
+ let mixed = Dict.fromArray([
123
+ (
124
+ "readModels",
125
+ JSON.Encode.array([
126
+ JSON.parseOrThrow(`{"name": "AvailableProducts", "visibility": "Internal"}`),
127
+ JSON.parseOrThrow(`{"name": "Products"}`),
128
+ ]),
129
+ ),
130
+ (
131
+ "stateViewSlices",
132
+ JSON.Encode.array([
133
+ JSON.parseOrThrow(`{"name": "PendingShipments", "visibility": "Internal"}`),
134
+ ]),
135
+ ),
136
+ ])->JSON.Encode.object
137
+
138
+ let namesAt = (~filter, collection) =>
139
+ entry(~filter, mixed)
140
+ ->Option.map(members(_, collection))
141
+ ->Option.getOr([])
142
+ ->Array.filterMap(m => m->Dict.get("name")->Option.flatMap(JSON.Decode.string))
143
+
144
+ testSync("emits the complement of the filter, fed by both source arrays", () =>
145
+ expect(namesAt(~filter=true, "internalQueryables"))->toEqual([
146
+ "AvailableProducts",
147
+ "PendingShipments",
148
+ ])
149
+ )
150
+
151
+ testSync("keeps the surface lists public-only", () => {
152
+ expect(namesAt(~filter=true, "readModels"))->toEqual(["Products"])
153
+ expect(namesAt(~filter=true, "stateViewSlices"))->toEqual([])
154
+ })
155
+
156
+ // Platform_PluginStructures serves Internal components inline; a duplicate list
157
+ // there would be a second answer to a question that already has one.
158
+ testSync("adds nothing to the complete field", () =>
159
+ expect(
160
+ entry(~filter=false, mixed)->Option.flatMap(o => o->Dict.get("internalQueryables")),
161
+ )->toEqual(None)
162
+ )
163
+
164
+ // A structure persisted before the field existed has no key for it, and must not
165
+ // grow one from its own contents — the complement is always recomputed.
166
+ testSync("computes the complement rather than reading it off the structure", () => {
167
+ let stale = Dict.fromArray([
168
+ ("internalQueryables", JSON.parseOrThrow(`[{"name": "Stale"}]`)),
169
+ ("readModels", JSON.parseOrThrow(`[{"name": "Products"}]`)),
170
+ ])->JSON.Encode.object
171
+ let names =
172
+ entry(~filter=true, stale)
173
+ ->Option.map(members(_, "internalQueryables"))
174
+ ->Option.getOr([])
175
+ ->Array.filterMap(m => m->Dict.get("name")->Option.flatMap(JSON.Decode.string))
176
+ expect(names)->toEqual([])
177
+ })
178
+ })
@@ -64,6 +64,50 @@ globalThis.describe("toEntryWith heals a structure persisted before a required l
64
64
  });
65
65
  });
66
66
 
67
+ globalThis.describe("toEntryWith carries Internal queryables on the filtered field", () => {
68
+ let mixed = Object.fromEntries([
69
+ [
70
+ "readModels",
71
+ [
72
+ JSON.parse(`{"name": "AvailableProducts", "visibility": "Internal"}`),
73
+ JSON.parse(`{"name": "Products"}`)
74
+ ]
75
+ ],
76
+ [
77
+ "stateViewSlices",
78
+ [JSON.parse(`{"name": "PendingShipments", "visibility": "Internal"}`)]
79
+ ]
80
+ ]);
81
+ let namesAt = (filter, collection) => Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.map(entry(filter, mixed), __x => members(__x, collection)), []), m => Stdlib_Option.flatMap(m["name"], Stdlib_JSON.Decode.string));
82
+ globalThis.test("emits the complement of the filter, fed by both source arrays", () => {
83
+ globalThis.expect(namesAt(true, "internalQueryables")).toEqual([
84
+ "AvailableProducts",
85
+ "PendingShipments"
86
+ ]);
87
+ });
88
+ globalThis.test("keeps the surface lists public-only", () => {
89
+ globalThis.expect(namesAt(true, "readModels")).toEqual(["Products"]);
90
+ globalThis.expect(namesAt(true, "stateViewSlices")).toEqual([]);
91
+ });
92
+ globalThis.test("adds nothing to the complete field", () => {
93
+ globalThis.expect(Stdlib_Option.flatMap(entry(false, mixed), o => o["internalQueryables"])).toEqual(undefined);
94
+ });
95
+ globalThis.test("computes the complement rather than reading it off the structure", () => {
96
+ let stale = Object.fromEntries([
97
+ [
98
+ "internalQueryables",
99
+ JSON.parse(`[{"name": "Stale"}]`)
100
+ ],
101
+ [
102
+ "readModels",
103
+ JSON.parse(`[{"name": "Products"}]`)
104
+ ]
105
+ ]);
106
+ let names = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.map(entry(true, stale), __x => members(__x, "internalQueryables")), []), m => Stdlib_Option.flatMap(m["name"], Stdlib_JSON.Decode.string));
107
+ globalThis.expect(names).toEqual([]);
108
+ });
109
+ });
110
+
67
111
  export {
68
112
  entry,
69
113
  members,