alchemy 0.63.1 → 0.65.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/bin/alchemy.js +7664 -5857
- package/bin/commands/init.ts +130 -9
- package/bin/constants.ts +4 -0
- package/bin/services/cdp-manager/cdp-proxy.ts +69 -0
- package/bin/services/cdp-manager/server.ts +173 -0
- package/bin/services/execute-alchemy.ts +83 -4
- package/bin/types.ts +1 -0
- package/lib/alchemy.d.ts +4 -0
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +10 -4
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.d.ts +3 -0
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +11 -1
- package/lib/apply.js.map +1 -1
- package/lib/cloudflare/bindings.d.ts +3 -1
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bucket.d.ts +2 -1
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/bundle/cloudflare-internal.d.ts +6 -0
- package/lib/cloudflare/bundle/cloudflare-internal.d.ts.map +1 -0
- package/lib/cloudflare/bundle/cloudflare-internal.js +39 -0
- package/lib/cloudflare/bundle/cloudflare-internal.js.map +1 -0
- package/lib/cloudflare/bundle/nodejs-builtin-modules.d.ts +2 -0
- package/lib/cloudflare/bundle/nodejs-builtin-modules.d.ts.map +1 -0
- package/lib/cloudflare/bundle/nodejs-builtin-modules.js +9 -0
- package/lib/cloudflare/bundle/nodejs-builtin-modules.js.map +1 -0
- package/lib/cloudflare/bundle/plugin-als-external.d.ts +6 -0
- package/lib/cloudflare/bundle/plugin-als-external.d.ts.map +1 -0
- package/lib/cloudflare/bundle/plugin-als-external.js +12 -0
- package/lib/cloudflare/bundle/plugin-als-external.js.map +1 -0
- package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts +4 -1
- package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts.map +1 -1
- package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js +23 -23
- package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js.map +1 -1
- package/lib/cloudflare/bundle/plugin-node-compat.d.ts +11 -0
- package/lib/cloudflare/bundle/plugin-node-compat.d.ts.map +1 -0
- package/lib/cloudflare/bundle/plugin-node-compat.js +93 -0
- package/lib/cloudflare/bundle/plugin-node-compat.js.map +1 -0
- package/lib/cloudflare/bundle/plugin-wasm.d.ts.map +1 -1
- package/lib/cloudflare/bundle/plugin-wasm.js +20 -11
- package/lib/cloudflare/bundle/plugin-wasm.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/d1-local-migrations.js.map +1 -1
- package/lib/cloudflare/durable-object-namespace.d.ts +1 -0
- package/lib/cloudflare/durable-object-namespace.d.ts.map +1 -1
- package/lib/cloudflare/durable-object-namespace.js.map +1 -1
- package/lib/cloudflare/hyperdrive.d.ts +7 -2
- package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
- package/lib/cloudflare/hyperdrive.js +39 -57
- package/lib/cloudflare/hyperdrive.js.map +1 -1
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/next.d.ts +13 -0
- package/lib/cloudflare/next.d.ts.map +1 -0
- package/lib/cloudflare/next.js +91 -0
- package/lib/cloudflare/next.js.map +1 -0
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +80 -82
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-assets.d.ts +3 -3
- package/lib/cloudflare/worker-assets.d.ts.map +1 -1
- package/lib/cloudflare/worker-assets.js +22 -47
- package/lib/cloudflare/worker-assets.js.map +1 -1
- package/lib/cloudflare/worker-bundle.d.ts +6 -0
- package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/worker-bundle.js +34 -10
- package/lib/cloudflare/worker-bundle.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +1 -0
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +24 -6
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +4 -6
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +20 -15
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/context.d.ts +3 -1
- package/lib/context.d.ts.map +1 -1
- package/lib/context.js +2 -2
- package/lib/context.js.map +1 -1
- package/lib/destroy.d.ts +8 -0
- package/lib/destroy.d.ts.map +1 -1
- package/lib/destroy.js +20 -4
- package/lib/destroy.js.map +1 -1
- package/lib/planetscale/api/client.gen.d.ts +1750 -0
- package/lib/planetscale/api/client.gen.d.ts.map +1 -0
- package/lib/planetscale/api/client.gen.js +2401 -0
- package/lib/planetscale/api/client.gen.js.map +1 -0
- package/lib/planetscale/api/index.d.ts +3 -0
- package/lib/planetscale/api/index.d.ts.map +1 -0
- package/lib/planetscale/api/index.js +4 -0
- package/lib/planetscale/api/index.js.map +1 -0
- package/lib/planetscale/api/types.gen.d.ts +12156 -0
- package/lib/planetscale/api/types.gen.d.ts.map +1 -0
- package/lib/planetscale/api/types.gen.js +2 -0
- package/lib/planetscale/api/types.gen.js.map +1 -0
- package/lib/planetscale/branch.d.ts +5 -8
- package/lib/planetscale/branch.d.ts.map +1 -1
- package/lib/planetscale/branch.js +122 -97
- package/lib/planetscale/branch.js.map +1 -1
- package/lib/planetscale/database.d.ts +24 -11
- package/lib/planetscale/database.d.ts.map +1 -1
- package/lib/planetscale/database.js +163 -118
- package/lib/planetscale/database.js.map +1 -1
- package/lib/planetscale/index.d.ts +2 -1
- package/lib/planetscale/index.d.ts.map +1 -1
- package/lib/planetscale/index.js +2 -1
- package/lib/planetscale/index.js.map +1 -1
- package/lib/planetscale/password.d.ts +7 -8
- package/lib/planetscale/password.d.ts.map +1 -1
- package/lib/planetscale/password.js +74 -57
- package/lib/planetscale/password.js.map +1 -1
- package/lib/planetscale/role.d.ts +126 -0
- package/lib/planetscale/role.d.ts.map +1 -0
- package/lib/planetscale/role.js +144 -0
- package/lib/planetscale/role.js.map +1 -0
- package/lib/planetscale/utils.d.ts +23 -7
- package/lib/planetscale/utils.d.ts.map +1 -1
- package/lib/planetscale/utils.js +169 -75
- package/lib/planetscale/utils.js.map +1 -1
- package/lib/scope.d.ts +5 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +11 -3
- package/lib/scope.js.map +1 -1
- package/lib/secret.d.ts +0 -3
- package/lib/secret.d.ts.map +1 -1
- package/lib/secret.js +0 -2
- package/lib/secret.js.map +1 -1
- package/lib/state/cloudflare-state-store.js +3 -3
- package/lib/state/cloudflare-state-store.js.map +1 -1
- package/lib/test/bun.d.ts +2 -2
- package/lib/test/bun.d.ts.map +1 -1
- package/lib/test/vitest.d.ts +2 -2
- package/lib/test/vitest.d.ts.map +1 -1
- package/lib/test/vitest.js +4 -4
- package/lib/test/vitest.js.map +1 -1
- package/lib/util/content-type.js +2 -2
- package/lib/util/content-type.js.map +1 -1
- package/lib/util/dedent.d.ts +3 -14
- package/lib/util/dedent.d.ts.map +1 -1
- package/lib/util/dedent.js +33 -45
- package/lib/util/dedent.js.map +1 -1
- package/lib/util/memoize.d.ts.map +1 -1
- package/lib/util/memoize.js +11 -1
- package/lib/util/memoize.js.map +1 -1
- package/package.json +8 -3
- package/src/alchemy.ts +14 -6
- package/src/apply.ts +13 -1
- package/src/cloudflare/bindings.ts +3 -1
- package/src/cloudflare/bucket.ts +3 -1
- package/src/cloudflare/bundle/cloudflare-internal.ts +42 -0
- package/src/cloudflare/bundle/nodejs-builtin-modules.ts +14 -0
- package/src/cloudflare/bundle/plugin-als-external.ts +13 -0
- package/src/cloudflare/bundle/plugin-hybrid-node-compat.ts +29 -28
- package/src/cloudflare/bundle/plugin-node-compat.ts +114 -0
- package/src/cloudflare/bundle/plugin-wasm.ts +34 -14
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/d1-local-migrations.ts +2 -2
- package/src/cloudflare/durable-object-namespace.ts +1 -0
- package/src/cloudflare/hyperdrive.ts +136 -95
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/next.ts +123 -0
- package/src/cloudflare/website.ts +88 -89
- package/src/cloudflare/worker-assets.ts +43 -105
- package/src/cloudflare/worker-bundle.ts +46 -9
- package/src/cloudflare/worker-metadata.ts +2 -0
- package/src/cloudflare/worker.ts +35 -7
- package/src/cloudflare/wrangler.json.ts +114 -113
- package/src/context.ts +5 -3
- package/src/destroy.ts +26 -8
- package/src/planetscale/api/client.gen.ts +2882 -0
- package/src/planetscale/api/index.ts +4 -0
- package/src/planetscale/api/types.gen.ts +13041 -0
- package/src/planetscale/branch.ts +170 -162
- package/src/planetscale/database.ts +263 -243
- package/src/planetscale/index.ts +2 -1
- package/src/planetscale/password.ts +89 -86
- package/src/planetscale/role.ts +278 -0
- package/src/planetscale/utils.ts +289 -128
- package/src/scope.ts +12 -3
- package/src/secret.ts +0 -4
- package/src/state/cloudflare-state-store.ts +4 -4
- package/src/test/bun.ts +2 -2
- package/src/test/vitest.ts +6 -6
- package/src/util/content-type.ts +2 -2
- package/src/util/dedent.ts +34 -50
- package/src/util/memoize.ts +13 -1
- package/templates/astro/alchemy.run.ts +1 -3
- package/templates/hono/alchemy.run.ts +0 -1
- package/templates/nextjs/README.md +85 -0
- package/templates/nextjs/_env +1 -0
- package/templates/nextjs/_env.example +1 -0
- package/templates/nextjs/_gitignore +46 -0
- package/templates/nextjs/alchemy.run.ts +16 -0
- package/templates/nextjs/eslint.config.mjs +16 -0
- package/templates/nextjs/next-env.d.ts +5 -0
- package/templates/nextjs/next.config.ts +10 -0
- package/templates/nextjs/open-next.config.ts +5 -0
- package/templates/nextjs/package.json +31 -0
- package/templates/nextjs/src/app/api/hello/route.ts +19 -0
- package/templates/nextjs/src/app/favicon.ico +0 -0
- package/templates/nextjs/src/app/globals.css +72 -0
- package/templates/nextjs/src/app/layout.tsx +19 -0
- package/templates/nextjs/src/app/page.tsx +55 -0
- package/templates/nextjs/tsconfig.json +33 -0
- package/templates/nextjs/types/env.d.ts +14 -0
- package/templates/nuxt/alchemy.run.ts +1 -3
- package/templates/react-router/alchemy.run.ts +1 -3
- package/templates/rwsdk/alchemy.run.ts +0 -2
- package/templates/sveltekit/alchemy.run.ts +1 -3
- package/templates/tanstack-start/alchemy.run.ts +1 -3
- package/templates/typescript/alchemy.run.ts +0 -1
- package/templates/vite/alchemy.run.ts +0 -1
- package/workers/cloudflare-state-store.js +58 -58
- package/lib/cloudflare/bundle/externals.d.ts +0 -3
- package/lib/cloudflare/bundle/externals.d.ts.map +0 -1
- package/lib/cloudflare/bundle/externals.js +0 -35
- package/lib/cloudflare/bundle/externals.js.map +0 -1
- package/lib/cloudflare/bundle/plugin-compat-warning.d.ts +0 -10
- package/lib/cloudflare/bundle/plugin-compat-warning.d.ts.map +0 -1
- package/lib/cloudflare/bundle/plugin-compat-warning.js +0 -67
- package/lib/cloudflare/bundle/plugin-compat-warning.js.map +0 -1
- package/lib/planetscale/api.d.ts +0 -53
- package/lib/planetscale/api.d.ts.map +0 -1
- package/lib/planetscale/api.js +0 -84
- package/lib/planetscale/api.js.map +0 -1
- package/src/cloudflare/bundle/externals.ts +0 -36
- package/src/cloudflare/bundle/plugin-compat-warning.ts +0 -94
- package/src/planetscale/api.ts +0 -115
|
@@ -1,17 +1,17 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import { alchemy } from "../alchemy.ts";
|
|
2
3
|
import type { Context } from "../context.ts";
|
|
3
4
|
import { Resource } from "../resource.ts";
|
|
4
5
|
import type { Secret } from "../secret.ts";
|
|
5
|
-
import { logger } from "../util/logger.ts";
|
|
6
6
|
import { lowercaseId } from "../util/nanoid.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { PlanetScaleClient, type PlanetScaleProps } from "./api/client.gen.ts";
|
|
8
8
|
import type { Branch } from "./branch.ts";
|
|
9
9
|
import type { Database } from "./database.ts";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Properties for creating or updating a PlanetScale Branch
|
|
13
13
|
*/
|
|
14
|
-
export interface PasswordProps {
|
|
14
|
+
export interface PasswordProps extends PlanetScaleProps {
|
|
15
15
|
/**
|
|
16
16
|
* The name of the password
|
|
17
17
|
*
|
|
@@ -21,7 +21,7 @@ export interface PasswordProps {
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* The organization ID where the password will be created
|
|
24
|
-
* Required when using string database name, optional when using Database resource
|
|
24
|
+
* Required when using string database name, optional when using Database or Branch resource
|
|
25
25
|
*/
|
|
26
26
|
organizationId?: string;
|
|
27
27
|
|
|
@@ -34,15 +34,10 @@ export interface PasswordProps {
|
|
|
34
34
|
/**
|
|
35
35
|
* The branch where the password will be created
|
|
36
36
|
* Can be either a branch name (string) or Branch resource
|
|
37
|
-
*
|
|
37
|
+
* @default "main"
|
|
38
38
|
*/
|
|
39
39
|
branch?: string | Branch;
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
* PlanetScale API token (overrides environment variable)
|
|
43
|
-
*/
|
|
44
|
-
apiKey?: Secret;
|
|
45
|
-
|
|
46
41
|
/**
|
|
47
42
|
* The password
|
|
48
43
|
*/
|
|
@@ -107,7 +102,9 @@ export interface Password
|
|
|
107
102
|
}
|
|
108
103
|
|
|
109
104
|
/**
|
|
110
|
-
* Create and manage database passwords for PlanetScale branches. Database passwords provide secure access to your database with specific roles and permissions.
|
|
105
|
+
* Create and manage database passwords for PlanetScale MySQL branches. Database passwords provide secure access to your database with specific roles and permissions.
|
|
106
|
+
*
|
|
107
|
+
* For Postgres, use [Roles](./role.ts) instead.
|
|
111
108
|
*
|
|
112
109
|
* @example
|
|
113
110
|
* ## Basic Reader Password
|
|
@@ -265,109 +262,115 @@ export const Password = Resource(
|
|
|
265
262
|
id: string,
|
|
266
263
|
props: PasswordProps,
|
|
267
264
|
): Promise<Password> {
|
|
268
|
-
const apiKey =
|
|
269
|
-
props.apiKey?.unencrypted || process.env.PLANETSCALE_API_TOKEN;
|
|
270
|
-
if (!apiKey) {
|
|
271
|
-
throw new Error("PLANETSCALE_API_TOKEN environment variable is required");
|
|
272
|
-
}
|
|
273
265
|
const nameSlug = this.isReplacement
|
|
274
266
|
? lowercaseId()
|
|
275
267
|
: (this.output?.nameSlug ?? lowercaseId());
|
|
276
268
|
const name = `${(props.name ?? this.output?.name ?? this.scope.createPhysicalName(id)).toLowerCase()}-${nameSlug}`;
|
|
277
269
|
|
|
278
|
-
const api = new
|
|
279
|
-
const
|
|
280
|
-
props.branch == null
|
|
281
|
-
? "main"
|
|
282
|
-
: typeof props.branch === "string"
|
|
283
|
-
? props.branch
|
|
284
|
-
: props.branch.name;
|
|
285
|
-
const databaseName =
|
|
270
|
+
const api = new PlanetScaleClient(props);
|
|
271
|
+
const database =
|
|
286
272
|
typeof props.database === "string" ? props.database : props.database.name;
|
|
273
|
+
const branch =
|
|
274
|
+
typeof props.branch === "string"
|
|
275
|
+
? props.branch
|
|
276
|
+
: (props.branch?.name ?? "main");
|
|
277
|
+
const organization =
|
|
278
|
+
props.organizationId ??
|
|
279
|
+
((typeof props.branch !== "string" && props.branch?.organizationId) ||
|
|
280
|
+
(typeof props.database !== "string" && props.database.organizationId));
|
|
281
|
+
|
|
282
|
+
if (!organization) {
|
|
283
|
+
throw new Error("Organization ID is required");
|
|
284
|
+
}
|
|
287
285
|
|
|
288
286
|
if (this.phase === "delete") {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
287
|
+
if (this.output?.id) {
|
|
288
|
+
const res = await api.organizations.databases.branches.passwords.delete(
|
|
289
|
+
{
|
|
290
|
+
path: {
|
|
291
|
+
organization,
|
|
292
|
+
database,
|
|
293
|
+
branch,
|
|
294
|
+
id: this.output.id,
|
|
295
|
+
},
|
|
296
|
+
result: "full",
|
|
297
|
+
},
|
|
298
|
+
);
|
|
294
299
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
+
if (res.error && res.error.status !== 404) {
|
|
301
|
+
throw new Error(`Failed to delete branch "${branch}"`, {
|
|
302
|
+
cause: res.error,
|
|
303
|
+
});
|
|
300
304
|
}
|
|
301
|
-
} catch (error) {
|
|
302
|
-
logger.error("Error deleting password:", error);
|
|
303
|
-
throw error;
|
|
304
305
|
}
|
|
305
306
|
return this.destroy();
|
|
306
307
|
}
|
|
307
308
|
if (this.phase === "update") {
|
|
309
|
+
// Only name and cidrs can be updated in place; all other properties require replacement.
|
|
308
310
|
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
Array.isArray(props.cidrs) &&
|
|
313
|
-
this.output.cidrs.length === props.cidrs.length &&
|
|
314
|
-
this.output.cidrs.every((cidr, i) => cidr === props.cidrs![i])))
|
|
311
|
+
diff({ ...props, name }, this.output).some(
|
|
312
|
+
(prop) => prop !== "name" && prop !== "cidrs",
|
|
313
|
+
)
|
|
315
314
|
) {
|
|
316
315
|
return this.replace();
|
|
317
316
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
317
|
+
await api.organizations.databases.branches.passwords.patch({
|
|
318
|
+
path: {
|
|
319
|
+
organization,
|
|
320
|
+
database,
|
|
321
|
+
branch,
|
|
322
|
+
id: this.output.id,
|
|
323
|
+
},
|
|
324
|
+
body: {
|
|
321
325
|
name,
|
|
322
326
|
cidrs: props.cidrs,
|
|
323
327
|
},
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
if (!updateResponse.ok) {
|
|
327
|
-
throw new Error(
|
|
328
|
-
`Failed to update password: ${updateResponse.statusText} ${await updateResponse.text()}`,
|
|
329
|
-
);
|
|
330
|
-
}
|
|
328
|
+
});
|
|
331
329
|
|
|
332
330
|
return this({
|
|
333
331
|
...this.output,
|
|
334
332
|
...props,
|
|
333
|
+
name,
|
|
335
334
|
});
|
|
336
335
|
}
|
|
337
336
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
`Failed to create password: ${createResponse.statusText} ${await createResponse.text()}`,
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const data = await createResponse.json<any>();
|
|
337
|
+
const data = await api.organizations.databases.branches.passwords.post({
|
|
338
|
+
path: {
|
|
339
|
+
organization,
|
|
340
|
+
database,
|
|
341
|
+
branch,
|
|
342
|
+
},
|
|
343
|
+
body: {
|
|
344
|
+
name,
|
|
345
|
+
role: props.role,
|
|
346
|
+
replica: props.replica,
|
|
347
|
+
ttl: props.ttl,
|
|
348
|
+
cidrs: props.cidrs,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
357
351
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
} catch (error) {
|
|
369
|
-
logger.error("Error managing password:", error);
|
|
370
|
-
throw error;
|
|
371
|
-
}
|
|
352
|
+
return this({
|
|
353
|
+
id: data.id,
|
|
354
|
+
expiresAt: data.expires_at,
|
|
355
|
+
host: data.access_host_url,
|
|
356
|
+
username: data.username,
|
|
357
|
+
password: alchemy.secret(data.plain_text),
|
|
358
|
+
nameSlug,
|
|
359
|
+
...props,
|
|
360
|
+
name: `${props.name}-${nameSlug}`,
|
|
361
|
+
});
|
|
372
362
|
},
|
|
373
363
|
);
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Returns an array of keys in `b` that are different from `a`.
|
|
367
|
+
*/
|
|
368
|
+
const diff = <T>(a: T, b: NoInfer<T>) => {
|
|
369
|
+
const keys: (keyof T)[] = [];
|
|
370
|
+
for (const key in a) {
|
|
371
|
+
if (!isDeepStrictEqual(a[key], b[key])) {
|
|
372
|
+
keys.push(key);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return keys;
|
|
376
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { alchemy } from "../alchemy.ts";
|
|
2
|
+
import type { Context } from "../context.ts";
|
|
3
|
+
import { Resource } from "../resource.ts";
|
|
4
|
+
import type { Secret } from "../secret.ts";
|
|
5
|
+
import { logger } from "../util/logger.ts";
|
|
6
|
+
import { PlanetScaleClient, type PlanetScaleProps } from "./api/client.gen.ts";
|
|
7
|
+
import type { CreateRoleData } from "./api/types.gen.ts";
|
|
8
|
+
import type { Branch } from "./branch.ts";
|
|
9
|
+
import type { Database } from "./database.ts";
|
|
10
|
+
import { waitForBranchReady } from "./utils.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Properties for creating or updating a PlanetScale PostgreSQL Role
|
|
14
|
+
*/
|
|
15
|
+
export interface RoleProps extends PlanetScaleProps {
|
|
16
|
+
/**
|
|
17
|
+
* The organization ID where the role will be created
|
|
18
|
+
* Required when using string database name, optional when using Database resource
|
|
19
|
+
*/
|
|
20
|
+
organizationId?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The database where the role will be created
|
|
24
|
+
* Can be either a database name (string) or Database resource
|
|
25
|
+
*/
|
|
26
|
+
database: string | Database;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The branch where the role will be created
|
|
30
|
+
* Can be either a branch name (string) or Branch resource
|
|
31
|
+
* @default "main"
|
|
32
|
+
*/
|
|
33
|
+
branch?: string | Branch;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Time to live in seconds
|
|
37
|
+
*/
|
|
38
|
+
ttl?: number;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Roles to inherit from.
|
|
42
|
+
* The `"postgres"` role provides full administrator access to the database.
|
|
43
|
+
* You can also inherit from another Role resource.
|
|
44
|
+
*/
|
|
45
|
+
inheritedRoles: InheritedRole[] | Role;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Roles that can be inherited from.
|
|
50
|
+
*/
|
|
51
|
+
export type InheritedRole =
|
|
52
|
+
| "postgres"
|
|
53
|
+
| "pg_checkpoint"
|
|
54
|
+
| "pg_create_subscription"
|
|
55
|
+
| "pg_maintain"
|
|
56
|
+
| "pg_monitor"
|
|
57
|
+
| "pg_read_all_data"
|
|
58
|
+
| "pg_read_all_settings"
|
|
59
|
+
| "pg_read_all_stats"
|
|
60
|
+
| "pg_signal_backend"
|
|
61
|
+
| "pg_stat_scan_tables"
|
|
62
|
+
| "pg_use_reserved_connections"
|
|
63
|
+
| "pg_write_all_data"
|
|
64
|
+
| (string & {});
|
|
65
|
+
|
|
66
|
+
export interface Role
|
|
67
|
+
extends Resource<"planetscale::Role">,
|
|
68
|
+
Omit<RoleProps, "inheritedRoles"> {
|
|
69
|
+
/**
|
|
70
|
+
* The unique identifier for the role
|
|
71
|
+
*/
|
|
72
|
+
id: string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The name of the role
|
|
76
|
+
*/
|
|
77
|
+
name: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The timestamp when the role expires (ISO 8601 format)
|
|
81
|
+
*/
|
|
82
|
+
expiresAt: string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The host URL for database connection
|
|
86
|
+
*/
|
|
87
|
+
host: string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The username for database authentication
|
|
91
|
+
*/
|
|
92
|
+
username: string;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The encrypted password for database authentication
|
|
96
|
+
*/
|
|
97
|
+
password: Secret<string>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The database name
|
|
101
|
+
*/
|
|
102
|
+
databaseName: string;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The direct connection URL for the database.
|
|
106
|
+
*/
|
|
107
|
+
connectionUrl: Secret<string>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The pooled connection URL for the database.
|
|
111
|
+
* Uses PSBouncer on port 6432. Recommended for production.
|
|
112
|
+
* @see https://planetscale.com/docs/postgres/connecting/psbouncer
|
|
113
|
+
*/
|
|
114
|
+
connectionUrlPooled: Secret<string>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The roles that this role inherits from.
|
|
118
|
+
*/
|
|
119
|
+
inheritedRoles: InheritedRole[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create and manage database roles for PlanetScale PostgreSQL branches. Database roles provide secure access to your database with specific roles and permissions.
|
|
124
|
+
*
|
|
125
|
+
* For MySQL, use [Passwords](./password.ts) instead.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ## Basic Role
|
|
129
|
+
*
|
|
130
|
+
* Create a default role with all permissions:
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* const role = await Role("my-role", {
|
|
134
|
+
* database: "my-database",
|
|
135
|
+
* inheritedRoles: ["postgres"],
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* ## Role with TTL
|
|
140
|
+
*
|
|
141
|
+
* Create a role with a TTL of 1 hour:
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* const role = await Role("my-role", {
|
|
145
|
+
* database: "my-database",
|
|
146
|
+
* ttl: 3600,
|
|
147
|
+
* });
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
150
|
+
* ## Role with Inherited Permissions
|
|
151
|
+
*
|
|
152
|
+
* Create a role with read-only access to all data and settings:
|
|
153
|
+
*
|
|
154
|
+
* ```ts
|
|
155
|
+
* const role = await Role("my-role", {
|
|
156
|
+
* database: "my-database",
|
|
157
|
+
* inheritedRoles: ["pg_read_all_data", "pg_read_all_settings"],
|
|
158
|
+
* });
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export const Role = Resource(
|
|
162
|
+
"planetscale::Role",
|
|
163
|
+
async function (
|
|
164
|
+
this: Context<Role, RoleProps>,
|
|
165
|
+
id: string,
|
|
166
|
+
props: RoleProps,
|
|
167
|
+
): Promise<Role> {
|
|
168
|
+
const api = new PlanetScaleClient(props);
|
|
169
|
+
const organization =
|
|
170
|
+
typeof props.branch === "object"
|
|
171
|
+
? props.branch.organizationId
|
|
172
|
+
: typeof props.database === "object"
|
|
173
|
+
? props.database.organizationId
|
|
174
|
+
: props.organizationId;
|
|
175
|
+
const database =
|
|
176
|
+
typeof props.database === "string" ? props.database : props.database.name;
|
|
177
|
+
const branch =
|
|
178
|
+
typeof props.branch === "string"
|
|
179
|
+
? props.branch
|
|
180
|
+
: (props.branch?.name ?? "main");
|
|
181
|
+
const inheritedRoles = Array.isArray(props.inheritedRoles)
|
|
182
|
+
? props.inheritedRoles
|
|
183
|
+
: props.inheritedRoles.inheritedRoles;
|
|
184
|
+
if (!organization) {
|
|
185
|
+
throw new Error("Organization ID is required");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
switch (this.phase) {
|
|
189
|
+
case "delete": {
|
|
190
|
+
if (this.output?.id) {
|
|
191
|
+
const res = await api.organizations.databases.branches.roles.delete({
|
|
192
|
+
path: {
|
|
193
|
+
organization,
|
|
194
|
+
database,
|
|
195
|
+
branch,
|
|
196
|
+
id: this.output.id,
|
|
197
|
+
},
|
|
198
|
+
result: "full",
|
|
199
|
+
});
|
|
200
|
+
if (res.error) {
|
|
201
|
+
switch (res.error.status) {
|
|
202
|
+
case 404:
|
|
203
|
+
break;
|
|
204
|
+
case 422:
|
|
205
|
+
// This is a workaround for 422 unprocessable: Role is still referenced and cannot be dropped.
|
|
206
|
+
// Essentially, the role is still linked to a branch, so we can't delete it here, but it'll be deleted when the branch is deleted.
|
|
207
|
+
logger.warn(
|
|
208
|
+
[
|
|
209
|
+
`Failed to delete role "${id}" (name: "${this.output.name}").`,
|
|
210
|
+
`Error: ${res.error.message}`,
|
|
211
|
+
`If you are deleting database "${database}" or branch "${branch}", the role will be deleted automatically.`,
|
|
212
|
+
`Otherwise, consider manually deleting the role at: https://app.planetscale.com/${organization}/${database}/settings/roles`,
|
|
213
|
+
].join("\n"),
|
|
214
|
+
);
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
throw new Error(`Failed to delete role "${id}"`, {
|
|
218
|
+
cause: res.error,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return this.destroy();
|
|
224
|
+
}
|
|
225
|
+
case "create": {
|
|
226
|
+
const { kind, ready } = await api.organizations.databases.branches.get({
|
|
227
|
+
path: {
|
|
228
|
+
organization,
|
|
229
|
+
database,
|
|
230
|
+
name: branch,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
if (kind !== "postgresql") {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Cannot create a role on MySQL database "${database}". Roles are only supported on PostgreSQL databases. For MySQL databases, please use the Password resource instead.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
// Cannot create role until branch is ready
|
|
239
|
+
if (!ready) {
|
|
240
|
+
await waitForBranchReady(api, organization, database, branch);
|
|
241
|
+
}
|
|
242
|
+
const role = await api.organizations.databases.branches.roles.post({
|
|
243
|
+
path: {
|
|
244
|
+
organization,
|
|
245
|
+
database,
|
|
246
|
+
branch,
|
|
247
|
+
},
|
|
248
|
+
body: {
|
|
249
|
+
ttl: props.ttl,
|
|
250
|
+
inherited_roles: inheritedRoles,
|
|
251
|
+
} as CreateRoleData["body"],
|
|
252
|
+
});
|
|
253
|
+
return this({
|
|
254
|
+
...props,
|
|
255
|
+
id: role.id,
|
|
256
|
+
name: role.name,
|
|
257
|
+
host: role.access_host_url,
|
|
258
|
+
username: role.username,
|
|
259
|
+
password: alchemy.secret(role.password),
|
|
260
|
+
expiresAt: role.expires_at,
|
|
261
|
+
databaseName: role.database_name,
|
|
262
|
+
inheritedRoles,
|
|
263
|
+
connectionUrl: alchemy.secret(
|
|
264
|
+
`postgresql://${role.username}:${role.password}@${role.access_host_url}:5432/${role.database_name}?sslmode=verify-full`,
|
|
265
|
+
),
|
|
266
|
+
connectionUrlPooled: alchemy.secret(
|
|
267
|
+
`postgresql://${role.username}:${role.password}@${role.access_host_url}:6432/${role.database_name}?sslmode=verify-full`,
|
|
268
|
+
),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
case "update": {
|
|
272
|
+
// According to the types, the only property that can be updated is the name.
|
|
273
|
+
// However, I was getting 500 errors when trying to update the name, so we'll just replace.
|
|
274
|
+
return this.replace();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
);
|