@reventlessdev/reventless-aws 3.0.0-alpha.197 → 3.0.0-alpha.199
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 +7 -7
- package/src/Platform.res +294 -44
- package/src/Platform.res.mjs +420 -94
- package/src/adapter/Api/ApiFragmentDeregistration.res +137 -0
- package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +108 -0
- package/src/adapter/Api/CommandSubscriptionResolvers_AppSync.res +4 -3
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res +222 -0
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +202 -0
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +27 -17
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +13 -13
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +2 -1
- package/src/adapter/QueryDb/QueryDbBackend.res +1 -1
- package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +235 -9
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +4 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +5 -0
- package/src/components/Api/AppSync_Adapter.res +36 -59
- package/src/components/Api/AppSync_Adapter.res.mjs +18 -74
- package/src/components/Api/AppSync_SdlDecorate.res +189 -0
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +125 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res +107 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +70 -7
- package/src/util/Util_AppSync_Caller.res +12 -4
- package/src/util/Util_AppSync_Caller.res.mjs +9 -3
- package/tests/AppSync_AdapterTest.res +6 -0
- package/tests/AppSync_AdapterTest.res.mjs +12 -6
- package/tests/AppSync_SdlDecorateTest.res +157 -0
- package/tests/AppSync_SdlDecorateTest.res.mjs +147 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Pulumi dynamic resource whose sole purpose is to deregister a plugin's
|
|
2
|
+
API-schema fragment on `pulumi destroy` (final retirement of the plugin
|
|
3
|
+
stack). `create` / `update` are no-ops — registration is done by
|
|
4
|
+
`Platform.registerFragmentViaApi` at deploy time; only `delete` matters,
|
|
5
|
+
and it calls `Platform_DeregisterApiFragment` on the platform's Platform API
|
|
6
|
+
(SigV4), whose event triggers the platform-side reactive single writer (2e)
|
|
7
|
+
to re-stitch the schema WITHOUT this plugin's fields.
|
|
8
|
+
|
|
9
|
+
Serialization: Pulumi serialises the provider object (and its captured import
|
|
10
|
+
closure) into stack state, so it must NOT statically capture the AWS SDK —
|
|
11
|
+
that breaks Pulumi's closure serialiser (the CJS/ESM dual-export confusion
|
|
12
|
+
documented in AppSync_Resolver_Retrying). The delete handler therefore
|
|
13
|
+
DYNAMICALLY imports the signed caller at invocation time; only the string
|
|
14
|
+
specifier lands in state.
|
|
15
|
+
|
|
16
|
+
Version supersession must NOT deregister: `diff` never reports changes/replaces
|
|
17
|
+
(pluginId, endpoint, and region are all version-stable), so a version bump is
|
|
18
|
+
an in-place no-op and the `delete` handler fires only on a genuine stack
|
|
19
|
+
destroy — the "final retirement, resolvers gone" trigger. */
|
|
20
|
+
|
|
21
|
+
let log = ReventlessCore.Logger.fromEnv()
|
|
22
|
+
|
|
23
|
+
type providerInputs = {
|
|
24
|
+
pluginId: string,
|
|
25
|
+
endpoint: string,
|
|
26
|
+
region: string,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Pulumi passes OUTPUTS (possibly undefined) to delete/read handlers, so encode
|
|
30
|
+
// the carrier fields into the resource id — always available. "|" cannot appear
|
|
31
|
+
// in a plugin name, an AWS region, or an AppSync GraphQL endpoint URL.
|
|
32
|
+
let encodeId = (i: providerInputs): string => `${i.pluginId}|${i.region}|${i.endpoint}`
|
|
33
|
+
let decodeId = (id: string): option<providerInputs> =>
|
|
34
|
+
switch id->String.split("|") {
|
|
35
|
+
| [pluginId, region, endpoint] => Some({pluginId, region, endpoint})
|
|
36
|
+
| _ => None
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Dynamically import the signed caller and send Platform_DeregisterApiFragment.
|
|
40
|
+
// The `import(...)` keeps Util_AppSync_Caller (and its transitive @aws-sdk/@smithy
|
|
41
|
+
// deps) OUT of the serialised provider closure — only this string is captured.
|
|
42
|
+
// Positional call matches the compiled signature
|
|
43
|
+
// `sendMutation(endpoint, region, mutation, selection, variables)`.
|
|
44
|
+
let sendDeregister: (JSON.t, string, string) => promise<unit> = %raw(`
|
|
45
|
+
async function (variables, endpoint, region) {
|
|
46
|
+
const mod = await import("@reventlessdev/reventless-aws/src/util/Util_AppSync_Caller.res.mjs");
|
|
47
|
+
await mod.sendMutation(endpoint, region, "Platform_DeregisterApiFragment", "{ __typename }", variables);
|
|
48
|
+
}
|
|
49
|
+
`)
|
|
50
|
+
|
|
51
|
+
let errMessage: 'a => string = %raw(`(e) => (e && e.message) ? String(e.message) : String(e)`)
|
|
52
|
+
|
|
53
|
+
type createResult = {id: string, outs: providerInputs}
|
|
54
|
+
type updateResult = {outs: providerInputs}
|
|
55
|
+
type diffResult = {changes: bool, replaces: array<string>, deleteBeforeReplace: bool}
|
|
56
|
+
|
|
57
|
+
let create = async (inputs: providerInputs): createResult => {id: encodeId(inputs), outs: inputs}
|
|
58
|
+
|
|
59
|
+
let update = async (_id: string, _olds: providerInputs, news: providerInputs): updateResult => {
|
|
60
|
+
outs: news,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Never change/replace — see the module note: keeps version bumps from deleting
|
|
64
|
+
// (and therefore deregistering) the fragment.
|
|
65
|
+
let diff = (_id: string, _olds: providerInputs, _news: providerInputs): diffResult => {
|
|
66
|
+
changes: false,
|
|
67
|
+
replaces: [],
|
|
68
|
+
deleteBeforeReplace: false,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let delete_ = async (id: string, props: providerInputs): unit => {
|
|
72
|
+
let carrier = switch decodeId(id) {
|
|
73
|
+
| Some(c) => c
|
|
74
|
+
| None => props
|
|
75
|
+
}
|
|
76
|
+
let variables =
|
|
77
|
+
Dict.fromArray([
|
|
78
|
+
// Platform_DeregisterApiFragment(id: ID!, pluginId: ID!) — id is the
|
|
79
|
+
// generator-prepended arg (unused server-side); pass the pluginId for both.
|
|
80
|
+
("id", JSON.Encode.string(carrier.pluginId)),
|
|
81
|
+
("pluginId", JSON.Encode.string(carrier.pluginId)),
|
|
82
|
+
])->JSON.Encode.object
|
|
83
|
+
// Best-effort: on a full teardown the platform API may already be gone, and a
|
|
84
|
+
// destroy must not fail because deregistration could not reach it.
|
|
85
|
+
try {
|
|
86
|
+
await sendDeregister(variables, carrier.endpoint, carrier.region)
|
|
87
|
+
log.info(~comp="ApiFragmentDeregistration", `Deregistered API fragment for ${carrier.pluginId}`)
|
|
88
|
+
} catch {
|
|
89
|
+
| exn =>
|
|
90
|
+
log.warn(
|
|
91
|
+
~comp="ApiFragmentDeregistration",
|
|
92
|
+
`deregister for ${carrier.pluginId} failed (best-effort; platform may be gone): ${errMessage(
|
|
93
|
+
exn,
|
|
94
|
+
)}`,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Provider as a plain JS object — no Pulumi Output captures; all state flows
|
|
100
|
+
// through inputs / id.
|
|
101
|
+
let provider = {
|
|
102
|
+
"create": create,
|
|
103
|
+
"update": update,
|
|
104
|
+
"delete": delete_,
|
|
105
|
+
"diff": diff,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Pulumi dynamic resource binding ──────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
type t
|
|
111
|
+
|
|
112
|
+
type constructorProps = {
|
|
113
|
+
pluginId: Pulumi.Input.t<string>,
|
|
114
|
+
endpoint: Pulumi.Input.t<string>,
|
|
115
|
+
region: Pulumi.Input.t<string>,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// pulumi.dynamic.Resource constructor: (provider, name, props, opts). Explicit
|
|
119
|
+
// /index.js path because @pulumi/pulumi/dynamic is a directory import not
|
|
120
|
+
// resolvable in ESM mode.
|
|
121
|
+
@module("@pulumi/pulumi/dynamic/index.js") @new
|
|
122
|
+
external _newResource: ('provider, string, 'props, Pulumi.CustomResourceOptions.t) => t = "Resource"
|
|
123
|
+
|
|
124
|
+
let make = (
|
|
125
|
+
~name: string,
|
|
126
|
+
~pluginId: string,
|
|
127
|
+
~endpoint: Pulumi.Input.t<string>,
|
|
128
|
+
~region: string,
|
|
129
|
+
~opts: Pulumi.CustomResourceOptions.t={},
|
|
130
|
+
): t => {
|
|
131
|
+
let props: constructorProps = {
|
|
132
|
+
pluginId: pluginId->Pulumi.Input.make,
|
|
133
|
+
endpoint,
|
|
134
|
+
region: region->Pulumi.Input.make,
|
|
135
|
+
}
|
|
136
|
+
_newResource(provider, name, props, opts)
|
|
137
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
4
|
+
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
5
|
+
import * as IndexJs from "@pulumi/pulumi/dynamic/index.js";
|
|
6
|
+
|
|
7
|
+
let log = Logger$ReventlessCore.fromEnv();
|
|
8
|
+
|
|
9
|
+
function encodeId(i) {
|
|
10
|
+
return i.pluginId + `|` + i.region + `|` + i.endpoint;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function decodeId(id) {
|
|
14
|
+
let match = id.split("|");
|
|
15
|
+
if (match.length !== 3) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let pluginId = match[0];
|
|
19
|
+
let region = match[1];
|
|
20
|
+
let endpoint = match[2];
|
|
21
|
+
return {
|
|
22
|
+
pluginId: pluginId,
|
|
23
|
+
endpoint: endpoint,
|
|
24
|
+
region: region
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let sendDeregister = (async function (variables, endpoint, region) {
|
|
29
|
+
const mod = await import("@reventlessdev/reventless-aws/src/util/Util_AppSync_Caller.res.mjs");
|
|
30
|
+
await mod.sendMutation(endpoint, region, "Platform_DeregisterApiFragment", "{ __typename }", variables);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let errMessage = ((e) => (e && e.message) ? String(e.message) : String(e));
|
|
34
|
+
|
|
35
|
+
async function create(inputs) {
|
|
36
|
+
return {
|
|
37
|
+
id: encodeId(inputs),
|
|
38
|
+
outs: inputs
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function update(_id, _olds, news) {
|
|
43
|
+
return {
|
|
44
|
+
outs: news
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function diff(_id, _olds, _news) {
|
|
49
|
+
return {
|
|
50
|
+
changes: false,
|
|
51
|
+
replaces: [],
|
|
52
|
+
deleteBeforeReplace: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function delete_(id, props) {
|
|
57
|
+
let c = decodeId(id);
|
|
58
|
+
let carrier = c !== undefined ? c : props;
|
|
59
|
+
let variables = Object.fromEntries([
|
|
60
|
+
[
|
|
61
|
+
"id",
|
|
62
|
+
carrier.pluginId
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
"pluginId",
|
|
66
|
+
carrier.pluginId
|
|
67
|
+
]
|
|
68
|
+
]);
|
|
69
|
+
try {
|
|
70
|
+
await sendDeregister(variables, carrier.endpoint, carrier.region);
|
|
71
|
+
return log.info("ApiFragmentDeregistration", undefined, `Deregistered API fragment for ` + carrier.pluginId);
|
|
72
|
+
} catch (raw_exn) {
|
|
73
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
74
|
+
return log.warn("ApiFragmentDeregistration", undefined, `deregister for ` + carrier.pluginId + ` failed (best-effort; platform may be gone): ` + errMessage(exn));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let provider = {
|
|
79
|
+
create: create,
|
|
80
|
+
update: update,
|
|
81
|
+
delete: delete_,
|
|
82
|
+
diff: diff
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function make(name, pluginId, endpoint, region, optsOpt) {
|
|
86
|
+
let opts = optsOpt !== undefined ? optsOpt : ({});
|
|
87
|
+
let props = {
|
|
88
|
+
pluginId: pluginId,
|
|
89
|
+
endpoint: endpoint,
|
|
90
|
+
region: region
|
|
91
|
+
};
|
|
92
|
+
return new IndexJs.Resource(provider, name, props, opts);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
log,
|
|
97
|
+
encodeId,
|
|
98
|
+
decodeId,
|
|
99
|
+
sendDeregister,
|
|
100
|
+
errMessage,
|
|
101
|
+
create,
|
|
102
|
+
update,
|
|
103
|
+
diff,
|
|
104
|
+
delete_,
|
|
105
|
+
provider,
|
|
106
|
+
make,
|
|
107
|
+
}
|
|
108
|
+
/* log Not a pure module */
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Source C: mutation-triggered subscription resolvers.
|
|
3
3
|
//
|
|
4
4
|
// Creates one AppSync Subscription-type resolver per command mutation field.
|
|
5
|
-
// The corresponding SDL field (e.g. `onPlugin_Agg_Cmd`)
|
|
6
|
-
// `@aws_subscribe(mutations: ["Plugin_Agg_Cmd"])`
|
|
7
|
-
//
|
|
5
|
+
// The corresponding neutral SDL field (e.g. `onPlugin_Agg_Cmd`) is emitted by
|
|
6
|
+
// Plugin_SubscriptionSchema; its `@aws_subscribe(mutations: ["Plugin_Agg_Cmd"])`
|
|
7
|
+
// directive is appended at push time by AppSync_SdlDecorate from the
|
|
8
|
+
// fragment's subscription-source metadata. This resolver registers the handler
|
|
8
9
|
// on the Subscription type so AppSync can deliver the mutation return value
|
|
9
10
|
// to all matching subscribers automatically. AWS requires a dataSourceName
|
|
10
11
|
// on every resolver; we reuse the corresponding mutation's data source since
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// AWS resolver for the `Platform_ApiFragments` admin GraphQL query — the deploy-facing
|
|
2
|
+
// push-status surface of the API-schema fragment registry.
|
|
3
|
+
//
|
|
4
|
+
// Backed by a Lambda DataSource that scans the ApiFragments StateViewSlice table (one item per
|
|
5
|
+
// plugin name that has registered an API-schema fragment). The persisted state carries the same
|
|
6
|
+
// status fields the in-memory adapter's `Platform_ApiFragmentsApi.encodeApiFragmentEntry`
|
|
7
|
+
// projects (pluginId, apiTarget, pushStatus, pushMessage, pushedAt, registeredAt, updatedAt) —
|
|
8
|
+
// all plain/bare-string primitives in DynamoDB — so the handler projects them as-is. The encoded
|
|
9
|
+
// SDL is deliberately NOT exposed (the deploy waiter needs status only). The registry is keyed by
|
|
10
|
+
// bare plugin name, so no name@version collapse is required (mirrors the ApiFragments slice).
|
|
11
|
+
|
|
12
|
+
open PulumiAws
|
|
13
|
+
|
|
14
|
+
let makeHandlerCode = (~tableName as _: string): string =>
|
|
15
|
+
`
|
|
16
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
17
|
+
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
|
|
18
|
+
|
|
19
|
+
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
|
20
|
+
const TABLE = process.env.API_FRAGMENT_RM_TABLE;
|
|
21
|
+
|
|
22
|
+
export async function handler() {
|
|
23
|
+
if (!TABLE) {
|
|
24
|
+
console.error("Platform_ApiFragments: API_FRAGMENT_RM_TABLE env var not set");
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const items = [];
|
|
28
|
+
let exclusiveStartKey;
|
|
29
|
+
do {
|
|
30
|
+
const out = await client.send(new ScanCommand({
|
|
31
|
+
TableName: TABLE,
|
|
32
|
+
Limit: 1000,
|
|
33
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
34
|
+
}));
|
|
35
|
+
if (out.Items) items.push(...out.Items);
|
|
36
|
+
exclusiveStartKey = out.LastEvaluatedKey;
|
|
37
|
+
} while (exclusiveStartKey);
|
|
38
|
+
|
|
39
|
+
// Status-only projection, byte-identical to Platform_ApiFragmentsApi.encodeApiFragmentEntry.
|
|
40
|
+
// The registry is keyed by bare plugin name; split("@")[0] is a defensive no-op against any
|
|
41
|
+
// legacy name@version row.
|
|
42
|
+
return items
|
|
43
|
+
.filter((item) => item && item.pluginId)
|
|
44
|
+
.map((item) => ({
|
|
45
|
+
pluginId: String(item.pluginId).split("@")[0],
|
|
46
|
+
apiTarget: item.apiTarget || "Domain",
|
|
47
|
+
pushStatus: item.pushStatus || "pending",
|
|
48
|
+
pushMessage: item.pushMessage || "",
|
|
49
|
+
pushedAt: item.pushedAt || "",
|
|
50
|
+
registeredAt: item.registeredAt || "",
|
|
51
|
+
updatedAt: item.updatedAt || "",
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
`
|
|
55
|
+
|
|
56
|
+
let resolverCode = `
|
|
57
|
+
import { util } from '@aws-appsync/utils';
|
|
58
|
+
export function request(ctx) {
|
|
59
|
+
return { operation: 'Invoke', payload: {} };
|
|
60
|
+
}
|
|
61
|
+
export function response(ctx) {
|
|
62
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
63
|
+
return ctx.result;
|
|
64
|
+
}
|
|
65
|
+
`->Pulumi.Input.make
|
|
66
|
+
|
|
67
|
+
let make = (
|
|
68
|
+
~api: Pulumi.Output.t<AppSync.GraphQLApi.t>,
|
|
69
|
+
~apiFragmentRegistryTableName: Pulumi.Output.t<string>,
|
|
70
|
+
// Resolves when the admin-base schema push has completed and the API is ACTIVE
|
|
71
|
+
// (Platform_Admin.adminSchemaPushed). CreateResolver is gated on it so it never
|
|
72
|
+
// races StartSchemaCreation on this first-ever-deployed field.
|
|
73
|
+
~schemaReady: Pulumi.Output.t<unit>,
|
|
74
|
+
~opts: Pulumi.ComponentResource.options,
|
|
75
|
+
) => {
|
|
76
|
+
let opts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
77
|
+
let name = "PlatformApiFragments"
|
|
78
|
+
|
|
79
|
+
let lambdaRole = IAM.Role.makeWithDefaultPolicy(
|
|
80
|
+
~name=name ++ "Lambda",
|
|
81
|
+
~servicePrincipal=AWS.Lambda.principal->Pulumi.Output.make,
|
|
82
|
+
~opts,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
let _ =
|
|
86
|
+
apiFragmentRegistryTableName->Pulumi.Output.apply(tableName => {
|
|
87
|
+
open PolicyDocument
|
|
88
|
+
let _rolePolicy = IAM.RolePolicy.make(
|
|
89
|
+
~name=name ++ "LambdaPolicy",
|
|
90
|
+
~args={
|
|
91
|
+
IAM.RolePolicy.policy: PolicyDocument.make(
|
|
92
|
+
~id=name ++ "LambdaPolicy",
|
|
93
|
+
~statements=[
|
|
94
|
+
{
|
|
95
|
+
sid: "AllowLambdaLogging",
|
|
96
|
+
effect: Allow,
|
|
97
|
+
actions: Action("logs:*"),
|
|
98
|
+
resources: Resource("arn:aws:logs:*:*:*"),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
sid: "AllowScanApiFragmentsTable",
|
|
102
|
+
effect: Allow,
|
|
103
|
+
actions: Actions(["dynamodb:Scan"]),
|
|
104
|
+
resources: Resource("arn:aws:dynamodb:*:*:table/" ++ tableName),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
)
|
|
108
|
+
->PolicyDocument.toJsonString
|
|
109
|
+
->Pulumi.Input.make,
|
|
110
|
+
role: lambdaRole.id->Pulumi.Output.asInput,
|
|
111
|
+
},
|
|
112
|
+
~opts,
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
let archiveContents: dict<Pulumi.Archive.assetOrArchive> = Dict.make()
|
|
117
|
+
let handlerCodeStub = makeHandlerCode(~tableName="")
|
|
118
|
+
archiveContents->Dict.set(
|
|
119
|
+
"index.mjs",
|
|
120
|
+
Pulumi.Asset.stringAsset(handlerCodeStub)->Pulumi.Archive.assetToAssetOrArchive,
|
|
121
|
+
)
|
|
122
|
+
// ESM self-containment: the handler imports @aws-sdk/* bare specifiers the
|
|
123
|
+
// nodejs22.x runtime provides only under /var/runtime — unreachable from
|
|
124
|
+
// /var/task ESM without the resolver hook. Ship the loader + set its env vars.
|
|
125
|
+
let loaderHash = Util_Bundle.addEsmLoaderAssets(archiveContents)
|
|
126
|
+
let code = Pulumi.Archive.assetArchive(archiveContents)
|
|
127
|
+
let sourceCodeHash = Util_Bundle.hashString(handlerCodeStub ++ "\n---\n" ++ loaderHash)
|
|
128
|
+
|
|
129
|
+
let layers =
|
|
130
|
+
Lambda.reventlessLayerArn
|
|
131
|
+
->Option.map(arn => [arn->Pulumi.Input.make])
|
|
132
|
+
->Option.getOr([])
|
|
133
|
+
->Pulumi.Input.make
|
|
134
|
+
|
|
135
|
+
let lambda = Lambda.Function.make(
|
|
136
|
+
~name=name ++ "Lambda",
|
|
137
|
+
~args={
|
|
138
|
+
handler: "index.handler"->Pulumi.Input.make,
|
|
139
|
+
runtime: "nodejs22.x"->Pulumi.Input.make,
|
|
140
|
+
code: code->Pulumi.Input.make,
|
|
141
|
+
sourceCodeHash: sourceCodeHash->Pulumi.Input.make,
|
|
142
|
+
role: lambdaRole.arn->Pulumi.Output.asInput,
|
|
143
|
+
memorySize: 512->Pulumi.Input.make,
|
|
144
|
+
timeout: 30->Pulumi.Input.make,
|
|
145
|
+
layers,
|
|
146
|
+
tags: AWS.Tags.make(~name=name ++ "Lambda", ReventlessCore.ReadModel.componentType),
|
|
147
|
+
environment: (
|
|
148
|
+
{
|
|
149
|
+
Lambda.Function.variables: Dict.fromArray([
|
|
150
|
+
("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
|
|
151
|
+
("API_FRAGMENT_RM_TABLE", apiFragmentRegistryTableName->Pulumi.Output.asInput),
|
|
152
|
+
("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
|
|
153
|
+
("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
|
|
154
|
+
]),
|
|
155
|
+
}: Lambda.Function.functionEnvironment
|
|
156
|
+
)->Pulumi.Input.make,
|
|
157
|
+
},
|
|
158
|
+
~opts,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
let dataSourceRole = IAM.Role.makeWithDefaultPolicy(
|
|
162
|
+
~name=name ++ "DataSource",
|
|
163
|
+
~servicePrincipal=AWS.AppSync.principal->Pulumi.Output.make,
|
|
164
|
+
~opts,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
let _ =
|
|
168
|
+
(lambda.arn, dataSourceRole.id)
|
|
169
|
+
->Pulumi.Output.all2
|
|
170
|
+
->Pulumi.Output.apply(((lambdaArn, dataSourceRoleId)) => {
|
|
171
|
+
open PolicyDocument
|
|
172
|
+
let _attach = IAM.RolePolicy.make(
|
|
173
|
+
~name=name ++ "DataSource",
|
|
174
|
+
~args={
|
|
175
|
+
IAM.RolePolicy.policy: PolicyDocument.make(
|
|
176
|
+
~id=name ++ "DataSourcePolicy",
|
|
177
|
+
~statements=[
|
|
178
|
+
{
|
|
179
|
+
sid: "AllowDataSourceInvokeLambda",
|
|
180
|
+
effect: Allow,
|
|
181
|
+
actions: Action("lambda:InvokeFunction"),
|
|
182
|
+
resources: Resource(lambdaArn),
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
)
|
|
186
|
+
->PolicyDocument.toJsonString
|
|
187
|
+
->Pulumi.Input.make,
|
|
188
|
+
role: dataSourceRoleId->Pulumi.Input.make,
|
|
189
|
+
},
|
|
190
|
+
~opts,
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
let dataSource = AppSync.DataSource.make(
|
|
195
|
+
~name=name ++ "DataSource",
|
|
196
|
+
~args={
|
|
197
|
+
type_: AWS_LAMBDA,
|
|
198
|
+
apiId: api->Pulumi.Output.flatMap(api => api.id)->Pulumi.Output.asInput,
|
|
199
|
+
lambdaConfig: {
|
|
200
|
+
AppSync.DataSource.functionArn: lambda.arn->Pulumi.Output.asInput,
|
|
201
|
+
}->Pulumi.Input.make,
|
|
202
|
+
serviceRoleArn: dataSourceRole.arn->Pulumi.Output.asInput,
|
|
203
|
+
},
|
|
204
|
+
~opts=Some(opts),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
// Create the resolver only after the admin schema push is ACTIVE — otherwise
|
|
208
|
+
// CreateResolver races StartSchemaCreation and fails with "No field named
|
|
209
|
+
// Platform_ApiFragments found on type Query" the first time this field ships.
|
|
210
|
+
let _resolver =
|
|
211
|
+
schemaReady->Pulumi.Output.apply(() =>
|
|
212
|
+
AppSync_Resolver_Native.makeUnitJsResolver(
|
|
213
|
+
~name=name ++ "Resolver",
|
|
214
|
+
~api,
|
|
215
|
+
~dataSourceName=dataSource.name->Pulumi.Output.asInput,
|
|
216
|
+
~type_="Query"->Pulumi.Input.make,
|
|
217
|
+
~field="Platform_ApiFragments"->Pulumi.Input.make,
|
|
218
|
+
~code=resolverCode,
|
|
219
|
+
~opts,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Aws from "@pulumi/aws";
|
|
4
|
+
import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
|
|
5
|
+
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
8
|
+
import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
|
|
9
|
+
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
10
|
+
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
11
|
+
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
12
|
+
import * as ReadModel$ReventlessCore from "@reventlessdev/reventless-core/src/components/ReadModel/ReadModel.res.mjs";
|
|
13
|
+
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
14
|
+
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
15
|
+
import * as AppSync_Resolver_Native$ReventlessAws from "./AppSync_Resolver_Native.res.mjs";
|
|
16
|
+
|
|
17
|
+
function makeHandlerCode(param) {
|
|
18
|
+
return `
|
|
19
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
20
|
+
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
|
|
21
|
+
|
|
22
|
+
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
|
23
|
+
const TABLE = process.env.API_FRAGMENT_RM_TABLE;
|
|
24
|
+
|
|
25
|
+
export async function handler() {
|
|
26
|
+
if (!TABLE) {
|
|
27
|
+
console.error("Platform_ApiFragments: API_FRAGMENT_RM_TABLE env var not set");
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const items = [];
|
|
31
|
+
let exclusiveStartKey;
|
|
32
|
+
do {
|
|
33
|
+
const out = await client.send(new ScanCommand({
|
|
34
|
+
TableName: TABLE,
|
|
35
|
+
Limit: 1000,
|
|
36
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
37
|
+
}));
|
|
38
|
+
if (out.Items) items.push(...out.Items);
|
|
39
|
+
exclusiveStartKey = out.LastEvaluatedKey;
|
|
40
|
+
} while (exclusiveStartKey);
|
|
41
|
+
|
|
42
|
+
// Status-only projection, byte-identical to Platform_ApiFragmentsApi.encodeApiFragmentEntry.
|
|
43
|
+
// The registry is keyed by bare plugin name; split("@")[0] is a defensive no-op against any
|
|
44
|
+
// legacy name@version row.
|
|
45
|
+
return items
|
|
46
|
+
.filter((item) => item && item.pluginId)
|
|
47
|
+
.map((item) => ({
|
|
48
|
+
pluginId: String(item.pluginId).split("@")[0],
|
|
49
|
+
apiTarget: item.apiTarget || "Domain",
|
|
50
|
+
pushStatus: item.pushStatus || "pending",
|
|
51
|
+
pushMessage: item.pushMessage || "",
|
|
52
|
+
pushedAt: item.pushedAt || "",
|
|
53
|
+
registeredAt: item.registeredAt || "",
|
|
54
|
+
updatedAt: item.updatedAt || "",
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let resolverCode = `
|
|
61
|
+
import { util } from '@aws-appsync/utils';
|
|
62
|
+
export function request(ctx) {
|
|
63
|
+
return { operation: 'Invoke', payload: {} };
|
|
64
|
+
}
|
|
65
|
+
export function response(ctx) {
|
|
66
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
67
|
+
return ctx.result;
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
function make(api, apiFragmentRegistryTableName, schemaReady, opts) {
|
|
72
|
+
let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
73
|
+
let name = "PlatformApiFragments";
|
|
74
|
+
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "Lambda", Pulumi.output(AWS$ReventlessAws.Lambda.principal), opts$1);
|
|
75
|
+
apiFragmentRegistryTableName.apply(tableName => {
|
|
76
|
+
new (Aws.iam.RolePolicy)(name + "LambdaPolicy", {
|
|
77
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "LambdaPolicy", [
|
|
78
|
+
{
|
|
79
|
+
Sid: "AllowLambdaLogging",
|
|
80
|
+
Effect: "Allow",
|
|
81
|
+
Action: "logs:*",
|
|
82
|
+
Resource: "arn:aws:logs:*:*:*"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
Sid: "AllowScanApiFragmentsTable",
|
|
86
|
+
Effect: "Allow",
|
|
87
|
+
Action: ["dynamodb:Scan"],
|
|
88
|
+
Resource: "arn:aws:dynamodb:*:*:table/" + tableName
|
|
89
|
+
}
|
|
90
|
+
])),
|
|
91
|
+
role: lambdaRole.id
|
|
92
|
+
}, opts$1);
|
|
93
|
+
});
|
|
94
|
+
let archiveContents = {};
|
|
95
|
+
let handlerCodeStub = `
|
|
96
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
97
|
+
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
|
|
98
|
+
|
|
99
|
+
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
|
100
|
+
const TABLE = process.env.API_FRAGMENT_RM_TABLE;
|
|
101
|
+
|
|
102
|
+
export async function handler() {
|
|
103
|
+
if (!TABLE) {
|
|
104
|
+
console.error("Platform_ApiFragments: API_FRAGMENT_RM_TABLE env var not set");
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
const items = [];
|
|
108
|
+
let exclusiveStartKey;
|
|
109
|
+
do {
|
|
110
|
+
const out = await client.send(new ScanCommand({
|
|
111
|
+
TableName: TABLE,
|
|
112
|
+
Limit: 1000,
|
|
113
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
114
|
+
}));
|
|
115
|
+
if (out.Items) items.push(...out.Items);
|
|
116
|
+
exclusiveStartKey = out.LastEvaluatedKey;
|
|
117
|
+
} while (exclusiveStartKey);
|
|
118
|
+
|
|
119
|
+
// Status-only projection, byte-identical to Platform_ApiFragmentsApi.encodeApiFragmentEntry.
|
|
120
|
+
// The registry is keyed by bare plugin name; split("@")[0] is a defensive no-op against any
|
|
121
|
+
// legacy name@version row.
|
|
122
|
+
return items
|
|
123
|
+
.filter((item) => item && item.pluginId)
|
|
124
|
+
.map((item) => ({
|
|
125
|
+
pluginId: String(item.pluginId).split("@")[0],
|
|
126
|
+
apiTarget: item.apiTarget || "Domain",
|
|
127
|
+
pushStatus: item.pushStatus || "pending",
|
|
128
|
+
pushMessage: item.pushMessage || "",
|
|
129
|
+
pushedAt: item.pushedAt || "",
|
|
130
|
+
registeredAt: item.registeredAt || "",
|
|
131
|
+
updatedAt: item.updatedAt || "",
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
archiveContents["index.mjs"] = new (Pulumi.asset.StringAsset)(handlerCodeStub);
|
|
136
|
+
let loaderHash = Util_Bundle$ReventlessAws.addEsmLoaderAssets(archiveContents);
|
|
137
|
+
let code = new (Pulumi.asset.AssetArchive)(archiveContents);
|
|
138
|
+
let sourceCodeHash = Util_Bundle$ReventlessAws.hashString(handlerCodeStub + "\n---\n" + loaderHash);
|
|
139
|
+
let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
|
|
140
|
+
let lambda = new (Aws.lambda.Function)(name + "Lambda", {
|
|
141
|
+
handler: "index.handler",
|
|
142
|
+
runtime: "nodejs22.x",
|
|
143
|
+
code: code,
|
|
144
|
+
role: lambdaRole.arn,
|
|
145
|
+
memorySize: 512,
|
|
146
|
+
timeout: 30,
|
|
147
|
+
layers: layers,
|
|
148
|
+
tags: AWS_Tags$ReventlessAws.make(name + "Lambda", ReadModel$ReventlessCore.componentType),
|
|
149
|
+
environment: {
|
|
150
|
+
variables: Object.fromEntries([
|
|
151
|
+
[
|
|
152
|
+
"Environment",
|
|
153
|
+
Pulumi.getStack()
|
|
154
|
+
],
|
|
155
|
+
[
|
|
156
|
+
"API_FRAGMENT_RM_TABLE",
|
|
157
|
+
apiFragmentRegistryTableName
|
|
158
|
+
],
|
|
159
|
+
[
|
|
160
|
+
"NODE_OPTIONS",
|
|
161
|
+
Util_Bundle$ReventlessAws.esmLoaderNodeOptions
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
"ESM_FALLBACK_DIRS",
|
|
165
|
+
Util_Bundle$ReventlessAws.esmFallbackDirs
|
|
166
|
+
]
|
|
167
|
+
])
|
|
168
|
+
},
|
|
169
|
+
sourceCodeHash: sourceCodeHash
|
|
170
|
+
}, opts$1);
|
|
171
|
+
let dataSourceRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "DataSource", Pulumi.output(AWS$ReventlessAws.AppSync.principal), opts$1);
|
|
172
|
+
Pulumi.all([
|
|
173
|
+
lambda.arn,
|
|
174
|
+
dataSourceRole.id
|
|
175
|
+
]).apply(param => {
|
|
176
|
+
new (Aws.iam.RolePolicy)(name + "DataSource", {
|
|
177
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "DataSourcePolicy", [{
|
|
178
|
+
Sid: "AllowDataSourceInvokeLambda",
|
|
179
|
+
Effect: "Allow",
|
|
180
|
+
Action: "lambda:InvokeFunction",
|
|
181
|
+
Resource: param[0]
|
|
182
|
+
}])),
|
|
183
|
+
role: param[1]
|
|
184
|
+
}, opts$1);
|
|
185
|
+
});
|
|
186
|
+
let dataSource = new (Aws.appsync.DataSource)(name + "DataSource", {
|
|
187
|
+
type: "AWS_LAMBDA",
|
|
188
|
+
apiId: Output$Pulumi.flatMap(api, api => api.id),
|
|
189
|
+
lambdaConfig: {
|
|
190
|
+
functionArn: lambda.arn
|
|
191
|
+
},
|
|
192
|
+
serviceRoleArn: dataSourceRole.arn
|
|
193
|
+
}, opts$1);
|
|
194
|
+
schemaReady.apply(() => AppSync_Resolver_Native$ReventlessAws.makeUnitJsResolver(name + "Resolver", api, dataSource.name, "Query", "Platform_ApiFragments", resolverCode, opts$1));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export {
|
|
198
|
+
makeHandlerCode,
|
|
199
|
+
resolverCode,
|
|
200
|
+
make,
|
|
201
|
+
}
|
|
202
|
+
/* @pulumi/aws Not a pure module */
|