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.
Files changed (235) hide show
  1. package/bin/alchemy.js +7664 -5857
  2. package/bin/commands/init.ts +130 -9
  3. package/bin/constants.ts +4 -0
  4. package/bin/services/cdp-manager/cdp-proxy.ts +69 -0
  5. package/bin/services/cdp-manager/server.ts +173 -0
  6. package/bin/services/execute-alchemy.ts +83 -4
  7. package/bin/types.ts +1 -0
  8. package/lib/alchemy.d.ts +4 -0
  9. package/lib/alchemy.d.ts.map +1 -1
  10. package/lib/alchemy.js +10 -4
  11. package/lib/alchemy.js.map +1 -1
  12. package/lib/apply.d.ts +3 -0
  13. package/lib/apply.d.ts.map +1 -1
  14. package/lib/apply.js +11 -1
  15. package/lib/apply.js.map +1 -1
  16. package/lib/cloudflare/bindings.d.ts +3 -1
  17. package/lib/cloudflare/bindings.d.ts.map +1 -1
  18. package/lib/cloudflare/bucket.d.ts +2 -1
  19. package/lib/cloudflare/bucket.d.ts.map +1 -1
  20. package/lib/cloudflare/bucket.js.map +1 -1
  21. package/lib/cloudflare/bundle/cloudflare-internal.d.ts +6 -0
  22. package/lib/cloudflare/bundle/cloudflare-internal.d.ts.map +1 -0
  23. package/lib/cloudflare/bundle/cloudflare-internal.js +39 -0
  24. package/lib/cloudflare/bundle/cloudflare-internal.js.map +1 -0
  25. package/lib/cloudflare/bundle/nodejs-builtin-modules.d.ts +2 -0
  26. package/lib/cloudflare/bundle/nodejs-builtin-modules.d.ts.map +1 -0
  27. package/lib/cloudflare/bundle/nodejs-builtin-modules.js +9 -0
  28. package/lib/cloudflare/bundle/nodejs-builtin-modules.js.map +1 -0
  29. package/lib/cloudflare/bundle/plugin-als-external.d.ts +6 -0
  30. package/lib/cloudflare/bundle/plugin-als-external.d.ts.map +1 -0
  31. package/lib/cloudflare/bundle/plugin-als-external.js +12 -0
  32. package/lib/cloudflare/bundle/plugin-als-external.js.map +1 -0
  33. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts +4 -1
  34. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts.map +1 -1
  35. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js +23 -23
  36. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js.map +1 -1
  37. package/lib/cloudflare/bundle/plugin-node-compat.d.ts +11 -0
  38. package/lib/cloudflare/bundle/plugin-node-compat.d.ts.map +1 -0
  39. package/lib/cloudflare/bundle/plugin-node-compat.js +93 -0
  40. package/lib/cloudflare/bundle/plugin-node-compat.js.map +1 -0
  41. package/lib/cloudflare/bundle/plugin-wasm.d.ts.map +1 -1
  42. package/lib/cloudflare/bundle/plugin-wasm.js +20 -11
  43. package/lib/cloudflare/bundle/plugin-wasm.js.map +1 -1
  44. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  45. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  46. package/lib/cloudflare/d1-local-migrations.js.map +1 -1
  47. package/lib/cloudflare/durable-object-namespace.d.ts +1 -0
  48. package/lib/cloudflare/durable-object-namespace.d.ts.map +1 -1
  49. package/lib/cloudflare/durable-object-namespace.js.map +1 -1
  50. package/lib/cloudflare/hyperdrive.d.ts +7 -2
  51. package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
  52. package/lib/cloudflare/hyperdrive.js +39 -57
  53. package/lib/cloudflare/hyperdrive.js.map +1 -1
  54. package/lib/cloudflare/index.d.ts +1 -0
  55. package/lib/cloudflare/index.d.ts.map +1 -1
  56. package/lib/cloudflare/index.js +1 -0
  57. package/lib/cloudflare/index.js.map +1 -1
  58. package/lib/cloudflare/next.d.ts +13 -0
  59. package/lib/cloudflare/next.d.ts.map +1 -0
  60. package/lib/cloudflare/next.js +91 -0
  61. package/lib/cloudflare/next.js.map +1 -0
  62. package/lib/cloudflare/website.d.ts.map +1 -1
  63. package/lib/cloudflare/website.js +80 -82
  64. package/lib/cloudflare/website.js.map +1 -1
  65. package/lib/cloudflare/worker-assets.d.ts +3 -3
  66. package/lib/cloudflare/worker-assets.d.ts.map +1 -1
  67. package/lib/cloudflare/worker-assets.js +22 -47
  68. package/lib/cloudflare/worker-assets.js.map +1 -1
  69. package/lib/cloudflare/worker-bundle.d.ts +6 -0
  70. package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
  71. package/lib/cloudflare/worker-bundle.js +34 -10
  72. package/lib/cloudflare/worker-bundle.js.map +1 -1
  73. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  74. package/lib/cloudflare/worker-metadata.js +1 -0
  75. package/lib/cloudflare/worker-metadata.js.map +1 -1
  76. package/lib/cloudflare/worker.d.ts.map +1 -1
  77. package/lib/cloudflare/worker.js +24 -6
  78. package/lib/cloudflare/worker.js.map +1 -1
  79. package/lib/cloudflare/wrangler.json.d.ts +4 -6
  80. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  81. package/lib/cloudflare/wrangler.json.js +20 -15
  82. package/lib/cloudflare/wrangler.json.js.map +1 -1
  83. package/lib/context.d.ts +3 -1
  84. package/lib/context.d.ts.map +1 -1
  85. package/lib/context.js +2 -2
  86. package/lib/context.js.map +1 -1
  87. package/lib/destroy.d.ts +8 -0
  88. package/lib/destroy.d.ts.map +1 -1
  89. package/lib/destroy.js +20 -4
  90. package/lib/destroy.js.map +1 -1
  91. package/lib/planetscale/api/client.gen.d.ts +1750 -0
  92. package/lib/planetscale/api/client.gen.d.ts.map +1 -0
  93. package/lib/planetscale/api/client.gen.js +2401 -0
  94. package/lib/planetscale/api/client.gen.js.map +1 -0
  95. package/lib/planetscale/api/index.d.ts +3 -0
  96. package/lib/planetscale/api/index.d.ts.map +1 -0
  97. package/lib/planetscale/api/index.js +4 -0
  98. package/lib/planetscale/api/index.js.map +1 -0
  99. package/lib/planetscale/api/types.gen.d.ts +12156 -0
  100. package/lib/planetscale/api/types.gen.d.ts.map +1 -0
  101. package/lib/planetscale/api/types.gen.js +2 -0
  102. package/lib/planetscale/api/types.gen.js.map +1 -0
  103. package/lib/planetscale/branch.d.ts +5 -8
  104. package/lib/planetscale/branch.d.ts.map +1 -1
  105. package/lib/planetscale/branch.js +122 -97
  106. package/lib/planetscale/branch.js.map +1 -1
  107. package/lib/planetscale/database.d.ts +24 -11
  108. package/lib/planetscale/database.d.ts.map +1 -1
  109. package/lib/planetscale/database.js +163 -118
  110. package/lib/planetscale/database.js.map +1 -1
  111. package/lib/planetscale/index.d.ts +2 -1
  112. package/lib/planetscale/index.d.ts.map +1 -1
  113. package/lib/planetscale/index.js +2 -1
  114. package/lib/planetscale/index.js.map +1 -1
  115. package/lib/planetscale/password.d.ts +7 -8
  116. package/lib/planetscale/password.d.ts.map +1 -1
  117. package/lib/planetscale/password.js +74 -57
  118. package/lib/planetscale/password.js.map +1 -1
  119. package/lib/planetscale/role.d.ts +126 -0
  120. package/lib/planetscale/role.d.ts.map +1 -0
  121. package/lib/planetscale/role.js +144 -0
  122. package/lib/planetscale/role.js.map +1 -0
  123. package/lib/planetscale/utils.d.ts +23 -7
  124. package/lib/planetscale/utils.d.ts.map +1 -1
  125. package/lib/planetscale/utils.js +169 -75
  126. package/lib/planetscale/utils.js.map +1 -1
  127. package/lib/scope.d.ts +5 -1
  128. package/lib/scope.d.ts.map +1 -1
  129. package/lib/scope.js +11 -3
  130. package/lib/scope.js.map +1 -1
  131. package/lib/secret.d.ts +0 -3
  132. package/lib/secret.d.ts.map +1 -1
  133. package/lib/secret.js +0 -2
  134. package/lib/secret.js.map +1 -1
  135. package/lib/state/cloudflare-state-store.js +3 -3
  136. package/lib/state/cloudflare-state-store.js.map +1 -1
  137. package/lib/test/bun.d.ts +2 -2
  138. package/lib/test/bun.d.ts.map +1 -1
  139. package/lib/test/vitest.d.ts +2 -2
  140. package/lib/test/vitest.d.ts.map +1 -1
  141. package/lib/test/vitest.js +4 -4
  142. package/lib/test/vitest.js.map +1 -1
  143. package/lib/util/content-type.js +2 -2
  144. package/lib/util/content-type.js.map +1 -1
  145. package/lib/util/dedent.d.ts +3 -14
  146. package/lib/util/dedent.d.ts.map +1 -1
  147. package/lib/util/dedent.js +33 -45
  148. package/lib/util/dedent.js.map +1 -1
  149. package/lib/util/memoize.d.ts.map +1 -1
  150. package/lib/util/memoize.js +11 -1
  151. package/lib/util/memoize.js.map +1 -1
  152. package/package.json +8 -3
  153. package/src/alchemy.ts +14 -6
  154. package/src/apply.ts +13 -1
  155. package/src/cloudflare/bindings.ts +3 -1
  156. package/src/cloudflare/bucket.ts +3 -1
  157. package/src/cloudflare/bundle/cloudflare-internal.ts +42 -0
  158. package/src/cloudflare/bundle/nodejs-builtin-modules.ts +14 -0
  159. package/src/cloudflare/bundle/plugin-als-external.ts +13 -0
  160. package/src/cloudflare/bundle/plugin-hybrid-node-compat.ts +29 -28
  161. package/src/cloudflare/bundle/plugin-node-compat.ts +114 -0
  162. package/src/cloudflare/bundle/plugin-wasm.ts +34 -14
  163. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  164. package/src/cloudflare/d1-local-migrations.ts +2 -2
  165. package/src/cloudflare/durable-object-namespace.ts +1 -0
  166. package/src/cloudflare/hyperdrive.ts +136 -95
  167. package/src/cloudflare/index.ts +1 -0
  168. package/src/cloudflare/next.ts +123 -0
  169. package/src/cloudflare/website.ts +88 -89
  170. package/src/cloudflare/worker-assets.ts +43 -105
  171. package/src/cloudflare/worker-bundle.ts +46 -9
  172. package/src/cloudflare/worker-metadata.ts +2 -0
  173. package/src/cloudflare/worker.ts +35 -7
  174. package/src/cloudflare/wrangler.json.ts +114 -113
  175. package/src/context.ts +5 -3
  176. package/src/destroy.ts +26 -8
  177. package/src/planetscale/api/client.gen.ts +2882 -0
  178. package/src/planetscale/api/index.ts +4 -0
  179. package/src/planetscale/api/types.gen.ts +13041 -0
  180. package/src/planetscale/branch.ts +170 -162
  181. package/src/planetscale/database.ts +263 -243
  182. package/src/planetscale/index.ts +2 -1
  183. package/src/planetscale/password.ts +89 -86
  184. package/src/planetscale/role.ts +278 -0
  185. package/src/planetscale/utils.ts +289 -128
  186. package/src/scope.ts +12 -3
  187. package/src/secret.ts +0 -4
  188. package/src/state/cloudflare-state-store.ts +4 -4
  189. package/src/test/bun.ts +2 -2
  190. package/src/test/vitest.ts +6 -6
  191. package/src/util/content-type.ts +2 -2
  192. package/src/util/dedent.ts +34 -50
  193. package/src/util/memoize.ts +13 -1
  194. package/templates/astro/alchemy.run.ts +1 -3
  195. package/templates/hono/alchemy.run.ts +0 -1
  196. package/templates/nextjs/README.md +85 -0
  197. package/templates/nextjs/_env +1 -0
  198. package/templates/nextjs/_env.example +1 -0
  199. package/templates/nextjs/_gitignore +46 -0
  200. package/templates/nextjs/alchemy.run.ts +16 -0
  201. package/templates/nextjs/eslint.config.mjs +16 -0
  202. package/templates/nextjs/next-env.d.ts +5 -0
  203. package/templates/nextjs/next.config.ts +10 -0
  204. package/templates/nextjs/open-next.config.ts +5 -0
  205. package/templates/nextjs/package.json +31 -0
  206. package/templates/nextjs/src/app/api/hello/route.ts +19 -0
  207. package/templates/nextjs/src/app/favicon.ico +0 -0
  208. package/templates/nextjs/src/app/globals.css +72 -0
  209. package/templates/nextjs/src/app/layout.tsx +19 -0
  210. package/templates/nextjs/src/app/page.tsx +55 -0
  211. package/templates/nextjs/tsconfig.json +33 -0
  212. package/templates/nextjs/types/env.d.ts +14 -0
  213. package/templates/nuxt/alchemy.run.ts +1 -3
  214. package/templates/react-router/alchemy.run.ts +1 -3
  215. package/templates/rwsdk/alchemy.run.ts +0 -2
  216. package/templates/sveltekit/alchemy.run.ts +1 -3
  217. package/templates/tanstack-start/alchemy.run.ts +1 -3
  218. package/templates/typescript/alchemy.run.ts +0 -1
  219. package/templates/vite/alchemy.run.ts +0 -1
  220. package/workers/cloudflare-state-store.js +58 -58
  221. package/lib/cloudflare/bundle/externals.d.ts +0 -3
  222. package/lib/cloudflare/bundle/externals.d.ts.map +0 -1
  223. package/lib/cloudflare/bundle/externals.js +0 -35
  224. package/lib/cloudflare/bundle/externals.js.map +0 -1
  225. package/lib/cloudflare/bundle/plugin-compat-warning.d.ts +0 -10
  226. package/lib/cloudflare/bundle/plugin-compat-warning.d.ts.map +0 -1
  227. package/lib/cloudflare/bundle/plugin-compat-warning.js +0 -67
  228. package/lib/cloudflare/bundle/plugin-compat-warning.js.map +0 -1
  229. package/lib/planetscale/api.d.ts +0 -53
  230. package/lib/planetscale/api.d.ts.map +0 -1
  231. package/lib/planetscale/api.js +0 -84
  232. package/lib/planetscale/api.js.map +0 -1
  233. package/src/cloudflare/bundle/externals.ts +0 -36
  234. package/src/cloudflare/bundle/plugin-compat-warning.ts +0 -94
  235. 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 { PlanetScaleApi } from "./api.ts";
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
- * Defaults to "main" if not provided
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 PlanetScaleApi({ apiKey });
279
- const branchName =
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
- try {
290
- if (this.output?.name) {
291
- const response = await api.delete(
292
- `/organizations/${props.organizationId}/databases/${databaseName}/branches/${branchName}/passwords/${this.output.id}`,
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
- if (!response.ok && response.status !== 404) {
296
- throw new Error(
297
- `Failed to delete branch: ${response.statusText} ${await response.text()}`,
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
- this.output?.name === name &&
310
- ((this.output?.cidrs === undefined && props.cidrs === undefined) ||
311
- (Array.isArray(this.output?.cidrs) &&
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
- const updateResponse = await api.patch(
319
- `/organizations/${props.organizationId}/databases/${databaseName}/branches/${branchName}/passwords/${this.output.id}`,
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
- try {
339
- const createResponse = await api.post(
340
- `/organizations/${props.organizationId}/databases/${databaseName}/branches/${branchName}/passwords`,
341
- {
342
- name,
343
- role: props.role,
344
- replica: props.replica,
345
- ttl: props.ttl,
346
- cidrs: props.cidrs,
347
- },
348
- );
349
-
350
- if (!createResponse.ok) {
351
- throw new Error(
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
- return this({
359
- id: data.id,
360
- expiresAt: data.expires_at,
361
- host: data.access_host_url,
362
- username: data.username,
363
- password: alchemy.secret(data.plain_text),
364
- nameSlug,
365
- ...props,
366
- name: `${props.name}-${nameSlug}`,
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
+ );