@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.
Files changed (123) hide show
  1. package/dist/cdk/blocks-backend.d.ts +35 -9
  2. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  3. package/dist/cdk/blocks-backend.js +52 -69
  4. package/dist/cdk/blocks-backend.test.js +61 -69
  5. package/dist/cdk/blocks-defaults.d.ts +27 -1
  6. package/dist/cdk/blocks-defaults.d.ts.map +1 -1
  7. package/dist/cdk/blocks-defaults.js +5 -1
  8. package/dist/cdk/blocks-defaults.test.js +4 -2
  9. package/dist/cdk/blocks-stack.test.js +73 -37
  10. package/dist/cdk/compute/default-compute-factory.d.ts +33 -0
  11. package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -0
  12. package/dist/cdk/compute/default-compute-factory.js +3 -0
  13. package/dist/cdk/index.d.ts +46 -5
  14. package/dist/cdk/index.d.ts.map +1 -1
  15. package/dist/cdk/index.js +57 -7
  16. package/dist/cdk/internal.d.ts +1 -0
  17. package/dist/cdk/internal.d.ts.map +1 -1
  18. package/dist/hosting-secrets.d.ts +48 -0
  19. package/dist/hosting-secrets.d.ts.map +1 -0
  20. package/dist/hosting-secrets.js +57 -0
  21. package/dist/hosting-secrets.test.d.ts +2 -0
  22. package/dist/hosting-secrets.test.d.ts.map +1 -0
  23. package/dist/hosting-secrets.test.js +81 -0
  24. package/dist/hosting.d.ts +88 -6
  25. package/dist/hosting.d.ts.map +1 -1
  26. package/dist/hosting.js +83 -24
  27. package/dist/hosting.test.js +158 -10
  28. package/dist/index.cdk.d.ts +13 -12
  29. package/dist/index.cdk.d.ts.map +1 -1
  30. package/dist/index.cdk.js +13 -9
  31. package/dist/index.d.ts +8 -7
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +9 -5
  34. package/dist/lambda-handler.test.js +66 -0
  35. package/dist/pipeline/index.d.ts +24 -2
  36. package/dist/pipeline/index.d.ts.map +1 -1
  37. package/dist/pipeline/index.js +41 -1
  38. package/dist/pipeline.test.d.ts +2 -0
  39. package/dist/pipeline.test.d.ts.map +1 -0
  40. package/dist/pipeline.test.js +47 -0
  41. package/dist/rpc.d.ts +21 -0
  42. package/dist/rpc.d.ts.map +1 -1
  43. package/dist/rpc.js +34 -0
  44. package/dist/rpc.test.js +38 -1
  45. package/dist/scripts/config.d.ts +15 -0
  46. package/dist/scripts/config.d.ts.map +1 -0
  47. package/dist/scripts/config.js +29 -0
  48. package/dist/scripts/deploy.d.ts.map +1 -1
  49. package/dist/scripts/deploy.js +4 -0
  50. package/dist/scripts/dev-server.d.ts +8 -0
  51. package/dist/scripts/dev-server.d.ts.map +1 -1
  52. package/dist/scripts/dev-server.js +31 -0
  53. package/dist/scripts/generate-client.d.ts.map +1 -1
  54. package/dist/scripts/generate-client.js +9 -4
  55. package/dist/scripts/generate-client.test.js +19 -4
  56. package/dist/scripts/index.d.ts +13 -10
  57. package/dist/scripts/index.d.ts.map +1 -1
  58. package/dist/scripts/index.js +11 -8
  59. package/dist/scripts/preflight-credentials.d.ts +30 -0
  60. package/dist/scripts/preflight-credentials.d.ts.map +1 -0
  61. package/dist/scripts/preflight-credentials.js +108 -0
  62. package/dist/scripts/preflight-credentials.test.d.ts +2 -0
  63. package/dist/scripts/preflight-credentials.test.d.ts.map +1 -0
  64. package/dist/scripts/preflight-credentials.test.js +77 -0
  65. package/dist/scripts/sandbox-args.test.d.ts +2 -0
  66. package/dist/scripts/sandbox-args.test.d.ts.map +1 -0
  67. package/dist/scripts/sandbox-args.test.js +77 -0
  68. package/dist/scripts/sandbox.d.ts +52 -0
  69. package/dist/scripts/sandbox.d.ts.map +1 -1
  70. package/dist/scripts/sandbox.js +92 -15
  71. package/dist/scripts/secret.d.ts +15 -0
  72. package/dist/scripts/secret.d.ts.map +1 -0
  73. package/dist/scripts/secret.js +29 -0
  74. package/dist/scripts/secret.test.d.ts +2 -0
  75. package/dist/scripts/secret.test.d.ts.map +1 -0
  76. package/dist/scripts/secret.test.js +28 -0
  77. package/dist/scripts/typegen.d.ts +15 -0
  78. package/dist/scripts/typegen.d.ts.map +1 -0
  79. package/dist/scripts/typegen.js +16 -0
  80. package/dist/secret-naming.d.ts +58 -0
  81. package/dist/secret-naming.d.ts.map +1 -0
  82. package/dist/secret-naming.js +67 -0
  83. package/dist/secret-naming.test.d.ts +2 -0
  84. package/dist/secret-naming.test.d.ts.map +1 -0
  85. package/dist/secret-naming.test.js +40 -0
  86. package/dist/version.d.ts +1 -1
  87. package/dist/version.js +1 -1
  88. package/package.json +2 -1
  89. package/src/cdk/blocks-backend.test.ts +267 -272
  90. package/src/cdk/blocks-backend.ts +71 -79
  91. package/src/cdk/blocks-defaults.test.ts +4 -2
  92. package/src/cdk/blocks-defaults.ts +27 -1
  93. package/src/cdk/blocks-stack.test.ts +161 -118
  94. package/src/cdk/compute/default-compute-factory.ts +37 -0
  95. package/src/cdk/index.ts +84 -8
  96. package/src/cdk/internal.ts +1 -0
  97. package/src/hosting-secrets.test.ts +106 -0
  98. package/src/hosting-secrets.ts +106 -0
  99. package/src/hosting.test.ts +244 -63
  100. package/src/hosting.ts +180 -68
  101. package/src/index.cdk.ts +77 -38
  102. package/src/index.ts +35 -16
  103. package/src/lambda-handler.test.ts +74 -0
  104. package/src/pipeline/index.ts +64 -11
  105. package/src/pipeline.test.ts +55 -0
  106. package/src/rpc.test.ts +44 -1
  107. package/src/rpc.ts +41 -0
  108. package/src/scripts/config.ts +35 -0
  109. package/src/scripts/deploy.ts +5 -0
  110. package/src/scripts/dev-server.ts +40 -0
  111. package/src/scripts/generate-client.test.ts +43 -8
  112. package/src/scripts/generate-client.ts +70 -65
  113. package/src/scripts/index.ts +20 -17
  114. package/src/scripts/preflight-credentials.test.ts +110 -0
  115. package/src/scripts/preflight-credentials.ts +131 -0
  116. package/src/scripts/sandbox-args.test.ts +92 -0
  117. package/src/scripts/sandbox.ts +102 -15
  118. package/src/scripts/secret.test.ts +35 -0
  119. package/src/scripts/secret.ts +35 -0
  120. package/src/scripts/typegen.ts +18 -0
  121. package/src/secret-naming.test.ts +59 -0
  122. package/src/secret-naming.ts +99 -0
  123. 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 lambda from 'aws-cdk-lib/aws-lambda-nodejs';
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
- /** Shared infra setup — creates Lambda + API Gateway on the given scope. */
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 the handler retains
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
- const handler = new lambda.NodejsFunction(scope, 'Handler', {
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 { handler, gateway: api, apiUrl: `${api.url}${BLOCKS_RPC_PREFIX.slice(1)}`, executionRole };
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
- // Override BLOCKS_STACK_NAME to include the parent stack name so runtime
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: BlocksBackendProps) {
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
- process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS ?? '') + ' --conditions=cdk';
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
- test('BlocksStack.create() with same backendCDKPath but different IDs produces constructs in each', async () => {
24
- const app = new cdk.App();
25
-
26
- const stack1 = await BlocksStack.create(app, 'PipelineStage1', {
27
- backendHandlerPath: handlerPath,
28
- backendCDKPath: sideEffectBackendPath,
29
- defaults: BlocksPresets.production,
30
- });
31
-
32
- const stack2 = await BlocksStack.create(app, 'PipelineStage2', {
33
- backendHandlerPath: handlerPath,
34
- backendCDKPath: sideEffectBackendPath,
35
- defaults: BlocksPresets.production,
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
- test('BlocksStack.create() calls default export function with the stack instance', async () => {
53
- const app = new cdk.App();
54
-
55
- const stack = await BlocksStack.create(app, 'FactoryBlocksStack', {
56
- backendHandlerPath: handlerPath,
57
- backendCDKPath: factoryBackendPath,
58
- defaults: BlocksPresets.production,
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
- test('module with only side effects still registers constructs via globalThis', async () => {
68
- const app = new cdk.App();
69
- const stack = new cdk.Stack(app, 'LegacyTestStack');
70
-
71
- const backend = await BlocksBackend.create(stack, 'LegacyStage', {
72
- backendHandlerPath: handlerPath,
73
- backendCDKPath: sideEffectBackendPath,
74
- defaults: BlocksPresets.production,
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
- // The role synth shape and the Scope.executionRole tree-walk are shared code
87
- // (setupBlocksInfra + the getter), covered in blocks-backend.test.ts. The only
88
- // BlocksStack-specific behavior is that its own constructor wires
89
- // executionRole — a separate code path from BlocksBackend's constructor.
90
- test('BlocksStack wires executionRole via its constructor', async () => {
91
- const app = new cdk.App();
92
- const stack = await BlocksStack.create(app, 'StackRoleStack', {
93
- backendHandlerPath: handlerPath,
94
- backendCDKPath: sideEffectBackendPath,
95
- defaults: BlocksPresets.production,
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
- test('resolves via globalThis.CURRENT_BLOCKS_STACK when no owner is in the tree', async () => {
104
- const app = new cdk.App();
105
- const stack = await BlocksStack.create(app, 'FallbackStack', {
106
- backendHandlerPath: handlerPath,
107
- backendCDKPath: sideEffectBackendPath,
108
- defaults: BlocksPresets.production,
109
- });
110
-
111
- // A Scope whose construct-tree ancestry has no BlocksStack/BlocksBackend
112
- // (parented under a plain cdk.Stack) exhausts the tree-walk and falls back
113
- // to globalThis.CURRENT_BLOCKS_STACK. The `as any` is test plumbing a
114
- // plain Stack isn't a ScopeParent, but it IS a valid Construct parent.
115
- const plainStack = new cdk.Stack(app, 'PlainStack');
116
- (globalThis as any).CURRENT_BLOCKS_STACK = stack;
117
- const orphan = new Scope('orphan', { parent: plainStack as any });
118
-
119
- assert.strictEqual(
120
- orphan.executionRole,
121
- stack.executionRole,
122
- 'fallback resolves to the ambient stack role',
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
- test('BlocksStack.create() throws when --conditions=cdk is missing', async () => {
129
- const origNodeOptions = process.env.NODE_OPTIONS;
130
- const origExecArgv = process.execArgv;
131
- process.env.NODE_OPTIONS = '';
132
- process.execArgv = [];
133
-
134
- try {
135
- const app = new cdk.App();
136
-
137
- await assert.rejects(
138
- BlocksStack.create(app, 'MissingConditionStack', {
139
- backendHandlerPath: handlerPath,
140
- backendCDKPath: sideEffectBackendPath,
141
- defaults: BlocksPresets.production,
142
- }),
143
- (err: Error) => {
144
- assert.ok(err.message.includes('Missing --conditions=cdk'), `Expected condition error, got: ${err.message}`);
145
- return true;
146
- },
147
- );
148
- } finally {
149
- process.env.NODE_OPTIONS = origNodeOptions;
150
- process.execArgv = origExecArgv;
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;