@aws/nx-plugin 0.111.0 → 0.112.1

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 (33) hide show
  1. package/README.md +1 -0
  2. package/generators.json +6 -0
  3. package/package.json +1 -1
  4. package/src/preset/__snapshots__/generator.spec.ts.snap +2 -0
  5. package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +4822 -0
  6. package/src/ts/rdb/files/Dockerfile.template +15 -0
  7. package/src/ts/rdb/files/prisma/models/example.prisma.template +5 -0
  8. package/src/ts/rdb/files/prisma/schema.prisma.template +9 -0
  9. package/src/ts/rdb/files/prisma.config.ts.template +24 -0
  10. package/src/ts/rdb/files/scripts/docker-pull.ts.template +15 -0
  11. package/src/ts/rdb/files/scripts/docker-start.ts.template +81 -0
  12. package/src/ts/rdb/files/scripts/wait-for-db.ts.template +55 -0
  13. package/src/ts/rdb/files/src/constants.ts.template +8 -0
  14. package/src/ts/rdb/files/src/create-db-user-handler.ts.template +128 -0
  15. package/src/ts/rdb/files/src/index.ts.template +2 -0
  16. package/src/ts/rdb/files/src/migration-handler.ts.template +54 -0
  17. package/src/ts/rdb/files/src/prisma.ts.template +104 -0
  18. package/src/ts/rdb/files/src/utils.ts.template +145 -0
  19. package/src/ts/rdb/generator.d.ts +10 -0
  20. package/src/ts/rdb/generator.js +206 -0
  21. package/src/ts/rdb/generator.js.map +1 -0
  22. package/src/ts/rdb/schema.d.ts +17 -0
  23. package/src/ts/rdb/schema.json +73 -0
  24. package/src/utils/rdb-constructs/files/cdk/app/dbs/__nameKebabCase__.ts.template +43 -0
  25. package/src/utils/rdb-constructs/files/cdk/core/rdb/aurora.ts.template +380 -0
  26. package/src/utils/rdb-constructs/files/terraform/app/dbs/__nameKebabCase__/__nameKebabCase__.tf.template +732 -0
  27. package/src/utils/rdb-constructs/files/terraform/core/rdb/aurora/aurora.tf.template +742 -0
  28. package/src/utils/rdb-constructs/rdb-constructs.d.ts +23 -0
  29. package/src/utils/rdb-constructs/rdb-constructs.js +59 -0
  30. package/src/utils/rdb-constructs/rdb-constructs.js.map +1 -0
  31. package/src/utils/versions.d.ts +12 -1
  32. package/src/utils/versions.js +11 -0
  33. package/src/utils/versions.js.map +1 -1
@@ -0,0 +1,15 @@
1
+ FROM public.ecr.aws/lambda/nodejs:24
2
+
3
+ WORKDIR ${LAMBDA_TASK_ROOT}
4
+
5
+ RUN npm install prisma@<%= prismaVersion %>
6
+
7
+ COPY index.js ./index.js
8
+ COPY prisma ./prisma
9
+ COPY prisma.config.ts ./prisma.config.ts
10
+
11
+ RUN curl -fsSL "https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem" \
12
+ -o /etc/pki/ca-trust/source/anchors/rds-bundle.pem && \
13
+ update-ca-trust
14
+
15
+ CMD ["index.handler"]
@@ -0,0 +1,5 @@
1
+ model ExampleTable {
2
+ id Int @id @default(autoincrement())
3
+ column1 String
4
+ column2 String
5
+ }
@@ -0,0 +1,9 @@
1
+ generator client {
2
+ provider = "prisma-client"
3
+ output = "../generated/prisma"
4
+ }
5
+
6
+ datasource db {
7
+ provider = "<%= databaseProvider %>"
8
+ }
9
+
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from 'prisma/config';
2
+
3
+ const getDatabaseUrl = async () => {
4
+ if (process.env.SERVE_LOCAL === 'true') {
5
+ const { LOCAL_DB_HOST, LOCAL_DB_NAME, LOCAL_DB_PASSWORD, LOCAL_DB_PORT, LOCAL_DB_USER } =
6
+ await import('./src/constants.js');
7
+ <%_ if (engine === 'MySQL') { _%>
8
+ return `mysql://${LOCAL_DB_USER}:${LOCAL_DB_PASSWORD}@${LOCAL_DB_HOST}:${LOCAL_DB_PORT}/${LOCAL_DB_NAME}`;
9
+ <%_ } else { _%>
10
+ return `postgresql://${LOCAL_DB_USER}:${LOCAL_DB_PASSWORD}@${LOCAL_DB_HOST}:${LOCAL_DB_PORT}/${LOCAL_DB_NAME}`;
11
+ <%_ } _%>
12
+ }
13
+ return process.env.DATABASE_URL;
14
+ };
15
+
16
+ export default defineConfig({
17
+ schema: 'prisma/',
18
+ migrations: {
19
+ path: 'prisma/migrations',
20
+ },
21
+ datasource: {
22
+ url: await getDatabaseUrl(),
23
+ },
24
+ });
@@ -0,0 +1,15 @@
1
+ import Docker from 'dockerode';
2
+ import { promisify } from 'util';
3
+
4
+ const docker = new Docker();
5
+ const image = process.argv[2];
6
+
7
+ try {
8
+ await docker.getImage(image).inspect();
9
+ } catch (e) {
10
+ if ((e as { statusCode?: number }).statusCode !== 404) throw e;
11
+ const stream = (await promisify(docker.pull.bind(docker))(
12
+ image,
13
+ )) as NodeJS.ReadableStream;
14
+ await promisify(docker.modem.followProgress.bind(docker.modem))(stream);
15
+ }
@@ -0,0 +1,81 @@
1
+ import Docker from 'dockerode';
2
+
3
+ <%_ if (engine === 'MySQL') { _%>
4
+ const [containerName, image, hostPort, dbName, dbPassword] =
5
+ process.argv.slice(2);
6
+ <%_ } else { _%>
7
+ const [containerName, image, hostPort, dbName, dbUser, dbPassword] =
8
+ process.argv.slice(2);
9
+ <%_ } _%>
10
+
11
+ const docker = new Docker();
12
+
13
+ let container: Docker.Container;
14
+
15
+ try {
16
+ const existing = docker.getContainer(containerName);
17
+ const info = await existing.inspect();
18
+ container = existing;
19
+ if (!info.State.Running) {
20
+ await container.start();
21
+ }
22
+ } catch (e) {
23
+ if ((e as { statusCode?: number }).statusCode !== 404) throw e;
24
+ container = await docker.createContainer({
25
+ name: containerName,
26
+ <%_ if (engine === 'MySQL') { _%>
27
+ Image: image,
28
+ Env: [
29
+ `MYSQL_DATABASE=${dbName}`,
30
+ `MYSQL_ROOT_PASSWORD=${dbPassword}`,
31
+ ],
32
+ ExposedPorts: { '3306/tcp': {} },
33
+ HostConfig: {
34
+ AutoRemove: true,
35
+ PortBindings: { '3306/tcp': [{ HostPort: hostPort }] },
36
+ Binds: [`${containerName}-data:/var/lib/mysql`],
37
+ },
38
+ <%_ } else { _%>
39
+ Image: image,
40
+ Env: [
41
+ `POSTGRES_DB=${dbName}`,
42
+ `POSTGRES_USER=${dbUser}`,
43
+ `POSTGRES_PASSWORD=${dbPassword}`,
44
+ ],
45
+ ExposedPorts: { '5432/tcp': {} },
46
+ HostConfig: {
47
+ AutoRemove: true,
48
+ PortBindings: { '5432/tcp': [{ HostPort: hostPort }] },
49
+ Binds: [`${containerName}-data:/var/lib/postgresql`],
50
+ },
51
+ <%_ } _%>
52
+ });
53
+ await container.start();
54
+ }
55
+
56
+ const stream = await container.attach({
57
+ stream: true,
58
+ stdout: true,
59
+ stderr: true,
60
+ });
61
+ container.modem.demuxStream(stream, process.stdout, process.stderr);
62
+
63
+ let exiting = false;
64
+
65
+ async function cleanup() {
66
+ if (exiting) return;
67
+ exiting = true;
68
+ try {
69
+ await container.stop();
70
+ } catch (e) {
71
+ if ((e as { statusCode?: number }).statusCode !== 404) console.error(e);
72
+ }
73
+ process.exit(0);
74
+ }
75
+
76
+ process.on('SIGTERM', () => void cleanup());
77
+ process.on('SIGINT', () => void cleanup());
78
+ process.on('SIGHUP', () => void cleanup());
79
+
80
+ const { StatusCode } = await container.wait();
81
+ if (!exiting) process.exit(StatusCode);
@@ -0,0 +1,55 @@
1
+ <%_ if (engine === 'MySQL') { _%>
2
+ import { createConnection, Connection } from 'mariadb';
3
+ <%_ } else { _%>
4
+ import { Client } from 'pg';
5
+ <%_ } _%>
6
+
7
+ const noop = () => undefined;
8
+
9
+ const [portArg, dbArg, userArg, passwordArg] = process.argv.slice(2);
10
+ const timeoutAt = Date.now() + 60000;
11
+
12
+ while (Date.now() < timeoutAt) {
13
+ <%_ if (engine === 'MySQL') { _%>
14
+ let conn: Connection | undefined;
15
+ try {
16
+ conn = await createConnection({
17
+ host: 'localhost',
18
+ port: parseInt(portArg),
19
+ user: userArg,
20
+ password: passwordArg,
21
+ database: dbArg,
22
+ connectTimeout: 500,
23
+ allowPublicKeyRetrieval: true,
24
+ });
25
+ await conn.end();
26
+ console.log('Database is ready.');
27
+ process.exit(0);
28
+ } catch {
29
+ console.log('Database is not ready.');
30
+ await conn?.end().catch(noop);
31
+ }
32
+ <%_ } else { _%>
33
+ const client = new Client({
34
+ host: 'localhost',
35
+ port: parseInt(portArg),
36
+ user: userArg,
37
+ password: passwordArg,
38
+ database: dbArg,
39
+ connectionTimeoutMillis: 500,
40
+ });
41
+ client.on('error', noop);
42
+ try {
43
+ await client.connect();
44
+ await client.end();
45
+ console.log('Database is ready.');
46
+ process.exit(0);
47
+ } catch {
48
+ console.log('Database is not ready.');
49
+ await client.end().catch(noop);
50
+ }
51
+ <%_ } _%>
52
+ await new Promise((r) => setTimeout(r, 200));
53
+ }
54
+
55
+ throw new Error(`Timed out waiting for <%= engine === 'MySQL' ? 'mysql' : 'postgres' %> on port ${portArg}`);
@@ -0,0 +1,8 @@
1
+ export const DB_PACKAGE_NAME = '<%= runtimeConfigKey %>';
2
+
3
+ // Local development connection details (used when SERVE_LOCAL=true, see serve-local Nx target)
4
+ export const LOCAL_DB_PORT = <%= localDbPort %>;
5
+ export const LOCAL_DB_HOST = '<%= localDbHost %>';
6
+ export const LOCAL_DB_NAME = '<%= localDbName %>';
7
+ export const LOCAL_DB_USER = '<%= localDbUser %>';
8
+ export const LOCAL_DB_PASSWORD = '<%= localDbPassword %>';
@@ -0,0 +1,128 @@
1
+ import type { CloudFormationCustomResourceEvent } from 'aws-lambda';
2
+ import { randomBytes } from 'node:crypto';
3
+ <%_ if (engine === 'MySQL') { _%>
4
+ import { createPool, type PoolConnection } from 'mariadb';
5
+ import { getDatabaseSecret, withConnectionRetry } from './utils.js';
6
+ <%_ } else { _%>
7
+ import { Client, escapeIdentifier } from 'pg';
8
+ import { getDatabaseSecret, withConnectionRetry } from './utils.js';
9
+ <%_ } _%>
10
+
11
+ type OnEventResult = {
12
+ PhysicalResourceId: string;
13
+ Data?: Record<string, unknown>;
14
+ };
15
+
16
+ const physicalResourceIdPrefix = 'db-user:';
17
+
18
+ const resolveDbUser = (physicalResourceId?: string): string =>
19
+ physicalResourceId?.startsWith(physicalResourceIdPrefix)
20
+ ? physicalResourceId.slice(physicalResourceIdPrefix.length)
21
+ : `db_${randomBytes(8).toString('hex')}`;
22
+
23
+ const ensureDatabaseUser = async (dbUser: string): Promise<void> => {
24
+ <%_ if (engine === 'MySQL') { _%>
25
+ const { dbname, username, password, host, port } = await getDatabaseSecret();
26
+ const pool = createPool({
27
+ host,
28
+ port,
29
+ database: dbname,
30
+ user: username,
31
+ password,
32
+ ssl: {
33
+ rejectUnauthorized: true,
34
+ },
35
+ connectionLimit: 1,
36
+ multipleStatements: true,
37
+ connectTimeout: 10_000,
38
+ });
39
+
40
+ const quotedDbName = pool.escapeId(dbname);
41
+ const quotedUser = pool.escape(dbUser);
42
+ const quotedHost = pool.escape('%');
43
+
44
+ let connection: PoolConnection | undefined;
45
+
46
+ try {
47
+ connection = await pool.getConnection();
48
+ await connection.query(
49
+ [
50
+ `CREATE USER IF NOT EXISTS ${quotedUser}@${quotedHost} IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'`,
51
+ `ALTER USER ${quotedUser}@${quotedHost} IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS' REQUIRE SSL`,
52
+ `GRANT ALL PRIVILEGES ON ${quotedDbName}.* TO ${quotedUser}@${quotedHost}`,
53
+ ].join(';\n'),
54
+ );
55
+ } finally {
56
+ await connection?.release();
57
+ await pool.end();
58
+ }
59
+ <%_ } else { _%>
60
+ const { dbname, username, password, host, port } = await getDatabaseSecret();
61
+ const quotedDbUser = escapeIdentifier(dbUser);
62
+ const quotedDbName = escapeIdentifier(dbname);
63
+ const client = new Client({
64
+ host,
65
+ port,
66
+ database: dbname,
67
+ user: username,
68
+ password,
69
+ ssl: {
70
+ rejectUnauthorized: true,
71
+ },
72
+ connectionTimeoutMillis: 10_000,
73
+ });
74
+
75
+ await client.connect();
76
+
77
+ try {
78
+ await client.query('BEGIN');
79
+
80
+ const roleExists = await client.query(
81
+ 'SELECT 1 FROM pg_roles WHERE rolname = $1',
82
+ [dbUser],
83
+ );
84
+ if (roleExists.rowCount === 0) {
85
+ await client.query(`CREATE ROLE ${quotedDbUser} WITH LOGIN;`);
86
+ }
87
+
88
+ await client.query(
89
+ `ALTER ROLE ${quotedDbUser} WITH LOGIN;
90
+ GRANT ALL PRIVILEGES ON DATABASE ${quotedDbName} TO ${quotedDbUser};
91
+ GRANT USAGE, CREATE ON SCHEMA public TO ${quotedDbUser};
92
+ GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${quotedDbUser};
93
+ GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${quotedDbUser};
94
+ GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ${quotedDbUser};
95
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO ${quotedDbUser};
96
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO ${quotedDbUser};
97
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO ${quotedDbUser};
98
+ GRANT rds_iam TO ${quotedDbUser};`,
99
+ );
100
+
101
+ await client.query('COMMIT');
102
+ } catch (error) {
103
+ await client.query('ROLLBACK');
104
+ throw error;
105
+ } finally {
106
+ await client.end();
107
+ }
108
+ <%_ } _%>
109
+ };
110
+
111
+ export const handler = async (
112
+ event: CloudFormationCustomResourceEvent,
113
+ ): Promise<OnEventResult> => {
114
+ const dbUser = resolveDbUser(
115
+ 'PhysicalResourceId' in event ? event.PhysicalResourceId : undefined,
116
+ );
117
+
118
+ if (event.RequestType !== 'Delete') {
119
+ await withConnectionRetry(() => ensureDatabaseUser(dbUser));
120
+ }
121
+
122
+ return {
123
+ PhysicalResourceId: `${physicalResourceIdPrefix}${dbUser}`,
124
+ Data: {
125
+ dbUser,
126
+ },
127
+ };
128
+ };
@@ -0,0 +1,2 @@
1
+ export { DB_PACKAGE_NAME } from './constants.js';
2
+ export { getPrisma } from './prisma.js';
@@ -0,0 +1,54 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ <%_ if (engine === 'MySQL') { _%>
4
+ import { getDatabaseSecret, withConnectionRetry } from './utils.js';
5
+ <%_ } else { _%>
6
+ import { Signer } from '@aws-sdk/rds-signer';
7
+ import { withConnectionRetry } from './utils.js';
8
+ <%_ } _%>
9
+
10
+ const buildDatabaseUrl = async (): Promise<string> => {
11
+ <%_ if (engine === 'MySQL') { _%>
12
+ const { host, port, dbname, username, password } = await getDatabaseSecret();
13
+
14
+ return (
15
+ `mysql://${encodeURIComponent(username)}` +
16
+ `:${encodeURIComponent(password)}` +
17
+ `@${host}:${port}/${dbname}` +
18
+ `?sslaccept=strict`
19
+ );
20
+ <%_ } else { _%>
21
+ const hostname = process.env.HOSTNAME!;
22
+ const port = process.env.PORT!;
23
+ const database = process.env.DATABASE!;
24
+ const dbUser = process.env.DBUSER!;
25
+ const region = process.env.AWS_REGION!;
26
+
27
+ const iamAuthToken = await new Signer({
28
+ hostname,
29
+ port: Number(port),
30
+ region,
31
+ username: dbUser,
32
+ }).getAuthToken();
33
+
34
+ return (
35
+ `postgresql://${encodeURIComponent(dbUser)}` +
36
+ `:${encodeURIComponent(iamAuthToken)}` +
37
+ `@${hostname}:${port}/${database}` +
38
+ `?sslaccept=strict` // `sslaccept` is the correct parameter here for postgresql. The Prisma CLI Rust engine does not use the standard libpq `sslmode` option.
39
+ );
40
+ <%_ } _%>
41
+ };
42
+
43
+ export const handler = async () => {
44
+ await withConnectionRetry(async () => {
45
+ const databaseUrl = await buildDatabaseUrl();
46
+ await promisify(execFile)('npx', ['prisma', 'migrate', 'deploy'], {
47
+ cwd: __dirname,
48
+ env: {
49
+ ...process.env,
50
+ DATABASE_URL: databaseUrl,
51
+ },
52
+ });
53
+ });
54
+ };
@@ -0,0 +1,104 @@
1
+ import { <%= prismaAdapterClassName %> } from '<%= prismaAdapterPackage %>';
2
+ import { Signer } from '@aws-sdk/rds-signer';
3
+ <%_ if (engine === 'MySQL') { _%>
4
+ import { PrismaClient } from '../generated/prisma/client.js';
5
+ <%_ } else { _%>
6
+ import { Pool } from 'pg';
7
+ import { PrismaClient } from '../generated/prisma/client.js';
8
+ <%_ } _%>
9
+ import {
10
+ DB_PACKAGE_NAME,
11
+ LOCAL_DB_HOST,
12
+ LOCAL_DB_NAME,
13
+ LOCAL_DB_PASSWORD,
14
+ LOCAL_DB_PORT,
15
+ LOCAL_DB_USER,
16
+ } from './constants.js';
17
+ import { getDatabaseConfig } from './utils.js';
18
+ <%_ if (engine === 'MySQL') { _%>
19
+
20
+ export const getPrisma = async (): Promise<PrismaClient> => {
21
+ if (process.env.SERVE_LOCAL === 'true') {
22
+ const adapter = new PrismaMariaDb({
23
+ host: LOCAL_DB_HOST,
24
+ port: LOCAL_DB_PORT,
25
+ database: LOCAL_DB_NAME,
26
+ user: LOCAL_DB_USER,
27
+ password: LOCAL_DB_PASSWORD,
28
+ allowPublicKeyRetrieval: true,
29
+ });
30
+ return new PrismaClient({ adapter });
31
+ }
32
+
33
+ const { hostname, port, database, dbUser, region } =
34
+ await getDatabaseConfig(DB_PACKAGE_NAME);
35
+ const iamAuthToken = await new Signer({
36
+ hostname,
37
+ port,
38
+ region,
39
+ username: dbUser,
40
+ }).getAuthToken();
41
+
42
+ const adapter = new PrismaMariaDb({
43
+ host: hostname,
44
+ port,
45
+ database,
46
+ user: dbUser,
47
+ password: iamAuthToken,
48
+ ssl: {
49
+ rejectUnauthorized: true,
50
+ },
51
+ });
52
+
53
+ return new PrismaClient({ adapter });
54
+ };
55
+ <%_ } else { _%>
56
+
57
+ let prismaPromise: Promise<PrismaClient> | undefined;
58
+
59
+ export const getPrisma = (): Promise<PrismaClient> => {
60
+ prismaPromise ??= (async () => {
61
+ if (process.env.SERVE_LOCAL === 'true') {
62
+ const adapter = new PrismaPg(
63
+ new Pool({
64
+ host: LOCAL_DB_HOST,
65
+ port: LOCAL_DB_PORT,
66
+ database: LOCAL_DB_NAME,
67
+ user: LOCAL_DB_USER,
68
+ password: LOCAL_DB_PASSWORD,
69
+ allowExitOnIdle: true,
70
+ }),
71
+ );
72
+ return new PrismaClient({ adapter });
73
+ }
74
+
75
+ const { hostname, port, database, dbUser, region } =
76
+ await getDatabaseConfig(DB_PACKAGE_NAME);
77
+ const adapter = new PrismaPg(
78
+ new Pool({
79
+ host: hostname,
80
+ port,
81
+ database,
82
+ user: dbUser,
83
+ ssl: {
84
+ rejectUnauthorized: true,
85
+ },
86
+ allowExitOnIdle: true,
87
+ password: async () => {
88
+ const token = await new Signer({
89
+ hostname,
90
+ port,
91
+ region,
92
+ username: dbUser,
93
+ }).getAuthToken();
94
+ return token;
95
+ },
96
+ }),
97
+ );
98
+
99
+ return new PrismaClient({ adapter });
100
+ })();
101
+
102
+ return prismaPromise;
103
+ };
104
+ <%_ } _%>
@@ -0,0 +1,145 @@
1
+ import { getAppConfig } from '@aws-lambda-powertools/parameters/appconfig';
2
+ import {
3
+ GetSecretValueCommand,
4
+ SecretsManagerClient,
5
+ } from '@aws-sdk/client-secrets-manager';
6
+
7
+ export type DatabaseConfig = {
8
+ hostname: string;
9
+ port: number;
10
+ database: string;
11
+ adminUser: string;
12
+ dbUser: string;
13
+ region: string;
14
+ };
15
+
16
+ export type DatabaseSecret = {
17
+ dbname: string;
18
+ username: string;
19
+ password: string;
20
+ host: string;
21
+ port: number;
22
+ };
23
+
24
+ const databaseConfigPromises: Record<string, Promise<DatabaseConfig>> = {};
25
+
26
+ const getSecretValue = async (secretArn: string): Promise<string> => {
27
+ const client = new SecretsManagerClient();
28
+ const data = await client.send(
29
+ new GetSecretValueCommand({
30
+ SecretId: secretArn,
31
+ }),
32
+ );
33
+
34
+ if (!data.SecretString) {
35
+ throw new Error('Database secret does not contain SecretString.');
36
+ }
37
+
38
+ return data.SecretString;
39
+ };
40
+
41
+ export const getDatabaseSecret = async (): Promise<DatabaseSecret> => {
42
+ const secretArn = process.env.DATABASE_SECRET_ARN;
43
+
44
+ if (!secretArn) {
45
+ throw new Error(
46
+ 'Missing required environment variable DATABASE_SECRET_ARN.',
47
+ );
48
+ }
49
+
50
+ return JSON.parse(await getSecretValue(secretArn)) as DatabaseSecret;
51
+ };
52
+
53
+ const loadDatabaseConfig = async (
54
+ runtimeConfigKey: string,
55
+ ): Promise<DatabaseConfig> => {
56
+ const appId = process.env.RUNTIME_CONFIG_APP_ID;
57
+
58
+ if (!appId) {
59
+ throw new Error(
60
+ 'Missing required environment variable RUNTIME_CONFIG_APP_ID.',
61
+ );
62
+ }
63
+
64
+ const config = await getAppConfig<{
65
+ [key: string]: DatabaseConfig | undefined;
66
+ }>('database', {
67
+ application: appId,
68
+ environment: 'default',
69
+ transform: 'json',
70
+ });
71
+
72
+ const databaseConfig = config?.[runtimeConfigKey];
73
+
74
+ if (!databaseConfig) {
75
+ throw new Error(`RuntimeConfig is missing database.${runtimeConfigKey}.`);
76
+ }
77
+
78
+ return databaseConfig;
79
+ };
80
+
81
+ export const getDatabaseConfig = (
82
+ runtimeConfigKey: string,
83
+ ): Promise<DatabaseConfig> => {
84
+ databaseConfigPromises[runtimeConfigKey] ??=
85
+ loadDatabaseConfig(runtimeConfigKey);
86
+ return databaseConfigPromises[runtimeConfigKey];
87
+ };
88
+
89
+ // Aurora's writer endpoint is briefly unreachable from within the VPC right
90
+ // after the cluster reports ready, and IAM policy attachments can take tens
91
+ // of seconds to propagate before RDS accepts an IAM auth token. Both surface
92
+ // as errors that resolve on retry.
93
+ const transientErrorPatterns = [
94
+ 'ETIMEDOUT',
95
+ 'ECONNREFUSED',
96
+ 'ENOTFOUND',
97
+ 'P1000', // Prisma: authentication failed
98
+ 'ER_ACCESS_DENIED_ERROR', // MySQL: access denied
99
+ ];
100
+
101
+ const asString = (value: unknown): string => {
102
+ if (value == null) return '';
103
+ if (typeof value === 'string') return value;
104
+ if (Buffer.isBuffer(value)) return value.toString('utf-8');
105
+ return String(value);
106
+ };
107
+
108
+ export const isTransientConnectionError = (error: unknown): boolean => {
109
+ const err = error as {
110
+ message?: unknown;
111
+ code?: unknown;
112
+ stderr?: unknown;
113
+ stdout?: unknown;
114
+ };
115
+ const haystack = [
116
+ error instanceof Error ? error.message : asString(err?.message),
117
+ asString(err?.code),
118
+ asString(err?.stderr),
119
+ asString(err?.stdout),
120
+ ].join('\n');
121
+ return transientErrorPatterns.some((p) => haystack.includes(p));
122
+ };
123
+
124
+ export const withConnectionRetry = async <T>(
125
+ fn: () => Promise<T>,
126
+ { maxAttempts = 6, delayMs = 10_000 }: { maxAttempts?: number; delayMs?: number } = {},
127
+ ): Promise<T> => {
128
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
129
+ try {
130
+ return await fn();
131
+ } catch (error) {
132
+ if (attempt === maxAttempts || !isTransientConnectionError(error)) {
133
+ throw error;
134
+ }
135
+ const wait = delayMs * attempt;
136
+ console.log(
137
+ `Transient connection error (attempt ${attempt}/${maxAttempts}), retrying in ${wait}ms: ${
138
+ error instanceof Error ? error.message : String(error)
139
+ }`,
140
+ );
141
+ await new Promise((resolve) => setTimeout(resolve, wait));
142
+ }
143
+ }
144
+ throw new Error('unreachable');
145
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { GeneratorCallback, Tree } from '@nx/devkit';
6
+ import { TsRdbGeneratorSchema } from './schema';
7
+ import { NxGeneratorInfo } from '../../utils/nx';
8
+ export declare const TS_RDB_GENERATOR_INFO: NxGeneratorInfo;
9
+ export declare const tsRdbGenerator: (tree: Tree, options: TsRdbGeneratorSchema) => Promise<GeneratorCallback>;
10
+ export default tsRdbGenerator;