@aws-blocks/bb-distributed-data 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -90,6 +90,10 @@ CREATE INDEX ASYNC idx_users_email ON users(email);
90
90
 
91
91
  Migrations are validated at dev time — unsupported features (FK, SERIAL, TRUNCATE, etc.) are caught before deploy.
92
92
 
93
+ > **Set `migrationsPath` to a path relative to your project root** (e.g. `'./aws-blocks/dsql-migrations'`); it's resolved at synth from the directory you run `cdk` / `npm run deploy` in. That's the simplest reliable pattern.
94
+ >
95
+ > You don't need `fileURLToPath(import.meta.url)` for this. Your backend module runs as ESM locally but is bundled to **CommonJS** in Lambda, where `import.meta` is empty. AWS Blocks shims `import.meta.url` / `import.meta.dirname` in the bundle so it won't crash at load — but at runtime those resolve to the **bundled output** location, not your source tree. So don't use `import.meta.url` to read a file relative to your source at request time; inline the data or ship it as a Lambda asset instead.
96
+
93
97
  ## DSQL Limitations
94
98
 
95
99
  DSQL is a subset of PostgreSQL. The local mock enforces these restrictions so code that works locally also works in production.
@@ -1,10 +1,31 @@
1
- import type { DatabaseEngine, TransactionHandle } from '@aws-blocks/data-common';
1
+ /**
2
+ * PGlite engine wrapped with DSQL validation layer for local development.
3
+ */
4
+ import { PGlite } from '@electric-sql/pglite';
5
+ import { type DatabaseEngine, type TransactionHandle } from '@aws-blocks/data-common';
2
6
  export declare class DsqlMockEngine implements DatabaseEngine {
3
7
  private db;
4
8
  private closed;
5
9
  private shouldConflict;
6
10
  private _allowDdl;
7
- constructor(dataDir: string);
11
+ private readonly dataDir;
12
+ private readonly createClient;
13
+ private ready?;
14
+ /**
15
+ * @param dataDir - directory the PGlite mock persists to.
16
+ * @param createClient - factory for the underlying PGlite instance; defaults
17
+ * to a real `PGlite`. Exposed as a seam so tests can inject an instance
18
+ * that simulates a WASM init trap.
19
+ */
20
+ constructor(dataDir: string, createClient?: (dataDir: string) => PGlite);
21
+ private createDb;
22
+ /**
23
+ * Force PGlite's lazy WASM initialization, retrying past intermittent
24
+ * `_pg_initdb` `unreachable` traps by recreating the instance. Runs once per
25
+ * engine; a permanent failure is not cached, so a later query can retry once
26
+ * transient memory pressure eases.
27
+ */
28
+ private ensureReady;
8
29
  /** Test helper: simulate OCC conflict on next commit. */
9
30
  simulateConflict(): void;
10
31
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"dsql-mock-engine.d.ts","sourceRoot":"","sources":["../../src/engines/dsql-mock-engine.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAoCjF,qBAAa,cAAe,YAAW,cAAc;IACnD,OAAO,CAAC,EAAE,CAAS;IACnB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAS;gBAEd,OAAO,EAAE,MAAM;IAM3B,yDAAyD;IACzD,gBAAgB,IAAI,IAAI;IAExB;;;OAGG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAK5C,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAMvD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAMvE,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAK9C,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAc3D,mBAAmB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7D,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAO/F,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAW/G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAK/B"}
1
+ {"version":3,"file":"dsql-mock-engine.d.ts","sourceRoot":"","sources":["../../src/engines/dsql-mock-engine.ts"],"names":[],"mappings":"AAGA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAG9C,OAAO,EAA6B,KAAK,cAAc,EAAE,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAoCjH,qBAAa,cAAe,YAAW,cAAc;IACnD,OAAO,CAAC,EAAE,CAAS;IACnB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA8B;IAC3D,OAAO,CAAC,KAAK,CAAC,CAAkB;IAEhC;;;;;OAKG;gBACS,OAAO,EAAE,MAAM,EAAE,YAAY,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAiC;IAMjG,OAAO,CAAC,QAAQ;IAMhB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IA6BnB,yDAAyD;IACzD,gBAAgB,IAAI,IAAI;IAExB;;;OAGG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAK5C,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAMvD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAMvE,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAQ9C,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAc3D,mBAAmB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7D,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAO/F,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAW/G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAK/B"}
@@ -6,6 +6,7 @@
6
6
  import { PGlite } from '@electric-sql/pglite';
7
7
  import { existsSync, unlinkSync, mkdirSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
+ import { initializePgliteWithRetry } from '@aws-blocks/data-common';
9
10
  import { DistributedDatabaseErrors, PG_SERIALIZATION_FAILURE, translateDsqlError } from '../errors.js';
10
11
  import { validateStatement, classifyStatement, TransactionTracker } from '../validation.js';
11
12
  function cleanStaleLock(dataDir) {
@@ -44,10 +45,58 @@ export class DsqlMockEngine {
44
45
  closed = false;
45
46
  shouldConflict = false;
46
47
  _allowDdl = false;
47
- constructor(dataDir) {
48
- cleanStaleLock(dataDir);
49
- mkdirSync(dataDir, { recursive: true });
50
- this.db = new PGlite(dataDir);
48
+ dataDir;
49
+ createClient;
50
+ ready;
51
+ /**
52
+ * @param dataDir - directory the PGlite mock persists to.
53
+ * @param createClient - factory for the underlying PGlite instance; defaults
54
+ * to a real `PGlite`. Exposed as a seam so tests can inject an instance
55
+ * that simulates a WASM init trap.
56
+ */
57
+ constructor(dataDir, createClient = (dir) => new PGlite(dir)) {
58
+ this.dataDir = dataDir;
59
+ this.createClient = createClient;
60
+ this.db = this.createDb();
61
+ }
62
+ createDb() {
63
+ cleanStaleLock(this.dataDir);
64
+ mkdirSync(this.dataDir, { recursive: true });
65
+ return this.createClient(this.dataDir);
66
+ }
67
+ /**
68
+ * Force PGlite's lazy WASM initialization, retrying past intermittent
69
+ * `_pg_initdb` `unreachable` traps by recreating the instance. Runs once per
70
+ * engine; a permanent failure is not cached, so a later query can retry once
71
+ * transient memory pressure eases.
72
+ */
73
+ ensureReady() {
74
+ if (!this.ready) {
75
+ this.ready = initializePgliteWithRetry(this.db, () => (this.db = this.createDb()), {
76
+ onRetry: (attempt, error) => console.warn(`[DsqlMockEngine] PGlite init trap on attempt ${attempt}; recreating instance`, error),
77
+ })
78
+ // Pin this.db to the settled instance explicitly (not just via the recreate closure).
79
+ .then((db) => (this.db = db))
80
+ .catch((error) => {
81
+ // Init retry is exhausted; initializePgliteWithRetry has already closed
82
+ // the last trapped instance, so this.db now points at a dead handle.
83
+ // Reset readiness AND swap in a fresh, un-probed instance. Without the
84
+ // swap, the next call would re-probe the CLOSED handle, whose error is a
85
+ // "closed" error (not an `unreachable` trap) and is therefore classified
86
+ // non-retryable — permanently wedging the engine and defeating the
87
+ // "a later query can retry" recovery documented above. Recreating here
88
+ // is best effort; if it throws we keep propagating the original init error.
89
+ this.ready = undefined;
90
+ try {
91
+ this.db = this.createDb();
92
+ }
93
+ catch {
94
+ // Leave the dead handle in place; the original init error is more useful.
95
+ }
96
+ throw error;
97
+ });
98
+ }
99
+ return this.ready;
51
100
  }
52
101
  /** Test helper: simulate OCC conflict on next commit. */
53
102
  simulateConflict() { this.shouldConflict = true; }
@@ -67,6 +116,7 @@ export class DsqlMockEngine {
67
116
  async query(sql, params) {
68
117
  const normalized = preprocessSqlForDsqlMock(sql, { allowDdl: this._allowDdl });
69
118
  try {
119
+ await this.ensureReady();
70
120
  return (await this.db.query(normalized, params)).rows;
71
121
  }
72
122
  catch (e) {
@@ -76,6 +126,7 @@ export class DsqlMockEngine {
76
126
  async execute(sql, params) {
77
127
  const normalized = preprocessSqlForDsqlMock(sql, { allowDdl: this._allowDdl });
78
128
  try {
129
+ await this.ensureReady();
79
130
  return { rowCount: (await this.db.query(normalized, params)).affectedRows ?? 0 };
80
131
  }
81
132
  catch (e) {
@@ -83,8 +134,14 @@ export class DsqlMockEngine {
83
134
  }
84
135
  }
85
136
  async beginTransaction() {
86
- await this.db.query('BEGIN');
87
- return { active: true, tracker: new TransactionTracker() };
137
+ try {
138
+ await this.ensureReady();
139
+ await this.db.query('BEGIN');
140
+ return { active: true, tracker: new TransactionTracker() };
141
+ }
142
+ catch (e) {
143
+ translateDsqlError(e);
144
+ }
88
145
  }
89
146
  async commitTransaction(handle) {
90
147
  if (this.shouldConflict) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dsql-mock-engine.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dsql-mock-engine.test.d.ts","sourceRoot":"","sources":["../../src/engines/dsql-mock-engine.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,112 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { test, afterEach } from 'node:test';
4
+ import assert from 'node:assert';
5
+ import { PGlite } from '@electric-sql/pglite';
6
+ import { rmSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { DsqlMockEngine } from './dsql-mock-engine.js';
9
+ import { DistributedDatabaseErrors } from '../errors.js';
10
+ const TEST_DIR = '.bb-data-dsql-mock-test-' + process.pid;
11
+ let engine;
12
+ afterEach(async () => {
13
+ if (engine)
14
+ await engine.destroy().catch(() => { });
15
+ rmSync(TEST_DIR, { recursive: true, force: true });
16
+ });
17
+ // --- Regression #188: PGlite WASM `_pg_initdb` `unreachable` init trap ---
18
+ // The DSQL mock wraps PGlite; the same lazy-init WASM trap kills the dev server
19
+ // during runMigrations. A factory injects a first instance that traps on the
20
+ // init probe; recovery falls through to a REAL PGlite so data round-trips.
21
+ test('recovers when the first PGlite instance traps during init', async () => {
22
+ const dir = join(TEST_DIR, 'init-trap-recover');
23
+ let creates = 0;
24
+ const factory = (d) => {
25
+ creates++;
26
+ if (creates === 1) {
27
+ return {
28
+ query: async () => {
29
+ throw new Error('Aborted(). Build with -sASSERTIONS for more info. RuntimeError: unreachable');
30
+ },
31
+ close: async () => { },
32
+ };
33
+ }
34
+ return new PGlite(d);
35
+ };
36
+ engine = new DsqlMockEngine(dir, factory);
37
+ await engine.withDdl(() => engine.execute('CREATE TABLE t (id TEXT PRIMARY KEY)'));
38
+ await engine.execute("INSERT INTO t (id) VALUES ('ok')");
39
+ const rows = await engine.query('SELECT id FROM t');
40
+ assert.deepStrictEqual(rows, [{ id: 'ok' }]);
41
+ assert.strictEqual(creates, 2, 'engine should recreate the trapped instance exactly once');
42
+ });
43
+ test('surfaces a QueryFailed error when init keeps trapping', async () => {
44
+ const dir = join(TEST_DIR, 'init-trap-persistent');
45
+ const factory = () => ({
46
+ query: async () => {
47
+ throw new Error('RuntimeError: unreachable');
48
+ },
49
+ close: async () => { },
50
+ });
51
+ engine = new DsqlMockEngine(dir, factory);
52
+ await assert.rejects(() => engine.execute("INSERT INTO t (id) VALUES ('x')"), (err) => {
53
+ assert.strictEqual(err.name, DistributedDatabaseErrors.QueryFailed);
54
+ return true;
55
+ });
56
+ });
57
+ // beginTransaction() forces the same lazy WASM init as query()/execute(); an
58
+ // exhausted init-retry at transaction start must also normalize to QueryFailed
59
+ // rather than leaking a raw `RuntimeError: unreachable`.
60
+ test('surfaces a QueryFailed error when init keeps trapping during beginTransaction', async () => {
61
+ const dir = join(TEST_DIR, 'init-trap-begin-transaction');
62
+ const factory = () => ({
63
+ query: async () => {
64
+ throw new Error('RuntimeError: unreachable');
65
+ },
66
+ close: async () => { },
67
+ });
68
+ engine = new DsqlMockEngine(dir, factory);
69
+ await assert.rejects(() => engine.beginTransaction(), (err) => {
70
+ assert.strictEqual(err.name, DistributedDatabaseErrors.QueryFailed);
71
+ return true;
72
+ });
73
+ });
74
+ // --- Regression #188 (recovery): a later call must succeed after the init
75
+ // retry budget is fully exhausted, once the factory stops trapping. The
76
+ // exhausted attempt closes the last instance; keeping that dead handle would
77
+ // make the next call re-probe a CLOSED instance whose error is a "closed"
78
+ // error (NOT `unreachable`) and thus non-retryable, wedging the engine
79
+ // permanently. Trapped instances model a real PGlite: `unreachable` while
80
+ // open, a "closed" error after close().
81
+ test('recovers on a later call after the init-retry budget is exhausted', async () => {
82
+ const dir = join(TEST_DIR, 'init-trap-exhaust-recover');
83
+ let creates = 0;
84
+ const factory = (d) => {
85
+ creates++;
86
+ if (creates <= 3) {
87
+ let closed = false;
88
+ return {
89
+ query: async () => {
90
+ throw new Error(closed ? 'PGlite is closed' : 'Aborted(). RuntimeError: unreachable');
91
+ },
92
+ close: async () => {
93
+ closed = true;
94
+ },
95
+ };
96
+ }
97
+ return new PGlite(d);
98
+ };
99
+ engine = new DsqlMockEngine(dir, factory);
100
+ // First call exhausts the 3-attempt retry budget and rejects.
101
+ await assert.rejects(() => engine.withDdl(() => engine.execute('CREATE TABLE t (id TEXT PRIMARY KEY)')), (err) => {
102
+ assert.strictEqual(err.name, DistributedDatabaseErrors.QueryFailed);
103
+ return true;
104
+ });
105
+ // Once the factory stops trapping, a later call must recover on a fresh
106
+ // instance instead of re-probing the closed handle forever.
107
+ await engine.withDdl(() => engine.execute('CREATE TABLE t (id TEXT PRIMARY KEY)'));
108
+ await engine.execute("INSERT INTO t (id) VALUES ('ok')");
109
+ const rows = await engine.query('SELECT id FROM t');
110
+ assert.deepStrictEqual(rows, [{ id: 'ok' }]);
111
+ assert.ok(creates >= 4, 'engine should build a fresh instance after exhaustion');
112
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAoC,MAAM,sBAAsB,CAAC;AAC/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAQpD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAG7D,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,0BAA0B;IAyFhF;;;;;OAKG;IACH,SAAS,IAAI,KAAK;CAGnB;AAaD,OAAO,EAAE,GAAG,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAA0D,MAAM,sBAAsB,CAAC;AACrG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAQpD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAG7D,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,0BAA0B;IAgGhF;;;;;OAKG;IACH,SAAS,IAAI,KAAK;CAGnB;AAaD,OAAO,EAAE,GAAG,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.cdk.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Provisions Aurora DSQL cluster via CloudFormation.
6
6
  * Optionally runs migrations via a CustomResource Lambda.
7
7
  */
8
- import { Scope, DEFAULT_NODE_RUNTIME, synthGuard } from '@aws-blocks/core/cdk';
8
+ import { Scope, DEFAULT_NODE_RUNTIME, synthGuard, blocksNodejsBundling } from '@aws-blocks/core/cdk';
9
9
  import * as cdk from 'aws-cdk-lib';
10
10
  import * as iam from 'aws-cdk-lib/aws-iam';
11
11
  import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
@@ -18,17 +18,23 @@ export class DistributedDatabase extends Scope {
18
18
  constructor(scope, id, options) {
19
19
  super(id, { parent: scope });
20
20
  const stack = cdk.Stack.of(this);
21
- const isSandbox = stack.node.tryGetContext('sandboxMode') === 'true';
22
21
  const envName = this.fullId.replace(ENV_SANITIZE, '_');
23
22
  const region = stack.region;
24
23
  const dbRole = sanitizeDbRoleName(this.fullId);
25
- // DSQL Cluster respect explicit removalPolicy; default to DESTROY in sandbox, RETAIN otherwise.
26
- const shouldDestroy = options?.removalPolicy === 'destroy' || (!options?.removalPolicy && isSandbox);
27
- const removalPolicy = shouldDestroy ? cdk.RemovalPolicy.DESTROY : cdk.RemovalPolicy.RETAIN;
24
+ // Removal policy and deletion protection are resolved independently from the
25
+ // stack-wide `defaults` (per-block `removalPolicy` option wins for that field).
26
+ // Reading `defaults.deletionProtection` directly rather than deriving it
27
+ // from `removalPolicy` — keeps every adopting block consistent: the same
28
+ // `defaults` object yields the same posture no matter which block reads it.
29
+ const removalPolicy = options?.removalPolicy === 'destroy'
30
+ ? cdk.RemovalPolicy.DESTROY
31
+ : options?.removalPolicy === 'retain'
32
+ ? cdk.RemovalPolicy.RETAIN
33
+ : this.defaults.removalPolicy;
28
34
  const cluster = new cdk.CfnResource(stack, `${this.fullId}DsqlCluster`, {
29
35
  type: 'AWS::DSQL::Cluster',
30
36
  properties: {
31
- DeletionProtectionEnabled: removalPolicy !== cdk.RemovalPolicy.DESTROY,
37
+ DeletionProtectionEnabled: this.defaults.deletionProtection,
32
38
  },
33
39
  });
34
40
  cluster.applyRemovalPolicy(removalPolicy);
@@ -63,7 +69,7 @@ export class DistributedDatabase extends Scope {
63
69
  APP_ROLE_ARN: appRoleArn,
64
70
  DB_ROLE_NAME: dbRole,
65
71
  },
66
- bundling: {
72
+ bundling: blocksNodejsBundling({
67
73
  commandHooks: {
68
74
  beforeBundling: () => [],
69
75
  beforeInstall: () => [],
@@ -71,7 +77,7 @@ export class DistributedDatabase extends Scope {
71
77
  ? [`cp -r ${resolvedMigrationsPath} ${outputDir}${LAMBDA_MIGRATIONS_DIR.replace('/var/task', '')}`]
72
78
  : [],
73
79
  },
74
- },
80
+ }),
75
81
  });
76
82
  // Migration Lambda needs Admin access (DDL + role management)
77
83
  migrationFn.addToRolePolicy(new iam.PolicyStatement({
@@ -12,7 +12,7 @@ import * as lambda from 'aws-cdk-lib/aws-lambda';
12
12
  import { Template, Match } from 'aws-cdk-lib/assertions';
13
13
  import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
- import { DEFAULT_NODE_RUNTIME } from '@aws-blocks/core/cdk';
15
+ import { DEFAULT_NODE_RUNTIME, BlocksPresets } from '@aws-blocks/core/cdk';
16
16
  import { DistributedDatabase } from './index.cdk.js';
17
17
  const MIGRATIONS_DIR = '.bb-data/__test_cdk_migrations__';
18
18
  function synth(build) {
@@ -52,17 +52,35 @@ test('CDK: cluster has DeletionProtectionEnabled=true by default', () => {
52
52
  DeletionPolicy: 'Retain',
53
53
  });
54
54
  });
55
- test('CDK: removalPolicy=destroy disables deletion protection', () => {
56
- const template = synth((stack) => {
57
- new DistributedDatabase(scope(stack), 'mydsql', { removalPolicy: 'destroy' });
58
- });
59
- template.hasResource('AWS::DSQL::Cluster', {
60
- Properties: { DeletionProtectionEnabled: false },
61
- DeletionPolicy: 'Delete',
55
+ test('CDK: per-block removalPolicy is independent of deletion protection', () => {
56
+ // Deletion protection is read from `defaults` independently of removalPolicy
57
+ // (consistent across all adopting blocks). Here: sandbox defaults (protection
58
+ // off) with a per-block `removalPolicy: 'retain'` override → the cluster is
59
+ // RETAINed on stack delete but not deletion-protected.
60
+ const app = new cdk.App();
61
+ const stack = new cdk.Stack(app, 'IndepStack');
62
+ const handler = new lambda.Function(stack, 'Handler', {
63
+ runtime: DEFAULT_NODE_RUNTIME,
64
+ handler: 'index.handler',
65
+ code: lambda.Code.fromInline('exports.handler = async () => {};'),
62
66
  });
67
+ stack.handler = handler;
68
+ stack.defaults = BlocksPresets.sandbox;
69
+ globalThis.CURRENT_BLOCKS_STACK = stack;
70
+ try {
71
+ new DistributedDatabase(scope(stack), 'mydsql', { removalPolicy: 'retain' });
72
+ const template = Template.fromStack(stack);
73
+ template.hasResource('AWS::DSQL::Cluster', {
74
+ Properties: { DeletionProtectionEnabled: false },
75
+ DeletionPolicy: 'Retain',
76
+ });
77
+ }
78
+ finally {
79
+ delete globalThis.CURRENT_BLOCKS_STACK;
80
+ }
63
81
  });
64
- test('CDK: sandboxMode=true disables deletion protection', () => {
65
- const app = new cdk.App({ context: { sandboxMode: 'true' } });
82
+ test('CDK: sandbox defaults disable deletion protection', () => {
83
+ const app = new cdk.App();
66
84
  const stack = new cdk.Stack(app, 'SandboxStack');
67
85
  const handler = new lambda.Function(stack, 'Handler', {
68
86
  runtime: DEFAULT_NODE_RUNTIME,
@@ -70,6 +88,9 @@ test('CDK: sandboxMode=true disables deletion protection', () => {
70
88
  code: lambda.Code.fromInline('exports.handler = async () => {};'),
71
89
  });
72
90
  stack.handler = handler;
91
+ // The cluster's removal/protection follows the stack-wide defaults (resolved
92
+ // via the globalThis fallback here), not the sandboxMode context.
93
+ stack.defaults = BlocksPresets.sandbox;
73
94
  globalThis.CURRENT_BLOCKS_STACK = stack;
74
95
  try {
75
96
  new DistributedDatabase(scope(stack), 'mydsql');
package/dist/types.d.ts CHANGED
@@ -6,8 +6,10 @@ export interface DistributedDatabaseOptions {
6
6
  /** Path to directory containing numbered .sql migration files. */
7
7
  migrationsPath?: string;
8
8
  /**
9
- * CloudFormation removal policy.
10
- * @default 'retain'
9
+ * CloudFormation removal policy for the DSQL cluster. When omitted, the
10
+ * stack-wide `defaults` apply (`production` → RETAIN, `sandbox` → DESTROY).
11
+ * Pass `'destroy'`/`'retain'` to override for this one database. Deletion
12
+ * protection follows: on unless the cluster is being destroyed.
11
13
  */
12
14
  removalPolicy?: 'destroy' | 'retain';
13
15
  /** Optional logger for internal operations. When omitted, a default Logger at error level is created. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtC,yGAAyG;IACzG,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtC,yGAAyG;IACzG,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export declare const BB_NAME = "DistributedDatabase";
2
- export declare const BB_VERSION = "0.1.5";
2
+ export declare const BB_VERSION = "0.1.6";
3
3
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.mjs — do not edit manually
2
2
  export const BB_NAME = 'DistributedDatabase';
3
- export const BB_VERSION = '0.1.5';
3
+ export const BB_VERSION = '0.1.6';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws-blocks/bb-distributed-data",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/aws-devtools-labs/aws-blocks.git",
@@ -37,9 +37,9 @@
37
37
  "test": "node --test 'dist/**/*.test.js'"
38
38
  },
39
39
  "dependencies": {
40
- "@aws-blocks/core": "^0.1.17",
41
- "@aws-blocks/data-common": "^0.1.3",
42
- "@aws-blocks/bb-logger": "^0.1.3",
40
+ "@aws-blocks/core": "^0.2.0",
41
+ "@aws-blocks/data-common": "^0.1.4",
42
+ "@aws-blocks/bb-logger": "^0.1.4",
43
43
  "@aws-sdk/dsql-signer": "^3.700.0",
44
44
  "@electric-sql/pglite": "^0.2.0",
45
45
  "pg": "^8.13.0"