@aws-blocks/bb-distributed-data 0.1.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/LICENSE +174 -0
- package/README.md +243 -0
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +50 -0
- package/dist/e2e-mock.test.d.ts +2 -0
- package/dist/e2e-mock.test.d.ts.map +1 -0
- package/dist/e2e-mock.test.js +185 -0
- package/dist/e2e.test.d.ts +2 -0
- package/dist/e2e.test.d.ts.map +1 -0
- package/dist/e2e.test.js +329 -0
- package/dist/engines/dsql-engine.d.ts +26 -0
- package/dist/engines/dsql-engine.d.ts.map +1 -0
- package/dist/engines/dsql-engine.js +103 -0
- package/dist/engines/dsql-mock-engine.d.ts +28 -0
- package/dist/engines/dsql-mock-engine.d.ts.map +1 -0
- package/dist/engines/dsql-mock-engine.js +132 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +45 -0
- package/dist/errors.test.d.ts +2 -0
- package/dist/errors.test.d.ts.map +1 -0
- package/dist/errors.test.js +55 -0
- package/dist/index.aws.d.ts +35 -0
- package/dist/index.aws.d.ts.map +1 -0
- package/dist/index.aws.js +59 -0
- package/dist/index.browser.d.ts +5 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +7 -0
- package/dist/index.cdk.d.ts +16 -0
- package/dist/index.cdk.d.ts.map +1 -0
- package/dist/index.cdk.js +103 -0
- package/dist/index.cdk.test.d.ts +2 -0
- package/dist/index.cdk.test.d.ts.map +1 -0
- package/dist/index.cdk.test.js +182 -0
- package/dist/index.mock.d.ts +39 -0
- package/dist/index.mock.d.ts.map +1 -0
- package/dist/index.mock.js +53 -0
- package/dist/migration-lambda.d.ts +5 -0
- package/dist/migration-lambda.d.ts.map +1 -0
- package/dist/migration-lambda.js +139 -0
- package/dist/migrations.d.ts +11 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +59 -0
- package/dist/transaction.d.ts +12 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +24 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/validation.d.ts +18 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +169 -0
- package/dist/validation.test.d.ts +2 -0
- package/dist/validation.test.d.ts.map +1 -0
- package/dist/validation.test.js +354 -0
- package/package.json +48 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DatabaseEngine, TransactionHandle } from '@aws-blocks/data-common';
|
|
2
|
+
export declare class DsqlMockEngine implements DatabaseEngine {
|
|
3
|
+
private db;
|
|
4
|
+
private closed;
|
|
5
|
+
private shouldConflict;
|
|
6
|
+
private _allowDdl;
|
|
7
|
+
constructor(dataDir: string);
|
|
8
|
+
/** Test helper: simulate OCC conflict on next commit. */
|
|
9
|
+
simulateConflict(): void;
|
|
10
|
+
/**
|
|
11
|
+
* Temporarily allow DDL statements (used by the migration runner).
|
|
12
|
+
* In normal app usage, DDL is rejected to match production behavior.
|
|
13
|
+
*/
|
|
14
|
+
withDdl<T>(fn: () => Promise<T>): Promise<T>;
|
|
15
|
+
query<T>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
16
|
+
execute(sql: string, params?: unknown[]): Promise<{
|
|
17
|
+
rowCount: number;
|
|
18
|
+
}>;
|
|
19
|
+
beginTransaction(): Promise<TransactionHandle>;
|
|
20
|
+
commitTransaction(handle: TransactionHandle): Promise<void>;
|
|
21
|
+
rollbackTransaction(handle: TransactionHandle): Promise<void>;
|
|
22
|
+
queryInTransaction<T>(handle: TransactionHandle, sql: string, params?: unknown[]): Promise<T[]>;
|
|
23
|
+
executeInTransaction(handle: TransactionHandle, sql: string, params?: unknown[]): Promise<{
|
|
24
|
+
rowCount: number;
|
|
25
|
+
}>;
|
|
26
|
+
destroy(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=dsql-mock-engine.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* PGlite engine wrapped with DSQL validation layer for local development.
|
|
5
|
+
*/
|
|
6
|
+
import { PGlite } from '@electric-sql/pglite';
|
|
7
|
+
import { existsSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { DistributedDatabaseErrors, PG_SERIALIZATION_FAILURE, translateDsqlError } from '../errors.js';
|
|
10
|
+
import { validateStatement, classifyStatement, TransactionTracker } from '../validation.js';
|
|
11
|
+
function cleanStaleLock(dataDir) {
|
|
12
|
+
const pidFile = join(dataDir, 'postmaster.pid');
|
|
13
|
+
if (existsSync(pidFile)) {
|
|
14
|
+
try {
|
|
15
|
+
unlinkSync(pidFile);
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Preprocess a SQL statement for execution on the DSQL mock (PGlite).
|
|
22
|
+
*
|
|
23
|
+
* 1. Validates the statement against DSQL compatibility rules (throws on
|
|
24
|
+
* unsupported features like FK, TRUNCATE, SERIAL, etc.).
|
|
25
|
+
* 2. Rejects DDL statements — in production the app Lambda only has
|
|
26
|
+
* dsql:DbConnect (DML-only). DDL must go in migration files.
|
|
27
|
+
* 3. Normalizes DSQL-only syntax that PGlite doesn't understand — currently
|
|
28
|
+
* just `CREATE [UNIQUE] INDEX ASYNC` (stripped to a synchronous CREATE INDEX).
|
|
29
|
+
*
|
|
30
|
+
* Returns the normalized SQL ready for PGlite execution.
|
|
31
|
+
*/
|
|
32
|
+
function preprocessSqlForDsqlMock(sql, { allowDdl = false } = {}) {
|
|
33
|
+
validateStatement(sql);
|
|
34
|
+
if (!allowDdl && classifyStatement(sql) === 'ddl') {
|
|
35
|
+
const err = new Error('DDL statements (CREATE, ALTER, DROP) are not allowed in the app runtime. ' +
|
|
36
|
+
'Use migration files instead — the migration Lambda has dsql:DbConnectAdmin for DDL.');
|
|
37
|
+
err.name = 'DsqlPermissionError';
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
return sql.replace(/\b(CREATE\s+(?:UNIQUE\s+)?INDEX)\s+ASYNC\b/gi, '$1');
|
|
41
|
+
}
|
|
42
|
+
export class DsqlMockEngine {
|
|
43
|
+
db;
|
|
44
|
+
closed = false;
|
|
45
|
+
shouldConflict = false;
|
|
46
|
+
_allowDdl = false;
|
|
47
|
+
constructor(dataDir) {
|
|
48
|
+
cleanStaleLock(dataDir);
|
|
49
|
+
mkdirSync(dataDir, { recursive: true });
|
|
50
|
+
this.db = new PGlite(dataDir);
|
|
51
|
+
}
|
|
52
|
+
/** Test helper: simulate OCC conflict on next commit. */
|
|
53
|
+
simulateConflict() { this.shouldConflict = true; }
|
|
54
|
+
/**
|
|
55
|
+
* Temporarily allow DDL statements (used by the migration runner).
|
|
56
|
+
* In normal app usage, DDL is rejected to match production behavior.
|
|
57
|
+
*/
|
|
58
|
+
async withDdl(fn) {
|
|
59
|
+
this._allowDdl = true;
|
|
60
|
+
try {
|
|
61
|
+
return await fn();
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
this._allowDdl = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async query(sql, params) {
|
|
68
|
+
const normalized = preprocessSqlForDsqlMock(sql, { allowDdl: this._allowDdl });
|
|
69
|
+
try {
|
|
70
|
+
return (await this.db.query(normalized, params)).rows;
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
translateDsqlError(e);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async execute(sql, params) {
|
|
77
|
+
const normalized = preprocessSqlForDsqlMock(sql, { allowDdl: this._allowDdl });
|
|
78
|
+
try {
|
|
79
|
+
return { rowCount: (await this.db.query(normalized, params)).affectedRows ?? 0 };
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
translateDsqlError(e);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async beginTransaction() {
|
|
86
|
+
await this.db.query('BEGIN');
|
|
87
|
+
return { active: true, tracker: new TransactionTracker() };
|
|
88
|
+
}
|
|
89
|
+
async commitTransaction(handle) {
|
|
90
|
+
if (this.shouldConflict) {
|
|
91
|
+
this.shouldConflict = false;
|
|
92
|
+
await this.db.query('ROLLBACK');
|
|
93
|
+
const err = Object.assign(new Error('SerializationFailureException: OCC conflict — transaction not committed.'), { code: PG_SERIALIZATION_FAILURE, name: DistributedDatabaseErrors.SerializationFailure });
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
await this.db.query('COMMIT');
|
|
97
|
+
handle.tracker.reset();
|
|
98
|
+
}
|
|
99
|
+
async rollbackTransaction(handle) {
|
|
100
|
+
await this.db.query('ROLLBACK');
|
|
101
|
+
handle.tracker.reset();
|
|
102
|
+
}
|
|
103
|
+
async queryInTransaction(handle, sql, params) {
|
|
104
|
+
const normalized = preprocessSqlForDsqlMock(sql, { allowDdl: this._allowDdl });
|
|
105
|
+
handle.tracker.recordStatement(sql);
|
|
106
|
+
try {
|
|
107
|
+
return (await this.db.query(normalized, params)).rows;
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
translateDsqlError(e);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async executeInTransaction(handle, sql, params) {
|
|
114
|
+
const normalized = preprocessSqlForDsqlMock(sql, { allowDdl: this._allowDdl });
|
|
115
|
+
const h = handle;
|
|
116
|
+
h.tracker.recordStatement(sql);
|
|
117
|
+
try {
|
|
118
|
+
const rowCount = (await this.db.query(normalized, params)).affectedRows ?? 0;
|
|
119
|
+
h.tracker.recordRowCount(rowCount);
|
|
120
|
+
return { rowCount };
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
translateDsqlError(e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async destroy() {
|
|
127
|
+
if (this.closed)
|
|
128
|
+
return;
|
|
129
|
+
this.closed = true;
|
|
130
|
+
await this.db.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSQL-specific error constants.
|
|
3
|
+
*/
|
|
4
|
+
export declare const DistributedDatabaseErrors: {
|
|
5
|
+
readonly QueryFailed: "QueryFailedException";
|
|
6
|
+
readonly ConnectionFailed: "ConnectionFailedException";
|
|
7
|
+
readonly TransactionFailed: "TransactionFailedException";
|
|
8
|
+
readonly UniqueConstraintViolation: "UniqueConstraintViolationException";
|
|
9
|
+
readonly SerializationFailure: "SerializationFailureException";
|
|
10
|
+
readonly TransactionRowLimitExceeded: "TransactionRowLimitExceededException";
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* PostgreSQL error codes used for DSQL error translation.
|
|
14
|
+
* @see https://www.postgresql.org/docs/current/errcodes-appendix.html
|
|
15
|
+
*/
|
|
16
|
+
/** Serialization failure — OCC conflict in DSQL. Class 40 (Transaction Rollback). */
|
|
17
|
+
export declare const PG_SERIALIZATION_FAILURE = "40001";
|
|
18
|
+
/** Unique constraint violation. Class 23 (Integrity Constraint Violation). */
|
|
19
|
+
export declare const PG_UNIQUE_VIOLATION = "23505";
|
|
20
|
+
/** Connection exception class prefix. Class 08 (Connection Exception). */
|
|
21
|
+
export declare const PG_CONNECTION_EXCEPTION_CLASS = "08";
|
|
22
|
+
/**
|
|
23
|
+
* Maximum rows mutated per DSQL transaction.
|
|
24
|
+
* @see https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-transactions.html
|
|
25
|
+
*/
|
|
26
|
+
export declare const TRANSACTION_ROW_LIMIT = 3000;
|
|
27
|
+
/** Translate a pg error code to a DistributedDatabaseErrors name. */
|
|
28
|
+
export declare function translateDsqlError(e: Error): never;
|
|
29
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,eAAO,MAAM,yBAAyB;;;;;;;CAO5B,CAAC;AAEX;;;GAGG;AAEH,qFAAqF;AACrF,eAAO,MAAM,wBAAwB,UAAU,CAAC;AAChD,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAC3C,0EAA0E;AAC1E,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAElD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,qEAAqE;AACrE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,CAYlD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* DSQL-specific error constants.
|
|
5
|
+
*/
|
|
6
|
+
export const DistributedDatabaseErrors = {
|
|
7
|
+
QueryFailed: 'QueryFailedException',
|
|
8
|
+
ConnectionFailed: 'ConnectionFailedException',
|
|
9
|
+
TransactionFailed: 'TransactionFailedException',
|
|
10
|
+
UniqueConstraintViolation: 'UniqueConstraintViolationException',
|
|
11
|
+
SerializationFailure: 'SerializationFailureException',
|
|
12
|
+
TransactionRowLimitExceeded: 'TransactionRowLimitExceededException',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* PostgreSQL error codes used for DSQL error translation.
|
|
16
|
+
* @see https://www.postgresql.org/docs/current/errcodes-appendix.html
|
|
17
|
+
*/
|
|
18
|
+
/** Serialization failure — OCC conflict in DSQL. Class 40 (Transaction Rollback). */
|
|
19
|
+
export const PG_SERIALIZATION_FAILURE = '40001';
|
|
20
|
+
/** Unique constraint violation. Class 23 (Integrity Constraint Violation). */
|
|
21
|
+
export const PG_UNIQUE_VIOLATION = '23505';
|
|
22
|
+
/** Connection exception class prefix. Class 08 (Connection Exception). */
|
|
23
|
+
export const PG_CONNECTION_EXCEPTION_CLASS = '08';
|
|
24
|
+
/**
|
|
25
|
+
* Maximum rows mutated per DSQL transaction.
|
|
26
|
+
* @see https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-transactions.html
|
|
27
|
+
*/
|
|
28
|
+
export const TRANSACTION_ROW_LIMIT = 3000;
|
|
29
|
+
/** Translate a pg error code to a DistributedDatabaseErrors name. */
|
|
30
|
+
export function translateDsqlError(e) {
|
|
31
|
+
const code = e.code;
|
|
32
|
+
if (code === PG_SERIALIZATION_FAILURE) {
|
|
33
|
+
e.name = DistributedDatabaseErrors.SerializationFailure;
|
|
34
|
+
}
|
|
35
|
+
else if (code === PG_UNIQUE_VIOLATION) {
|
|
36
|
+
e.name = DistributedDatabaseErrors.UniqueConstraintViolation;
|
|
37
|
+
}
|
|
38
|
+
else if (code && code.startsWith(PG_CONNECTION_EXCEPTION_CLASS)) {
|
|
39
|
+
e.name = DistributedDatabaseErrors.ConnectionFailed;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
e.name = DistributedDatabaseErrors.QueryFailed;
|
|
43
|
+
}
|
|
44
|
+
throw e;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../src/errors.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Unit tests for DSQL error translation.
|
|
5
|
+
*/
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { translateDsqlError, DistributedDatabaseErrors, PG_SERIALIZATION_FAILURE, PG_UNIQUE_VIOLATION, } from './errors.js';
|
|
9
|
+
test('translateDsqlError: serialization failure (40001) → SerializationFailure', () => {
|
|
10
|
+
const err = Object.assign(new Error('conflict'), { code: PG_SERIALIZATION_FAILURE });
|
|
11
|
+
assert.throws(() => translateDsqlError(err), (e) => {
|
|
12
|
+
assert.equal(e.name, DistributedDatabaseErrors.SerializationFailure);
|
|
13
|
+
assert.equal(e.message, 'conflict');
|
|
14
|
+
return true;
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
test('translateDsqlError: unique violation (23505) → UniqueConstraintViolation', () => {
|
|
18
|
+
const err = Object.assign(new Error('duplicate key'), { code: PG_UNIQUE_VIOLATION });
|
|
19
|
+
assert.throws(() => translateDsqlError(err), (e) => {
|
|
20
|
+
assert.equal(e.name, DistributedDatabaseErrors.UniqueConstraintViolation);
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
test('translateDsqlError: connection error (08006) → ConnectionFailed', () => {
|
|
25
|
+
const err = Object.assign(new Error('connection refused'), { code: '08006' });
|
|
26
|
+
assert.throws(() => translateDsqlError(err), (e) => {
|
|
27
|
+
assert.equal(e.name, DistributedDatabaseErrors.ConnectionFailed);
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
test('translateDsqlError: connection error (08001) → ConnectionFailed', () => {
|
|
32
|
+
const err = Object.assign(new Error('unable to connect'), { code: '08001' });
|
|
33
|
+
assert.throws(() => translateDsqlError(err), (e) => {
|
|
34
|
+
assert.equal(e.name, DistributedDatabaseErrors.ConnectionFailed);
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
test('translateDsqlError: unknown pg error code → QueryFailed', () => {
|
|
39
|
+
const err = Object.assign(new Error('syntax error'), { code: '42601' });
|
|
40
|
+
assert.throws(() => translateDsqlError(err), (e) => {
|
|
41
|
+
assert.equal(e.name, DistributedDatabaseErrors.QueryFailed);
|
|
42
|
+
return true;
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
test('translateDsqlError: Error without code → QueryFailed', () => {
|
|
46
|
+
const err = new Error('something broke');
|
|
47
|
+
assert.throws(() => translateDsqlError(err), (e) => {
|
|
48
|
+
assert.equal(e.name, DistributedDatabaseErrors.QueryFailed);
|
|
49
|
+
assert.equal(e.message, 'something broke');
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
test('translateDsqlError: always throws (never returns)', () => {
|
|
54
|
+
assert.throws(() => translateDsqlError(new Error('test')));
|
|
55
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DistributedDatabase — AWS Lambda runtime entry point.
|
|
3
|
+
* pg driver + IAM token authentication.
|
|
4
|
+
*/
|
|
5
|
+
import { Scope } from '@aws-blocks/core';
|
|
6
|
+
import type { ScopeParent } from '@aws-blocks/core';
|
|
7
|
+
import { type SqlQuery, type Transaction } from '@aws-blocks/data-common';
|
|
8
|
+
import type { DistributedDatabaseOptions, TransactionOptions } from './types.js';
|
|
9
|
+
import type { ChildLogger } from '@aws-blocks/bb-logger';
|
|
10
|
+
export declare class DistributedDatabase extends Scope {
|
|
11
|
+
private _base;
|
|
12
|
+
/** @internal Logger for internal operations. Defaults to error-level when not provided. */
|
|
13
|
+
protected log: ChildLogger;
|
|
14
|
+
constructor(scope: ScopeParent, id: string, _options?: DistributedDatabaseOptions);
|
|
15
|
+
private get base();
|
|
16
|
+
query<T>(query: SqlQuery): Promise<T[]>;
|
|
17
|
+
queryOne<T>(query: SqlQuery): Promise<T | null>;
|
|
18
|
+
execute(query: SqlQuery): Promise<{
|
|
19
|
+
rowCount: number;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Execute a function within a transaction with optional OCC retry.
|
|
23
|
+
*
|
|
24
|
+
* DSQL uses Optimistic Concurrency Control. Commit may fail with
|
|
25
|
+
* SerializationFailureException if another transaction modified the same rows.
|
|
26
|
+
*/
|
|
27
|
+
transaction<T>(fn: (tx: Transaction) => Promise<T>, options?: TransactionOptions): Promise<T>;
|
|
28
|
+
/** @internal */
|
|
29
|
+
getEngine(): import("@aws-blocks/data-common").DatabaseEngine;
|
|
30
|
+
}
|
|
31
|
+
export { sql, createKyselyAdapter } from '@aws-blocks/data-common';
|
|
32
|
+
export type { SqlQuery, Transaction } from '@aws-blocks/data-common';
|
|
33
|
+
export { DistributedDatabaseErrors } from './errors.js';
|
|
34
|
+
export type { DistributedDatabaseOptions, TransactionOptions } from './types.js';
|
|
35
|
+
//# sourceMappingURL=index.aws.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.aws.d.ts","sourceRoot":"","sources":["../src/index.aws.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAA0B,MAAM,kBAAkB,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAgB,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAIxF,OAAO,KAAK,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,OAAO,CAAC,KAAK,CAA6B;IAE1C,2FAA2F;IAC3F,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC;gBAEf,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,0BAA0B;IAQjF,OAAO,KAAK,IAAI,GAiBf;IAED,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IACvC,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAC/C,OAAO,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAEvD;;;;;OAKG;IACG,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,CAAC,CAAC;IAInG,gBAAgB;IAChB,SAAS;CACV;AAED,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,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* DistributedDatabase — AWS Lambda runtime entry point.
|
|
5
|
+
* pg driver + IAM token authentication.
|
|
6
|
+
*/
|
|
7
|
+
import { Scope, registerSdkIdentifiers } from '@aws-blocks/core';
|
|
8
|
+
import { DatabaseBase } from '@aws-blocks/data-common';
|
|
9
|
+
import { DsqlSigner } from '@aws-sdk/dsql-signer';
|
|
10
|
+
import { DsqlEngine } from './engines/dsql-engine.js';
|
|
11
|
+
import { transactionWithRetry } from './transaction.js';
|
|
12
|
+
import { ENV_SANITIZE, sanitizeDbRoleName } from './constants.js';
|
|
13
|
+
import { Logger } from '@aws-blocks/bb-logger';
|
|
14
|
+
export class DistributedDatabase extends Scope {
|
|
15
|
+
_base = null;
|
|
16
|
+
/** @internal Logger for internal operations. Defaults to error-level when not provided. */
|
|
17
|
+
log;
|
|
18
|
+
constructor(scope, id, _options) {
|
|
19
|
+
super(id, { parent: scope });
|
|
20
|
+
this.log = _options?.logger ?? new Logger(this, 'logger', { level: 'error' });
|
|
21
|
+
const envName = this.fullId.replace(ENV_SANITIZE, '_');
|
|
22
|
+
const clusterEndpoint = process.env[`BLOCKS_${envName}_ENDPOINT`] ?? '';
|
|
23
|
+
registerSdkIdentifiers(this.fullId, { clusterEndpoint });
|
|
24
|
+
}
|
|
25
|
+
get base() {
|
|
26
|
+
if (!this._base) {
|
|
27
|
+
const envName = this.fullId.replace(ENV_SANITIZE, '_');
|
|
28
|
+
const endpoint = process.env[`BLOCKS_${envName}_ENDPOINT`];
|
|
29
|
+
const region = process.env[`BLOCKS_${envName}_REGION`];
|
|
30
|
+
if (!endpoint || !region) {
|
|
31
|
+
throw new Error(`Missing env: BLOCKS_${envName}_ENDPOINT / BLOCKS_${envName}_REGION`);
|
|
32
|
+
}
|
|
33
|
+
const dbRole = sanitizeDbRoleName(this.fullId);
|
|
34
|
+
const signer = new DsqlSigner({ hostname: endpoint, region });
|
|
35
|
+
this._base = new DatabaseBase(new DsqlEngine({
|
|
36
|
+
endpoint, region,
|
|
37
|
+
role: dbRole,
|
|
38
|
+
getAuthToken: () => signer.getDbConnectAuthToken(),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
return this._base;
|
|
42
|
+
}
|
|
43
|
+
query(query) { return this.base.query(query); }
|
|
44
|
+
queryOne(query) { return this.base.queryOne(query); }
|
|
45
|
+
execute(query) { return this.base.execute(query); }
|
|
46
|
+
/**
|
|
47
|
+
* Execute a function within a transaction with optional OCC retry.
|
|
48
|
+
*
|
|
49
|
+
* DSQL uses Optimistic Concurrency Control. Commit may fail with
|
|
50
|
+
* SerializationFailureException if another transaction modified the same rows.
|
|
51
|
+
*/
|
|
52
|
+
async transaction(fn, options) {
|
|
53
|
+
return transactionWithRetry(this.base, fn, options);
|
|
54
|
+
}
|
|
55
|
+
/** @internal */
|
|
56
|
+
getEngine() { return this.base.getEngine(); }
|
|
57
|
+
}
|
|
58
|
+
export { sql, createKyselyAdapter } from '@aws-blocks/data-common';
|
|
59
|
+
export { DistributedDatabaseErrors } from './errors.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.browser.d.ts","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAIA,qBAAa,mBAAmB;gBAClB,GAAG,KAAK,EAAE,GAAG,EAAE;CAC5B;AAED,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Browser stub — DistributedDatabase runs server-side only
|
|
4
|
+
export class DistributedDatabase {
|
|
5
|
+
constructor(..._args) { }
|
|
6
|
+
}
|
|
7
|
+
export { DistributedDatabaseErrors } from './errors.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DistributedDatabase — CDK infrastructure entry point.
|
|
3
|
+
* Provisions Aurora DSQL cluster via CloudFormation.
|
|
4
|
+
* Optionally runs migrations via a CustomResource Lambda.
|
|
5
|
+
*/
|
|
6
|
+
import { Scope } from '@aws-blocks/core/cdk';
|
|
7
|
+
import type { ScopeParent } from '@aws-blocks/core';
|
|
8
|
+
import type { DistributedDatabaseOptions } from './types.js';
|
|
9
|
+
export declare class DistributedDatabase extends Scope {
|
|
10
|
+
constructor(scope: ScopeParent, id: string, options?: DistributedDatabaseOptions);
|
|
11
|
+
}
|
|
12
|
+
export { sql, createKyselyAdapter } from '@aws-blocks/data-common';
|
|
13
|
+
export type { SqlQuery, Transaction } from '@aws-blocks/data-common';
|
|
14
|
+
export { DistributedDatabaseErrors } from './errors.js';
|
|
15
|
+
export type { DistributedDatabaseOptions } from './types.js';
|
|
16
|
+
//# sourceMappingURL=index.cdk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAwB,MAAM,sBAAsB,CAAC;AACnE,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;CAwFjF;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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* DistributedDatabase — CDK infrastructure entry point.
|
|
5
|
+
* Provisions Aurora DSQL cluster via CloudFormation.
|
|
6
|
+
* Optionally runs migrations via a CustomResource Lambda.
|
|
7
|
+
*/
|
|
8
|
+
import { Scope, DEFAULT_NODE_RUNTIME } from '@aws-blocks/core/cdk';
|
|
9
|
+
import * as cdk from 'aws-cdk-lib';
|
|
10
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
11
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
12
|
+
import * as cr from 'aws-cdk-lib/custom-resources';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join, resolve } from 'node:path';
|
|
16
|
+
import { LAMBDA_MIGRATIONS_DIR, MIGRATION_LAMBDA_TIMEOUT_MINUTES, ENV_SANITIZE, sanitizeDbRoleName } from './constants.js';
|
|
17
|
+
export class DistributedDatabase extends Scope {
|
|
18
|
+
constructor(scope, id, options) {
|
|
19
|
+
super(id, { parent: scope });
|
|
20
|
+
const stack = cdk.Stack.of(this);
|
|
21
|
+
const isSandbox = stack.node.tryGetContext('sandboxMode') === 'true';
|
|
22
|
+
const envName = this.fullId.replace(ENV_SANITIZE, '_');
|
|
23
|
+
const region = stack.region;
|
|
24
|
+
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;
|
|
28
|
+
const cluster = new cdk.CfnResource(stack, `${this.fullId}DsqlCluster`, {
|
|
29
|
+
type: 'AWS::DSQL::Cluster',
|
|
30
|
+
properties: {
|
|
31
|
+
DeletionProtectionEnabled: removalPolicy !== cdk.RemovalPolicy.DESTROY,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
cluster.applyRemovalPolicy(removalPolicy);
|
|
35
|
+
const endpoint = cluster.getAtt('Endpoint').toString();
|
|
36
|
+
// Env vars for runtime
|
|
37
|
+
this.handler.addEnvironment(`BLOCKS_${envName}_ENDPOINT`, endpoint);
|
|
38
|
+
this.handler.addEnvironment(`BLOCKS_${envName}_REGION`, region);
|
|
39
|
+
// IAM grant — app Lambda gets DML-only access via custom DB role (least privilege)
|
|
40
|
+
this.handler.addToRolePolicy(new iam.PolicyStatement({
|
|
41
|
+
actions: ['dsql:DbConnect'],
|
|
42
|
+
resources: [`arn:aws:dsql:${region}:${stack.account}:cluster/${cluster.ref}`],
|
|
43
|
+
}));
|
|
44
|
+
new cdk.CfnOutput(stack, `${this.fullId}DsqlEndpoint`, { value: endpoint });
|
|
45
|
+
// The app Lambda's IAM role ARN is needed to map the custom DB role.
|
|
46
|
+
const appRoleArn = this.handler.role.roleArn;
|
|
47
|
+
// Resolve migrations path if provided
|
|
48
|
+
const resolvedMigrationsPath = options?.migrationsPath ? resolve(options.migrationsPath) : undefined;
|
|
49
|
+
const migrationsHash = resolvedMigrationsPath ? hashMigrationsDir(resolvedMigrationsPath) : 'no-migrations';
|
|
50
|
+
// Migration/provisioning Lambda — always created to provision the app DB role.
|
|
51
|
+
// Also runs .sql migrations when migrationsPath is provided.
|
|
52
|
+
const migrationFn = new lambda.NodejsFunction(stack, `${this.fullId}DsqlMigrationFn`, {
|
|
53
|
+
// Points at the compiled migration-lambda.js in dist/ (same directory as this file at runtime).
|
|
54
|
+
// Must NOT use ../src/migration-lambda.ts — src/ is excluded from the published package.
|
|
55
|
+
entry: join(import.meta.dirname ?? new URL('.', import.meta.url).pathname, 'migration-lambda.js'),
|
|
56
|
+
handler: 'handler',
|
|
57
|
+
runtime: DEFAULT_NODE_RUNTIME,
|
|
58
|
+
timeout: cdk.Duration.minutes(MIGRATION_LAMBDA_TIMEOUT_MINUTES),
|
|
59
|
+
environment: {
|
|
60
|
+
DSQL_ENDPOINT: endpoint,
|
|
61
|
+
DSQL_REGION: region,
|
|
62
|
+
MIGRATIONS_DIR: LAMBDA_MIGRATIONS_DIR,
|
|
63
|
+
APP_ROLE_ARN: appRoleArn,
|
|
64
|
+
DB_ROLE_NAME: dbRole,
|
|
65
|
+
},
|
|
66
|
+
bundling: {
|
|
67
|
+
commandHooks: {
|
|
68
|
+
beforeBundling: () => [],
|
|
69
|
+
beforeInstall: () => [],
|
|
70
|
+
afterBundling: (_inputDir, outputDir) => resolvedMigrationsPath
|
|
71
|
+
? [`cp -r ${resolvedMigrationsPath} ${outputDir}${LAMBDA_MIGRATIONS_DIR.replace('/var/task', '')}`]
|
|
72
|
+
: [],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
// Migration Lambda needs Admin access (DDL + role management)
|
|
77
|
+
migrationFn.addToRolePolicy(new iam.PolicyStatement({
|
|
78
|
+
actions: ['dsql:DbConnectAdmin'],
|
|
79
|
+
resources: [`arn:aws:dsql:${region}:${stack.account}:cluster/${cluster.ref}`],
|
|
80
|
+
}));
|
|
81
|
+
const provider = new cr.Provider(stack, `${this.fullId}DsqlMigrationProvider`, {
|
|
82
|
+
onEventHandler: migrationFn,
|
|
83
|
+
});
|
|
84
|
+
const migrationCR = new cdk.CustomResource(stack, `${this.fullId}DsqlMigrationCR`, {
|
|
85
|
+
serviceToken: provider.serviceToken,
|
|
86
|
+
properties: { migrationsHash, dbRole },
|
|
87
|
+
});
|
|
88
|
+
// Ensure migrations run after cluster is created
|
|
89
|
+
migrationCR.node.addDependency(cluster);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Hash all .sql files in a directory to detect changes. */
|
|
93
|
+
function hashMigrationsDir(dir) {
|
|
94
|
+
const hash = createHash('sha256');
|
|
95
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.sql')).sort();
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
hash.update(file);
|
|
98
|
+
hash.update(readFileSync(join(dir, file), 'utf-8'));
|
|
99
|
+
}
|
|
100
|
+
return hash.digest('hex').slice(0, 16);
|
|
101
|
+
}
|
|
102
|
+
export { sql, createKyselyAdapter } from '@aws-blocks/data-common';
|
|
103
|
+
export { DistributedDatabaseErrors } from './errors.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cdk.test.d.ts","sourceRoot":"","sources":["../src/index.cdk.test.ts"],"names":[],"mappings":""}
|