@aws-blocks/core 0.2.0 → 0.3.0
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/dist/cdk/blocks-backend.d.ts +35 -9
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +52 -69
- package/dist/cdk/blocks-backend.test.js +61 -69
- package/dist/cdk/blocks-defaults.d.ts +27 -1
- package/dist/cdk/blocks-defaults.d.ts.map +1 -1
- package/dist/cdk/blocks-defaults.js +5 -1
- package/dist/cdk/blocks-defaults.test.js +4 -2
- package/dist/cdk/blocks-stack.test.js +73 -37
- package/dist/cdk/compute/default-compute-factory.d.ts +33 -0
- package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -0
- package/dist/cdk/compute/default-compute-factory.js +3 -0
- package/dist/cdk/index.d.ts +46 -5
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +57 -7
- package/dist/cdk/internal.d.ts +1 -0
- package/dist/cdk/internal.d.ts.map +1 -1
- package/dist/hosting-secrets.d.ts +48 -0
- package/dist/hosting-secrets.d.ts.map +1 -0
- package/dist/hosting-secrets.js +57 -0
- package/dist/hosting-secrets.test.d.ts +2 -0
- package/dist/hosting-secrets.test.d.ts.map +1 -0
- package/dist/hosting-secrets.test.js +81 -0
- package/dist/hosting.d.ts +88 -6
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +83 -24
- package/dist/hosting.test.js +158 -10
- package/dist/index.cdk.d.ts +13 -12
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +13 -9
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/lambda-handler.test.js +66 -0
- package/dist/pipeline/index.d.ts +24 -2
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +41 -1
- package/dist/pipeline.test.d.ts +2 -0
- package/dist/pipeline.test.d.ts.map +1 -0
- package/dist/pipeline.test.js +47 -0
- package/dist/rpc.d.ts +21 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +34 -0
- package/dist/rpc.test.js +38 -1
- package/dist/scripts/config.d.ts +15 -0
- package/dist/scripts/config.d.ts.map +1 -0
- package/dist/scripts/config.js +29 -0
- package/dist/scripts/deploy.d.ts.map +1 -1
- package/dist/scripts/deploy.js +4 -0
- package/dist/scripts/dev-server.d.ts +8 -0
- package/dist/scripts/dev-server.d.ts.map +1 -1
- package/dist/scripts/dev-server.js +31 -0
- package/dist/scripts/generate-client.d.ts.map +1 -1
- package/dist/scripts/generate-client.js +9 -4
- package/dist/scripts/generate-client.test.js +19 -4
- package/dist/scripts/index.d.ts +13 -10
- package/dist/scripts/index.d.ts.map +1 -1
- package/dist/scripts/index.js +11 -8
- package/dist/scripts/preflight-credentials.d.ts +30 -0
- package/dist/scripts/preflight-credentials.d.ts.map +1 -0
- package/dist/scripts/preflight-credentials.js +108 -0
- package/dist/scripts/preflight-credentials.test.d.ts +2 -0
- package/dist/scripts/preflight-credentials.test.d.ts.map +1 -0
- package/dist/scripts/preflight-credentials.test.js +77 -0
- package/dist/scripts/sandbox-args.test.d.ts +2 -0
- package/dist/scripts/sandbox-args.test.d.ts.map +1 -0
- package/dist/scripts/sandbox-args.test.js +77 -0
- package/dist/scripts/sandbox.d.ts +52 -0
- package/dist/scripts/sandbox.d.ts.map +1 -1
- package/dist/scripts/sandbox.js +92 -15
- package/dist/scripts/secret.d.ts +15 -0
- package/dist/scripts/secret.d.ts.map +1 -0
- package/dist/scripts/secret.js +29 -0
- package/dist/scripts/secret.test.d.ts +2 -0
- package/dist/scripts/secret.test.d.ts.map +1 -0
- package/dist/scripts/secret.test.js +28 -0
- package/dist/scripts/typegen.d.ts +15 -0
- package/dist/scripts/typegen.d.ts.map +1 -0
- package/dist/scripts/typegen.js +16 -0
- package/dist/secret-naming.d.ts +58 -0
- package/dist/secret-naming.d.ts.map +1 -0
- package/dist/secret-naming.js +67 -0
- package/dist/secret-naming.test.d.ts +2 -0
- package/dist/secret-naming.test.d.ts.map +1 -0
- package/dist/secret-naming.test.js +40 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/src/cdk/blocks-backend.test.ts +267 -272
- package/src/cdk/blocks-backend.ts +71 -79
- package/src/cdk/blocks-defaults.test.ts +4 -2
- package/src/cdk/blocks-defaults.ts +27 -1
- package/src/cdk/blocks-stack.test.ts +161 -118
- package/src/cdk/compute/default-compute-factory.ts +37 -0
- package/src/cdk/index.ts +84 -8
- package/src/cdk/internal.ts +1 -0
- package/src/hosting-secrets.test.ts +106 -0
- package/src/hosting-secrets.ts +106 -0
- package/src/hosting.test.ts +244 -63
- package/src/hosting.ts +180 -68
- package/src/index.cdk.ts +77 -38
- package/src/index.ts +35 -16
- package/src/lambda-handler.test.ts +74 -0
- package/src/pipeline/index.ts +64 -11
- package/src/pipeline.test.ts +55 -0
- package/src/rpc.test.ts +44 -1
- package/src/rpc.ts +41 -0
- package/src/scripts/config.ts +35 -0
- package/src/scripts/deploy.ts +5 -0
- package/src/scripts/dev-server.ts +40 -0
- package/src/scripts/generate-client.test.ts +43 -8
- package/src/scripts/generate-client.ts +70 -65
- package/src/scripts/index.ts +20 -17
- package/src/scripts/preflight-credentials.test.ts +110 -0
- package/src/scripts/preflight-credentials.ts +131 -0
- package/src/scripts/sandbox-args.test.ts +92 -0
- package/src/scripts/sandbox.ts +102 -15
- package/src/scripts/secret.test.ts +35 -0
- package/src/scripts/secret.ts +35 -0
- package/src/scripts/typegen.ts +18 -0
- package/src/secret-naming.test.ts +59 -0
- package/src/secret-naming.ts +99 -0
- package/src/version.ts +1 -1
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import * as cdk from 'aws-cdk-lib';
|
|
5
|
-
import * as
|
|
6
|
-
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
5
|
+
import type * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
7
6
|
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
8
7
|
import { CfnGroup } from 'aws-cdk-lib/aws-resourcegroups';
|
|
9
8
|
import { Construct } from 'constructs';
|
|
10
9
|
import { pathToFileURL } from 'node:url';
|
|
11
|
-
import { DEFAULT_NODE_RUNTIME } from './node-version.js';
|
|
12
|
-
import { blocksNodejsBundling } from './bundling.js';
|
|
13
10
|
import { addBlocksStackMetadata } from './stack-metadata.js';
|
|
14
11
|
import { finalizeConfigRegistry, registerConfig } from './config-registry.js';
|
|
15
12
|
import type { BlocksDefaults } from './blocks-defaults.js';
|
|
16
|
-
import { BLOCKS_NAMESPACE, BLOCKS_RPC_PREFIX } from '../constants.js';
|
|
17
13
|
import { registerBuiltinRoutes } from '../builtin-routes.js';
|
|
14
|
+
import type { Compute } from './compute/compute.js';
|
|
15
|
+
import type { DefaultComputeFactory, LambdaShapedCompute } from './compute/default-compute-factory.js';
|
|
18
16
|
|
|
19
17
|
/**
|
|
20
18
|
* Validate that the Node.js process was started with `--conditions=cdk`.
|
|
@@ -44,6 +42,13 @@ export function assertCdkConditionActive(): void {
|
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Timeout of the shared Blocks handler Lambda, and therefore the ceiling on a
|
|
47
|
+
* single invocation. Exported because resources that feed the handler have to
|
|
48
|
+
* size their own timeouts against it (e.g. an SQS queue's visibility timeout).
|
|
49
|
+
*/
|
|
50
|
+
export const SHARED_HANDLER_TIMEOUT_SECONDS = 60 * 15;
|
|
51
|
+
|
|
47
52
|
export interface BlocksBackendProps {
|
|
48
53
|
backendHandlerPath: string;
|
|
49
54
|
backendCDKPath: string;
|
|
@@ -57,7 +62,23 @@ export interface BlocksBackendProps {
|
|
|
57
62
|
defaults: BlocksDefaults;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Core's `create()` props: the public {@link BlocksBackendProps} plus the
|
|
67
|
+
* required `defaultComputeFactory`, spread on by the umbrella (`@aws-blocks/blocks`).
|
|
68
|
+
* Customers use {@link BlocksBackendProps} and never set the factory.
|
|
69
|
+
*
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
export interface CoreBlocksBackendProps extends BlocksBackendProps {
|
|
73
|
+
/** Builds the backend's default compute. Injected by `@aws-blocks/blocks`. */
|
|
74
|
+
defaultComputeFactory: DefaultComputeFactory;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Shared infra setup — provisions the stack-level resources that are NOT owned
|
|
79
|
+
* by a compute: the shared execution role, resource groups, and console-redirect
|
|
80
|
+
* routes.
|
|
81
|
+
*/
|
|
61
82
|
export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id?: string) {
|
|
62
83
|
// Fail fast with an actionable message at the create() call site if `defaults`
|
|
63
84
|
// is missing (e.g. a plain-JS caller, `as any`, or a dynamically-built props
|
|
@@ -70,12 +91,19 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
|
|
|
70
91
|
);
|
|
71
92
|
}
|
|
72
93
|
|
|
73
|
-
// ── Shared execution role
|
|
94
|
+
// ── Shared execution role ───────────────────────────────────────────────
|
|
74
95
|
// A single IAM role that every Building Block grants to. Provisioned here so
|
|
75
96
|
// it exists before the backend module is imported (Building Blocks reach it
|
|
76
97
|
// via `scope.executionRole`). Block grants sit on the role's default (inline)
|
|
77
|
-
// policy. AWSLambdaBasicExecutionRole is attached so
|
|
98
|
+
// policy. AWSLambdaBasicExecutionRole is attached so compute functions retain
|
|
78
99
|
// CloudWatch Logs permissions.
|
|
100
|
+
//
|
|
101
|
+
// INVARIANT: this must be a mutable, framework-owned `iam.Role` — never an
|
|
102
|
+
// imported role (`Role.fromRoleArn`/`fromRoleName`), which is immutable by
|
|
103
|
+
// default. On an immutable role, every Building Block's `grant*()` /
|
|
104
|
+
// `addToPrincipalPolicy()` silently becomes a no-op (returns false, no error),
|
|
105
|
+
// so permissions would quietly vanish. If a bring-your-own-role option is ever
|
|
106
|
+
// added, it must resolve to a mutable role (`{ mutable: true }`).
|
|
79
107
|
const executionRole = new iam.Role(scope, 'BlocksRole', {
|
|
80
108
|
// CompositePrincipal (rather than a bare ServicePrincipal) so additional
|
|
81
109
|
// compute types can assume this same shared role as they are introduced
|
|
@@ -86,63 +114,7 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
|
|
|
86
114
|
],
|
|
87
115
|
});
|
|
88
116
|
|
|
89
|
-
|
|
90
|
-
entry: props.backendHandlerPath,
|
|
91
|
-
runtime: DEFAULT_NODE_RUNTIME,
|
|
92
|
-
handler: 'handler',
|
|
93
|
-
role: executionRole,
|
|
94
|
-
memorySize: 2048,
|
|
95
|
-
timeout: cdk.Duration.seconds(60 * 15),
|
|
96
|
-
environment: {
|
|
97
|
-
NODE_ENV: 'production',
|
|
98
|
-
/**
|
|
99
|
-
* BLOCKS_STACK_NAME is used at runtime to derive physical resource names
|
|
100
|
-
* (DynamoDB table names, env var prefixes). It must match the CDK-time
|
|
101
|
-
* fullId of the BlocksStack/BlocksBackend so resource lookups work correctly.
|
|
102
|
-
*
|
|
103
|
-
* For BlocksStack: this equals the stack name (id).
|
|
104
|
-
* For BlocksBackend: the caller overrides this after construction to include
|
|
105
|
-
* the parent stack name for deployment uniqueness.
|
|
106
|
-
*/
|
|
107
|
-
BLOCKS_STACK_NAME: id ?? cdk.Stack.of(scope).stackName,
|
|
108
|
-
},
|
|
109
|
-
// blocksNodejsBundling shims import.meta.* to CommonJS equivalents so a
|
|
110
|
-
// CJS-bundled `fileURLToPath(import.meta.url)` resolves instead of throwing at
|
|
111
|
-
// Lambda load. See ./bundling.ts.
|
|
112
|
-
bundling: blocksNodejsBundling({
|
|
113
|
-
minify: true,
|
|
114
|
-
esbuildArgs: { '--conditions': 'aws-runtime' },
|
|
115
|
-
}),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// In sandbox mode, allow localhost origins so the local dev frontend can
|
|
119
|
-
// reach the deployed Lambda API via CORS.
|
|
120
|
-
const isSandbox =
|
|
121
|
-
scope.node.tryGetContext('sandboxMode') === 'true' ||
|
|
122
|
-
scope.node.tryGetContext('sandboxMode') === true;
|
|
123
|
-
if (isSandbox) {
|
|
124
|
-
handler.addEnvironment('CORS_ALLOWED_ORIGINS', '^https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?$');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const api = new apigateway.RestApi(scope, 'API', {
|
|
128
|
-
restApiName: 'Blocks API',
|
|
129
|
-
deployOptions: { cachingEnabled: false },
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const integration = new apigateway.LambdaIntegration(handler);
|
|
133
|
-
|
|
134
|
-
// Build the nested resource tree for /aws-blocks/api.
|
|
135
|
-
// Intermediate resource gets a proxy so sub-paths (RawRoutes) still reach Lambda.
|
|
136
|
-
const awsBlocksResource = api.root.addResource(BLOCKS_NAMESPACE.slice(1));
|
|
137
|
-
awsBlocksResource.addProxy({ defaultIntegration: integration, anyMethod: true });
|
|
138
|
-
|
|
139
|
-
const apiResource = awsBlocksResource.addResource('api');
|
|
140
|
-
apiResource.addMethod('POST', integration);
|
|
141
|
-
apiResource.addMethod('OPTIONS', integration);
|
|
142
|
-
|
|
143
|
-
api.root.addProxy({ defaultIntegration: integration, anyMethod: true });
|
|
144
|
-
|
|
145
|
-
// ── Resource Groups ────────────────────────────────────────────────────
|
|
117
|
+
// ── Resource Groups ───────────────────────────────────────────────────
|
|
146
118
|
let rootStack = cdk.Stack.of(scope);
|
|
147
119
|
while (rootStack.nestedStackParent) rootStack = rootStack.nestedStackParent;
|
|
148
120
|
const groupPrefix = (id && id !== rootStack.stackName) ? `${rootStack.stackName}-${id}` : rootStack.stackName;
|
|
@@ -178,7 +150,7 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
|
|
|
178
150
|
},
|
|
179
151
|
});
|
|
180
152
|
|
|
181
|
-
// ── Console redirect routes
|
|
153
|
+
// ── Console redirect routes ───────────────────────────────────────────
|
|
182
154
|
const region = cdk.Fn.ref('AWS::Region');
|
|
183
155
|
const resourcesUrl = cdk.Fn.join('', [
|
|
184
156
|
'https://', region, '.console.aws.amazon.com/resource-groups/group/',
|
|
@@ -194,7 +166,7 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
|
|
|
194
166
|
|
|
195
167
|
registerBuiltinRoutes();
|
|
196
168
|
|
|
197
|
-
return {
|
|
169
|
+
return { executionRole };
|
|
198
170
|
}
|
|
199
171
|
|
|
200
172
|
/**
|
|
@@ -215,14 +187,33 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
|
|
|
215
187
|
* ```
|
|
216
188
|
*/
|
|
217
189
|
export class BlocksBackend extends Construct {
|
|
218
|
-
public readonly apiUrl: string;
|
|
219
|
-
public readonly gateway: apigateway.RestApi;
|
|
220
|
-
public readonly handler: cdk.aws_lambda_nodejs.NodejsFunction;
|
|
221
190
|
public readonly backendHandlerPath: string;
|
|
222
191
|
/** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
|
|
223
192
|
public readonly executionRole: iam.IRole;
|
|
224
193
|
/** Infrastructure defaults for Building Blocks created under this backend. */
|
|
225
194
|
public readonly defaults: BlocksDefaults;
|
|
195
|
+
/** The default compute (owns the Lambda function + API Gateway); set in `create()`. @internal */
|
|
196
|
+
_defaultCompute?: Compute;
|
|
197
|
+
|
|
198
|
+
/** The default compute's Lambda function. To be removed once consumers move to the multi-compute model. */
|
|
199
|
+
get handler(): cdk.aws_lambda_nodejs.NodejsFunction {
|
|
200
|
+
return this.requireDefaultCompute().fn;
|
|
201
|
+
}
|
|
202
|
+
/** The default compute's API Gateway REST API. To be removed once consumers move to the multi-compute model. */
|
|
203
|
+
get gateway(): apigateway.RestApi {
|
|
204
|
+
return this.requireDefaultCompute().apiGateway;
|
|
205
|
+
}
|
|
206
|
+
/** The default compute's RPC endpoint URL. To be removed once consumers move to the multi-compute model. */
|
|
207
|
+
get apiUrl(): string {
|
|
208
|
+
return this.requireDefaultCompute().apiUrl;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private requireDefaultCompute(): LambdaShapedCompute {
|
|
212
|
+
if (!this._defaultCompute) {
|
|
213
|
+
throw new Error('Blocks backend not fully initialized — access .handler/.gateway/.apiUrl after BlocksBackend.create() resolves.');
|
|
214
|
+
}
|
|
215
|
+
return this._defaultCompute as LambdaShapedCompute;
|
|
216
|
+
}
|
|
226
217
|
|
|
227
218
|
/**
|
|
228
219
|
* The fullId used by child Scopes to compute their env var names,
|
|
@@ -269,20 +260,21 @@ export class BlocksBackend extends Construct {
|
|
|
269
260
|
this.defaults = props.defaults;
|
|
270
261
|
|
|
271
262
|
const infra = setupBlocksInfra(this, props, id);
|
|
272
|
-
this.handler = infra.handler;
|
|
273
|
-
this.gateway = infra.gateway;
|
|
274
|
-
this.apiUrl = infra.apiUrl;
|
|
275
263
|
this.executionRole = infra.executionRole;
|
|
276
|
-
|
|
277
|
-
//
|
|
278
|
-
// resource lookups (DynamoDB table names) match the CDK-time fullId
|
|
279
|
-
// and are unique per deployment.
|
|
280
|
-
this.handler.addEnvironment('BLOCKS_STACK_NAME', this.fullId);
|
|
264
|
+
// The default compute (and thus handler/gateway) is created in create(),
|
|
265
|
+
// after construction — it derives BLOCKS_STACK_NAME from this.fullId.
|
|
281
266
|
}
|
|
282
267
|
|
|
283
|
-
static async create(scope: Construct, id: string, props:
|
|
268
|
+
static async create(scope: Construct, id: string, props: CoreBlocksBackendProps) {
|
|
284
269
|
assertCdkConditionActive();
|
|
285
270
|
const backend = new BlocksBackend(scope, id, props);
|
|
271
|
+
// Create the default compute before importing the backend: it OWNS the
|
|
272
|
+
// Lambda function + API Gateway (which back .handler/.gateway/.apiUrl), and
|
|
273
|
+
// a block reading `this.compute` in its constructor (during that import)
|
|
274
|
+
// must resolve to it. The factory is supplied by the umbrella
|
|
275
|
+
// @aws-blocks/blocks (which injects LambdaCompute) via props, so core never
|
|
276
|
+
// imports the concrete compute class.
|
|
277
|
+
backend._defaultCompute = props.defaultComputeFactory(backend);
|
|
286
278
|
// file:// URL (not a raw path) so the cache-busting query works on Windows,
|
|
287
279
|
// where an absolute path like `D:\...` is rejected as URL scheme `d:`.
|
|
288
280
|
const backendUrl = pathToFileURL(props.backendCDKPath);
|
|
@@ -13,13 +13,15 @@ import { RemovalPolicy } from 'aws-cdk-lib';
|
|
|
13
13
|
import { BlocksPresets } from './blocks-defaults.js';
|
|
14
14
|
|
|
15
15
|
describe('BlocksPresets', () => {
|
|
16
|
-
test('sandbox is disposable: DESTROY + deletion protection off', () => {
|
|
16
|
+
test('sandbox is disposable: DESTROY + deletion protection off + no PITR', () => {
|
|
17
17
|
assert.strictEqual(BlocksPresets.sandbox.removalPolicy, RemovalPolicy.DESTROY);
|
|
18
18
|
assert.strictEqual(BlocksPresets.sandbox.deletionProtection, false);
|
|
19
|
+
assert.strictEqual(BlocksPresets.sandbox.pointInTimeRecovery, false);
|
|
19
20
|
});
|
|
20
21
|
|
|
21
|
-
test('production is durable: RETAIN + deletion protection on', () => {
|
|
22
|
+
test('production is durable: RETAIN + deletion protection on + PITR on', () => {
|
|
22
23
|
assert.strictEqual(BlocksPresets.production.removalPolicy, RemovalPolicy.RETAIN);
|
|
23
24
|
assert.strictEqual(BlocksPresets.production.deletionProtection, true);
|
|
25
|
+
assert.strictEqual(BlocksPresets.production.pointInTimeRecovery, true);
|
|
24
26
|
});
|
|
25
27
|
});
|
|
@@ -25,6 +25,28 @@ export interface BlocksDefaults {
|
|
|
25
25
|
* well as the CLI, API, and AWS console — until it is turned off.
|
|
26
26
|
*/
|
|
27
27
|
deletionProtection: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* CORS origins the compute's API accepts, as regular-expression patterns
|
|
31
|
+
* matched against the request `Origin` header. Empty means no cross-origin
|
|
32
|
+
* requests are allowed. The `sandbox` preset allows localhost so a local dev
|
|
33
|
+
* frontend can reach a deployed API; `production` allows none by default.
|
|
34
|
+
*/
|
|
35
|
+
allowedOrigins: string[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether stateful resources that support continuous backups keep them on by
|
|
39
|
+
* default — e.g. DynamoDB Point-in-Time Recovery, letting you restore to any
|
|
40
|
+
* second in the retention window. On in `production`, off in `sandbox` (where
|
|
41
|
+
* throwaway data isn't worth the backup-storage cost). Blocks whose service
|
|
42
|
+
* has no equivalent simply ignore it.
|
|
43
|
+
*
|
|
44
|
+
* `true` enables backups with the service's default window; `false` disables
|
|
45
|
+
* them; `{ retentionDays: n }` enables them and pins the window (a block
|
|
46
|
+
* clamps/validates `n` to its service's supported range — DynamoDB PITR is
|
|
47
|
+
* 1–35 days). Backups only have a window when on, so the two are one field.
|
|
48
|
+
*/
|
|
49
|
+
pointInTimeRecovery: boolean | { retentionDays: number };
|
|
28
50
|
}
|
|
29
51
|
|
|
30
52
|
/**
|
|
@@ -36,14 +58,18 @@ export interface BlocksDefaults {
|
|
|
36
58
|
* ```
|
|
37
59
|
*/
|
|
38
60
|
export const BlocksPresets = {
|
|
39
|
-
/** Disposable development stacks: tear down cleanly, no delete guard. */
|
|
61
|
+
/** Disposable development stacks: tear down cleanly, no delete guard, no backups. */
|
|
40
62
|
sandbox: {
|
|
41
63
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
42
64
|
deletionProtection: false,
|
|
65
|
+
allowedOrigins: ['^https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?$'],
|
|
66
|
+
pointInTimeRecovery: false,
|
|
43
67
|
},
|
|
44
68
|
/** Durable, protected posture for permanent deployments. */
|
|
45
69
|
production: {
|
|
46
70
|
removalPolicy: RemovalPolicy.RETAIN,
|
|
47
71
|
deletionProtection: true,
|
|
72
|
+
allowedOrigins: [],
|
|
73
|
+
pointInTimeRecovery: true,
|
|
48
74
|
},
|
|
49
75
|
} satisfies Record<string, BlocksDefaults>;
|
|
@@ -1,17 +1,61 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { test, describe, before } from 'node:test';
|
|
5
4
|
import assert from 'node:assert';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
5
|
import { dirname, join } from 'node:path';
|
|
6
|
+
import { before, describe, test } from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import * as cdk from 'aws-cdk-lib';
|
|
9
|
+
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
10
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
11
|
+
import type { Construct } from 'constructs';
|
|
12
|
+
import type { ScopeParent } from '../common/index.js';
|
|
13
|
+
import { BLOCKS_RPC_PREFIX } from '../constants.js';
|
|
9
14
|
import { BlocksBackend } from './blocks-backend.js';
|
|
15
|
+
import { Compute } from './compute/compute.js';
|
|
16
|
+
import type { DefaultComputeFactory } from './compute/default-compute-factory.js';
|
|
10
17
|
import { BlocksStack, BlocksPresets, Scope } from './index.js';
|
|
11
18
|
|
|
19
|
+
// A real app gets its default compute from @aws-blocks/bb-lambda-compute (via
|
|
20
|
+
// @aws-blocks/blocks), which core's own tests can't depend on. Use an
|
|
21
|
+
// equivalent inline stub: a Compute that owns a NodejsFunction + API Gateway,
|
|
22
|
+
// so create() can build the default and the handler/gateway/apiUrl accessors
|
|
23
|
+
// and synth-shape assertions have something real to resolve to. It is passed to
|
|
24
|
+
// each create() via the internal `defaultComputeFactory` option (see makeStack /
|
|
25
|
+
// makeBackend), exactly as @aws-blocks/blocks injects LambdaCompute.
|
|
26
|
+
class StubLambdaCompute extends Compute {
|
|
27
|
+
readonly fn: lambda.NodejsFunction;
|
|
28
|
+
readonly apiGateway: apigateway.RestApi;
|
|
29
|
+
readonly apiUrl: string;
|
|
30
|
+
|
|
31
|
+
constructor(scope: ScopeParent, id: string) {
|
|
32
|
+
super(id, { parent: scope });
|
|
33
|
+
this.fn = new lambda.NodejsFunction(this, 'Handler', {
|
|
34
|
+
entry: this.backendHandlerPath,
|
|
35
|
+
runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,
|
|
36
|
+
handler: 'handler',
|
|
37
|
+
role: this.executionRole,
|
|
38
|
+
environment: { BLOCKS_STACK_NAME: this.backendStackName },
|
|
39
|
+
bundling: { minify: true, esbuildArgs: { '--conditions': 'aws-runtime' } },
|
|
40
|
+
});
|
|
41
|
+
this.apiGateway = new apigateway.RestApi(this, 'API', { restApiName: 'Blocks API' });
|
|
42
|
+
this.apiGateway.root.addProxy({
|
|
43
|
+
defaultIntegration: new apigateway.LambdaIntegration(this.fn),
|
|
44
|
+
anyMethod: true,
|
|
45
|
+
});
|
|
46
|
+
this.apiUrl = `${this.apiGateway.url}${BLOCKS_RPC_PREFIX.slice(1)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setEnv(key: string, value: string): void {
|
|
50
|
+
this.fn.addEnvironment(key, value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const stubComputeFactory: DefaultComputeFactory = (root) => new StubLambdaCompute(root as never, 'DefaultCompute');
|
|
55
|
+
|
|
12
56
|
// Simulate the CDK condition being active (tests import CDK files directly)
|
|
13
57
|
before(() => {
|
|
14
|
-
|
|
58
|
+
process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS ?? '') + ' --conditions=cdk';
|
|
15
59
|
});
|
|
16
60
|
|
|
17
61
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -19,135 +63,134 @@ const handlerPath = join(__dirname, '__fixtures__', 'handler.js');
|
|
|
19
63
|
const sideEffectBackendPath = join(__dirname, '__fixtures__', 'side-effect-backend.js');
|
|
20
64
|
const factoryBackendPath = join(__dirname, '__fixtures__', 'factory-backend.js');
|
|
21
65
|
|
|
66
|
+
// Wrap create(), injecting the stub default-compute factory the way
|
|
67
|
+
// @aws-blocks/blocks injects LambdaCompute — so tests don't repeat it.
|
|
68
|
+
const makeStack = (scope: Construct, id: string, backendCDKPath: string) =>
|
|
69
|
+
BlocksStack.create(scope, id, { backendHandlerPath: handlerPath, backendCDKPath, defaults: BlocksPresets.production, defaultComputeFactory: stubComputeFactory });
|
|
70
|
+
const makeBackend = (scope: Construct, id: string, backendCDKPath: string) =>
|
|
71
|
+
BlocksBackend.create(scope, id, { backendHandlerPath: handlerPath, backendCDKPath, defaults: BlocksPresets.production, defaultComputeFactory: stubComputeFactory });
|
|
72
|
+
|
|
22
73
|
describe('ESM cache-busting (multi-stage)', () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const findMarker = (scope: any) => scope.node.tryFindChild('SideEffectMarker');
|
|
39
|
-
|
|
40
|
-
assert.ok(
|
|
41
|
-
findMarker(stack1),
|
|
42
|
-
'First stack should have SideEffectMarker from module side effect',
|
|
43
|
-
);
|
|
44
|
-
assert.ok(
|
|
45
|
-
findMarker(stack2),
|
|
46
|
-
'Second stack should have SideEffectMarker from re-executed module (cache busted)',
|
|
47
|
-
);
|
|
48
|
-
});
|
|
74
|
+
test('BlocksStack.create() with same backendCDKPath but different IDs produces constructs in each', async () => {
|
|
75
|
+
const app = new cdk.App();
|
|
76
|
+
|
|
77
|
+
const stack1 = await makeStack(app, 'PipelineStage1', sideEffectBackendPath);
|
|
78
|
+
|
|
79
|
+
const stack2 = await makeStack(app, 'PipelineStage2', sideEffectBackendPath);
|
|
80
|
+
|
|
81
|
+
const findMarker = (scope: any) => scope.node.tryFindChild('SideEffectMarker');
|
|
82
|
+
|
|
83
|
+
assert.ok(findMarker(stack1), 'First stack should have SideEffectMarker from module side effect');
|
|
84
|
+
assert.ok(
|
|
85
|
+
findMarker(stack2),
|
|
86
|
+
'Second stack should have SideEffectMarker from re-executed module (cache busted)',
|
|
87
|
+
);
|
|
88
|
+
});
|
|
49
89
|
});
|
|
50
90
|
|
|
51
91
|
describe('factory function support', () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const marker = stack.node.tryFindChild('FactoryMarker');
|
|
62
|
-
assert.ok(marker, 'Factory function should have created FactoryMarker on the stack');
|
|
63
|
-
});
|
|
92
|
+
test('BlocksStack.create() calls default export function with the stack instance', async () => {
|
|
93
|
+
const app = new cdk.App();
|
|
94
|
+
|
|
95
|
+
const stack = await makeStack(app, 'FactoryBlocksStack', factoryBackendPath);
|
|
96
|
+
|
|
97
|
+
const marker = stack.node.tryFindChild('FactoryMarker');
|
|
98
|
+
assert.ok(marker, 'Factory function should have created FactoryMarker on the stack');
|
|
99
|
+
});
|
|
64
100
|
});
|
|
65
101
|
|
|
66
102
|
describe('legacy side-effect mode (no default export)', () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const marker = backend.node.tryFindChild('SideEffectMarker');
|
|
78
|
-
assert.ok(
|
|
79
|
-
marker,
|
|
80
|
-
'Side-effect-only module should register construct via globalThis.CURRENT_BLOCKS_STACK',
|
|
81
|
-
);
|
|
82
|
-
});
|
|
103
|
+
test('module with only side effects still registers constructs via globalThis', async () => {
|
|
104
|
+
const app = new cdk.App();
|
|
105
|
+
const stack = new cdk.Stack(app, 'LegacyTestStack');
|
|
106
|
+
|
|
107
|
+
const backend = await makeBackend(stack, 'LegacyStage', sideEffectBackendPath);
|
|
108
|
+
|
|
109
|
+
const marker = backend.node.tryFindChild('SideEffectMarker');
|
|
110
|
+
assert.ok(marker, 'Side-effect-only module should register construct via globalThis.CURRENT_BLOCKS_STACK');
|
|
111
|
+
});
|
|
83
112
|
});
|
|
84
113
|
|
|
85
114
|
describe('shared execution role (BlocksStack)', () => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
assert.ok(stack.executionRole, 'BlocksStack should expose a populated .executionRole');
|
|
99
|
-
});
|
|
115
|
+
// The role synth shape and the Scope.executionRole tree-walk are shared code
|
|
116
|
+
// (setupBlocksInfra + the getter), covered in blocks-backend.test.ts. The only
|
|
117
|
+
// BlocksStack-specific behavior is that its own constructor wires
|
|
118
|
+
// executionRole — a separate code path from BlocksBackend's constructor.
|
|
119
|
+
test('BlocksStack wires executionRole via its constructor', async () => {
|
|
120
|
+
const app = new cdk.App();
|
|
121
|
+
const stack = await makeStack(app, 'StackRoleStack', sideEffectBackendPath);
|
|
122
|
+
|
|
123
|
+
assert.ok(stack.executionRole, 'BlocksStack should expose a populated .executionRole');
|
|
124
|
+
});
|
|
100
125
|
});
|
|
101
126
|
|
|
102
127
|
describe('executionRole globalThis fallback', () => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
test('resolves via globalThis.CURRENT_BLOCKS_STACK when no owner is in the tree', async () => {
|
|
129
|
+
const app = new cdk.App();
|
|
130
|
+
const stack = await makeStack(app, 'FallbackStack', sideEffectBackendPath);
|
|
131
|
+
|
|
132
|
+
// A Scope whose construct-tree ancestry has no BlocksStack/BlocksBackend
|
|
133
|
+
// (parented under a plain cdk.Stack) exhausts the tree-walk and falls back
|
|
134
|
+
// to globalThis.CURRENT_BLOCKS_STACK. The `as any` is test plumbing — a
|
|
135
|
+
// plain Stack isn't a ScopeParent, but it IS a valid Construct parent.
|
|
136
|
+
const plainStack = new cdk.Stack(app, 'PlainStack');
|
|
137
|
+
(globalThis as any).CURRENT_BLOCKS_STACK = stack;
|
|
138
|
+
const orphan = new Scope('orphan', { parent: plainStack as any });
|
|
139
|
+
|
|
140
|
+
assert.strictEqual(orphan.executionRole, stack.executionRole, 'fallback resolves to the ambient stack role');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('root is bound to the owning stack (multi-stack synth)', () => {
|
|
145
|
+
test('a block under each of two stacks resolves its OWN stack, not the last globalThis', async () => {
|
|
146
|
+
// `root` is resolved once at Scope construction (not re-walked per getter),
|
|
147
|
+
// and `create()` mutates globalThis.CURRENT_BLOCKS_STACK for each stack it
|
|
148
|
+
// builds. With two stacks in one synth, a block constructed under the first
|
|
149
|
+
// must stay bound to the first even after the second stack is created and
|
|
150
|
+
// overwrites globalThis — otherwise its root-derived accessors (handler,
|
|
151
|
+
// executionRole, backendStackName) would silently point at the wrong stack.
|
|
152
|
+
const app = new cdk.App();
|
|
153
|
+
|
|
154
|
+
const stackA = await makeStack(app, 'RootBindingA', sideEffectBackendPath);
|
|
155
|
+
// A block explicitly parented under stackA (construct-tree walk resolves
|
|
156
|
+
// stackA regardless of the ambient globalThis).
|
|
157
|
+
const blockA = new Scope('blockA', { parent: stackA });
|
|
158
|
+
|
|
159
|
+
// Building the second stack overwrites globalThis.CURRENT_BLOCKS_STACK.
|
|
160
|
+
const stackB = await makeStack(app, 'RootBindingB', sideEffectBackendPath);
|
|
161
|
+
const blockB = new Scope('blockB', { parent: stackB });
|
|
162
|
+
|
|
163
|
+
assert.strictEqual(blockA.executionRole, stackA.executionRole, 'blockA stays bound to stackA');
|
|
164
|
+
assert.strictEqual(blockB.executionRole, stackB.executionRole, 'blockB binds to stackB');
|
|
165
|
+
assert.notStrictEqual(stackA.executionRole, stackB.executionRole, 'the two stacks have distinct roles');
|
|
166
|
+
assert.strictEqual(blockA.backendStackName, 'RootBindingA', 'blockA derives its own stack name');
|
|
167
|
+
assert.strictEqual(blockB.backendStackName, 'RootBindingB', 'blockB derives its own stack name');
|
|
168
|
+
});
|
|
125
169
|
});
|
|
126
170
|
|
|
127
171
|
describe('assertCdkConditionActive', () => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
172
|
+
test('BlocksStack.create() throws when --conditions=cdk is missing', async () => {
|
|
173
|
+
const origNodeOptions = process.env.NODE_OPTIONS;
|
|
174
|
+
const origExecArgv = process.execArgv;
|
|
175
|
+
process.env.NODE_OPTIONS = '';
|
|
176
|
+
process.execArgv = [];
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const app = new cdk.App();
|
|
180
|
+
|
|
181
|
+
await assert.rejects(
|
|
182
|
+
makeStack(app, 'MissingConditionStack', sideEffectBackendPath),
|
|
183
|
+
(err: Error) => {
|
|
184
|
+
assert.ok(
|
|
185
|
+
err.message.includes('Missing --conditions=cdk'),
|
|
186
|
+
`Expected condition error, got: ${err.message}`,
|
|
187
|
+
);
|
|
188
|
+
return true;
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
} finally {
|
|
192
|
+
process.env.NODE_OPTIONS = origNodeOptions;
|
|
193
|
+
process.execArgv = origExecArgv;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
153
196
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type * as cdk from 'aws-cdk-lib';
|
|
5
|
+
import type { Compute } from './compute.js';
|
|
6
|
+
import type { BlocksStack } from '../index.js';
|
|
7
|
+
import type { BlocksBackend } from '../blocks-backend.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Lambda-shaped surface of the default compute that the legacy
|
|
11
|
+
* `handler` / `gateway` / `apiUrl` accessors on BlocksStack/BlocksBackend read.
|
|
12
|
+
* The default compute (`LambdaCompute` from `@aws-blocks/bb-lambda-compute`)
|
|
13
|
+
* satisfies this structurally, so core exposes those accessors without
|
|
14
|
+
* importing the concrete class. To be removed with those accessors once
|
|
15
|
+
* consumers move to the multi-compute model.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export interface LambdaShapedCompute extends Compute {
|
|
20
|
+
readonly fn: cdk.aws_lambda_nodejs.NodejsFunction;
|
|
21
|
+
readonly apiGateway: cdk.aws_apigateway.RestApi;
|
|
22
|
+
readonly apiUrl: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds the default {@link Compute} for a stack/backend. `create()` reads it
|
|
27
|
+
* from its props (`CoreBlocksStackProps.defaultComputeFactory`) and calls it to
|
|
28
|
+
* build the default without importing a concrete compute class — the factory is
|
|
29
|
+
* supplied by whoever owns both core and a concrete compute package (the
|
|
30
|
+
* umbrella `@aws-blocks/blocks`, which injects `LambdaCompute`). It lives on
|
|
31
|
+
* `CoreBlocksStackProps`/`CoreBlocksBackendProps`, which the umbrella derives
|
|
32
|
+
* from the customer-facing props, so it is absent from `BlocksStackProps` and
|
|
33
|
+
* customers cannot set it.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export type DefaultComputeFactory = (root: BlocksStack | BlocksBackend) => Compute;
|