@alchemy.run/planetscale 0.0.0 → 2.0.0-beta.85

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 (123) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +83 -4
  3. package/lib/AuthProvider.d.ts +64 -0
  4. package/lib/AuthProvider.d.ts.map +1 -0
  5. package/lib/AuthProvider.js +395 -0
  6. package/lib/AuthProvider.js.map +1 -0
  7. package/lib/Branch.d.ts +128 -0
  8. package/lib/Branch.d.ts.map +1 -0
  9. package/lib/Branch.js +420 -0
  10. package/lib/Branch.js.map +1 -0
  11. package/lib/Contributions.d.ts +7 -0
  12. package/lib/Contributions.d.ts.map +1 -0
  13. package/lib/Contributions.js +6 -0
  14. package/lib/Contributions.js.map +1 -0
  15. package/lib/Credentials.d.ts +33 -0
  16. package/lib/Credentials.d.ts.map +1 -0
  17. package/lib/Credentials.js +59 -0
  18. package/lib/Credentials.js.map +1 -0
  19. package/lib/Database.d.ts +138 -0
  20. package/lib/Database.d.ts.map +1 -0
  21. package/lib/Database.js +1 -0
  22. package/lib/Database.js.map +1 -0
  23. package/lib/MySQL/MySQLBranch.d.ts +87 -0
  24. package/lib/MySQL/MySQLBranch.d.ts.map +1 -0
  25. package/lib/MySQL/MySQLBranch.js +15 -0
  26. package/lib/MySQL/MySQLBranch.js.map +1 -0
  27. package/lib/MySQL/MySQLClusterSize.d.ts +32 -0
  28. package/lib/MySQL/MySQLClusterSize.d.ts.map +1 -0
  29. package/lib/MySQL/MySQLClusterSize.js +111 -0
  30. package/lib/MySQL/MySQLClusterSize.js.map +1 -0
  31. package/lib/MySQL/MySQLDatabase.d.ts +141 -0
  32. package/lib/MySQL/MySQLDatabase.d.ts.map +1 -0
  33. package/lib/MySQL/MySQLDatabase.js +294 -0
  34. package/lib/MySQL/MySQLDatabase.js.map +1 -0
  35. package/lib/MySQL/MySQLMigrations.d.ts +20 -0
  36. package/lib/MySQL/MySQLMigrations.d.ts.map +1 -0
  37. package/lib/MySQL/MySQLMigrations.js +72 -0
  38. package/lib/MySQL/MySQLMigrations.js.map +1 -0
  39. package/lib/MySQL/MySQLOrigin.d.ts +15 -0
  40. package/lib/MySQL/MySQLOrigin.d.ts.map +1 -0
  41. package/lib/MySQL/MySQLOrigin.js +2 -0
  42. package/lib/MySQL/MySQLOrigin.js.map +1 -0
  43. package/lib/MySQL/MySQLPassword.d.ts +116 -0
  44. package/lib/MySQL/MySQLPassword.d.ts.map +1 -0
  45. package/lib/MySQL/MySQLPassword.js +212 -0
  46. package/lib/MySQL/MySQLPassword.js.map +1 -0
  47. package/lib/MySQL/index.d.ts +6 -0
  48. package/lib/MySQL/index.d.ts.map +1 -0
  49. package/lib/MySQL/index.js +6 -0
  50. package/lib/MySQL/index.js.map +1 -0
  51. package/lib/OAuthClient.d.ts +32 -0
  52. package/lib/OAuthClient.d.ts.map +1 -0
  53. package/lib/OAuthClient.js +52 -0
  54. package/lib/OAuthClient.js.map +1 -0
  55. package/lib/Postgres/PostgresBranch.d.ts +75 -0
  56. package/lib/Postgres/PostgresBranch.d.ts.map +1 -0
  57. package/lib/Postgres/PostgresBranch.js +15 -0
  58. package/lib/Postgres/PostgresBranch.js.map +1 -0
  59. package/lib/Postgres/PostgresClusterSize.d.ts +77 -0
  60. package/lib/Postgres/PostgresClusterSize.d.ts.map +1 -0
  61. package/lib/Postgres/PostgresClusterSize.js +89 -0
  62. package/lib/Postgres/PostgresClusterSize.js.map +1 -0
  63. package/lib/Postgres/PostgresDatabase.d.ts +95 -0
  64. package/lib/Postgres/PostgresDatabase.d.ts.map +1 -0
  65. package/lib/Postgres/PostgresDatabase.js +294 -0
  66. package/lib/Postgres/PostgresDatabase.js.map +1 -0
  67. package/lib/Postgres/PostgresDefaultRole.d.ts +94 -0
  68. package/lib/Postgres/PostgresDefaultRole.d.ts.map +1 -0
  69. package/lib/Postgres/PostgresDefaultRole.js +187 -0
  70. package/lib/Postgres/PostgresDefaultRole.js.map +1 -0
  71. package/lib/Postgres/PostgresMigrations.d.ts +16 -0
  72. package/lib/Postgres/PostgresMigrations.d.ts.map +1 -0
  73. package/lib/Postgres/PostgresMigrations.js +65 -0
  74. package/lib/Postgres/PostgresMigrations.js.map +1 -0
  75. package/lib/Postgres/PostgresOrigin.d.ts +15 -0
  76. package/lib/Postgres/PostgresOrigin.d.ts.map +1 -0
  77. package/lib/Postgres/PostgresOrigin.js +2 -0
  78. package/lib/Postgres/PostgresOrigin.js.map +1 -0
  79. package/lib/Postgres/PostgresRole.d.ts +142 -0
  80. package/lib/Postgres/PostgresRole.d.ts.map +1 -0
  81. package/lib/Postgres/PostgresRole.js +268 -0
  82. package/lib/Postgres/PostgresRole.js.map +1 -0
  83. package/lib/Postgres/index.d.ts +7 -0
  84. package/lib/Postgres/index.d.ts.map +1 -0
  85. package/lib/Postgres/index.js +7 -0
  86. package/lib/Postgres/index.js.map +1 -0
  87. package/lib/Providers.d.ts +24 -0
  88. package/lib/Providers.d.ts.map +1 -0
  89. package/lib/Providers.js +40 -0
  90. package/lib/Providers.js.map +1 -0
  91. package/lib/Util.d.ts +93 -0
  92. package/lib/Util.d.ts.map +1 -0
  93. package/lib/Util.js +135 -0
  94. package/lib/Util.js.map +1 -0
  95. package/lib/index.d.ts +9 -0
  96. package/lib/index.d.ts.map +1 -0
  97. package/lib/index.js +9 -0
  98. package/lib/index.js.map +1 -0
  99. package/package.json +79 -3
  100. package/src/AuthProvider.ts +584 -0
  101. package/src/Branch.ts +657 -0
  102. package/src/Contributions.ts +9 -0
  103. package/src/Credentials.ts +92 -0
  104. package/src/Database.ts +152 -0
  105. package/src/MySQL/MySQLBranch.ts +109 -0
  106. package/src/MySQL/MySQLClusterSize.ts +185 -0
  107. package/src/MySQL/MySQLDatabase.ts +521 -0
  108. package/src/MySQL/MySQLMigrations.ts +133 -0
  109. package/src/MySQL/MySQLOrigin.ts +15 -0
  110. package/src/MySQL/MySQLPassword.ts +428 -0
  111. package/src/MySQL/index.ts +5 -0
  112. package/src/OAuthClient.ts +59 -0
  113. package/src/Postgres/PostgresBranch.ts +97 -0
  114. package/src/Postgres/PostgresClusterSize.ts +262 -0
  115. package/src/Postgres/PostgresDatabase.ts +469 -0
  116. package/src/Postgres/PostgresDefaultRole.ts +340 -0
  117. package/src/Postgres/PostgresMigrations.ts +122 -0
  118. package/src/Postgres/PostgresOrigin.ts +15 -0
  119. package/src/Postgres/PostgresRole.ts +547 -0
  120. package/src/Postgres/index.ts +6 -0
  121. package/src/Providers.ts +67 -0
  122. package/src/Util.ts +205 -0
  123. package/src/index.ts +14 -0
@@ -0,0 +1,469 @@
1
+ import {
2
+ diffMigrations,
3
+ migrationsAttrs,
4
+ migrationsInputOf,
5
+ stampedOf,
6
+ } from "@alchemy.run/sql/Migrations";
7
+ import { hashImports } from "@alchemy.run/sql/SqlFile";
8
+ import * as planetscale from "@distilled.cloud/planetscale";
9
+ import { Credentials } from "@distilled.cloud/planetscale/Credentials";
10
+ import { havePropsChanged, isResolved } from "alchemy/Diff";
11
+ import { createPhysicalName } from "alchemy/PhysicalName";
12
+ import * as Provider from "alchemy/Provider";
13
+ import { Resource } from "alchemy/Resource";
14
+ import { recordsEqual } from "alchemy/Util/equal";
15
+ import * as Effect from "effect/Effect";
16
+ import * as Stream from "effect/Stream";
17
+ import type { BaseDatabaseAttributes, BaseDatabaseProps } from "../Database.ts";
18
+ import type { Providers } from "../Providers.ts";
19
+ import {
20
+ deleteUnprotectedDatabase,
21
+ PlanetscaleConflict,
22
+ replaceDatabase,
23
+ waitForBranchReady,
24
+ waitForDatabaseReady,
25
+ } from "../Util.ts";
26
+ import {
27
+ ensurePostgresProductionBranchClusterSize,
28
+ toPostgresClusterArch,
29
+ toPostgresClusterSku,
30
+ type PostgresClusterSize,
31
+ } from "./PostgresClusterSize.ts";
32
+ import { runPostgresImports, runPostgresMigrations } from "./PostgresMigrations.ts";
33
+
34
+ /**
35
+ * Properties for creating or updating a PostgreSQL PlanetScale database.
36
+ */
37
+ export interface PostgresDatabaseProps extends BaseDatabaseProps {
38
+ /**
39
+ * The PostgreSQL database cluster size. Required.
40
+ *
41
+ * Short NAS sizes (`"PS_10"`) are expanded using the target region and
42
+ * architecture. [PlanetScale Metal](https://planetscale.com/docs/metal)
43
+ * requires the full SKU (e.g. `"M1_10_AWS_ARM_D_METAL_10"`) because
44
+ * Metal also encodes CPU series, architecture, and NVMe drive size.
45
+ *
46
+ * @see https://planetscale.com/docs/postgres/pricing
47
+ * @see {@link PostgresClusterSize}
48
+ */
49
+ clusterSize: PostgresClusterSize;
50
+
51
+ /**
52
+ * PostgreSQL major version. Defaults to the latest available major
53
+ * version.
54
+ */
55
+ majorVersion?: string;
56
+
57
+ /**
58
+ * CPU architecture. Cannot be changed after creation.
59
+ * @default "x86"
60
+ */
61
+ arch?: "x86" | "arm";
62
+ }
63
+
64
+ /**
65
+ * Output attributes of a deployed PostgreSQL PlanetScale database.
66
+ */
67
+ export interface PostgresDatabaseAttributes extends BaseDatabaseAttributes {
68
+ /** PostgreSQL CPU architecture. */
69
+ arch: "x86" | "arm";
70
+ }
71
+
72
+ /**
73
+ * A PostgreSQL PlanetScale database. For MySQL, use {@link MySQLDatabase}
74
+ * instead.
75
+ *
76
+ * ### Creating a PostgreSQL Database
77
+ * **Example:** Basic PostgreSQL database
78
+ * ```typescript
79
+ * const db = yield* Planetscale.PostgresDatabase("MyDb", {
80
+ * clusterSize: "PS_10",
81
+ * });
82
+ * ```
83
+ *
84
+ * **Example:** PlanetScale Metal (NVMe)
85
+ * ```typescript
86
+ * const db = yield* Planetscale.PostgresDatabase("MyDb", {
87
+ * clusterSize: "M1_10_AWS_ARM_D_METAL_10",
88
+ * arch: "arm",
89
+ * });
90
+ * ```
91
+ *
92
+ * ### Migrations and seed data
93
+ * **Example:** Apply migrations and seed files
94
+ * ```typescript
95
+ * const db = yield* Planetscale.PostgresDatabase("MyDb", {
96
+ * clusterSize: "PS_10",
97
+ * migrationsDir: "./migrations/postgres",
98
+ * importFiles: ["./seed/postgres.sql"],
99
+ * });
100
+ * ```
101
+ *
102
+ * ### Deletion protection
103
+ * **Example:** Refuse deletes of a production database
104
+ * ```typescript
105
+ * const db = yield* Planetscale.PostgresDatabase("MyDb", {
106
+ * clusterSize: "PS_10",
107
+ * deletionProtection: true,
108
+ * });
109
+ * ```
110
+ *
111
+ * ### Adoption
112
+ * **Example:** Adopting an existing database
113
+ * ```typescript
114
+ * import { adopt } from "alchemy/AdoptPolicy";
115
+ *
116
+ * const db = yield* Planetscale.PostgresDatabase("Existing", {
117
+ * name: "existing-db",
118
+ * clusterSize: "PS_10",
119
+ * }).pipe(adopt());
120
+ * ```
121
+ */
122
+ export type PostgresDatabase = Resource<
123
+ "Planetscale.PostgresDatabase",
124
+ PostgresDatabaseProps,
125
+ PostgresDatabaseAttributes,
126
+ never,
127
+ Providers
128
+ >;
129
+
130
+ /** @resource */
131
+ export const PostgresDatabase = Resource<PostgresDatabase>("Planetscale.PostgresDatabase");
132
+
133
+ export const PostgresDatabaseProvider = () =>
134
+ Provider.succeed(PostgresDatabase, {
135
+ stables: ["id", "organization", "region"],
136
+ diff: Effect.fn(function* ({ news, olds, output }) {
137
+ if (!isResolved(news)) return undefined;
138
+
139
+ // Database names are rename-mutable (reconcile folds `new_name`
140
+ // into the settings sync), so `name` cannot live in the
141
+ // provider-level stables. Almost no update is a rename though —
142
+ // for those, advertise `name` as stable on the update so
143
+ // downstream consumers (branches, roles) still resolve
144
+ // `database.name` at plan time instead of seeing `undefined` and
145
+ // falsely planning a replacement. The name only changes when the
146
+ // `name` prop itself changes: an explicit name renames iff it
147
+ // differs from the observed name, and an omitted name is
148
+ // engine-generated deterministically (stable across updates).
149
+ const nameIsStable =
150
+ output?.name !== undefined &&
151
+ (news.name !== undefined ? news.name === output.name : olds?.name === undefined);
152
+ const stables = nameIsStable ? ["id", "organization", "region", "name"] : undefined;
153
+
154
+ if (news.region?.slug && output?.region?.slug && news.region.slug !== output.region.slug) {
155
+ return yield* replaceDatabase(news, output, "region");
156
+ }
157
+
158
+ if (news.replicas !== olds.replicas) {
159
+ return yield* replaceDatabase(news, output, "replicas");
160
+ }
161
+
162
+ const oldArch = output?.arch ?? olds.arch ?? "x86";
163
+ if (news.arch && news.arch !== oldArch) {
164
+ return yield* replaceDatabase(news, output, "arch");
165
+ }
166
+
167
+ if (yield* diffMigrations({ news, output })) {
168
+ return { action: "update", stables } as const;
169
+ }
170
+ if (news.importFiles?.length) {
171
+ const newHashes = yield* hashImports(news.importFiles, yield* rootDir);
172
+ if (!recordsEqual(newHashes, output?.importHashes ?? {})) {
173
+ return { action: "update", stables } as const;
174
+ }
175
+ }
176
+
177
+ // Remaining prop changes (rename, settings, clusterSize, …) are
178
+ // in-place updates. Decide them here instead of falling back to
179
+ // the engine's default deep-compare so the conditional `name`
180
+ // stable above is attached — the default path uses the
181
+ // provider-level stables, which strip `name` from downstream plan
182
+ // resolution.
183
+ if (havePropsChanged(olds, news)) {
184
+ return { action: "update", stables } as const;
185
+ }
186
+
187
+ // Nothing changed. Still advertise the conditional `name` stable so
188
+ // a `--force` deploy (which upgrades this noop to an update) keeps
189
+ // `name` resolvable downstream instead of falsely replacing
190
+ // consumers such as roles and passwords (#1832).
191
+ return stables ? ({ action: "noop", stables } as const) : undefined;
192
+ }),
193
+
194
+ read: Effect.fn(function* ({ id, output, olds }) {
195
+ const { organization } = yield* yield* Credentials;
196
+ const databaseName = output?.name ?? (yield* createDatabaseName(id, olds?.name));
197
+
198
+ const data = yield* planetscale
199
+ .getDatabase({
200
+ organization,
201
+ database: databaseName,
202
+ })
203
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
204
+
205
+ if (!data) return undefined;
206
+
207
+ if (data.kind !== "postgresql") {
208
+ return yield* new PlanetscaleConflict({
209
+ message:
210
+ `Planetscale database "${data.name}" has kind "${data.kind}" but this resource ` +
211
+ `is a PostgresDatabase. Use Planetscale.${data.kind === "mysql" ? "MySQLDatabase" : data.kind}() instead, ` +
212
+ `or delete the existing database and retry.`,
213
+ });
214
+ }
215
+
216
+ const defaultBranch = data.default_branch ?? "main";
217
+
218
+ // Observe `arch` and `clusterSize` from the default branch rather
219
+ // than echoing input.
220
+ const branch = yield* planetscale
221
+ .getBranch({
222
+ organization,
223
+ database: data.name,
224
+ branch: defaultBranch,
225
+ })
226
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
227
+
228
+ const arch = toPostgresClusterArch(branch!.cluster_architecture);
229
+ const clusterSize = branch!.cluster_name;
230
+
231
+ return {
232
+ id: data.id,
233
+ name: data.name,
234
+ organization,
235
+ state: data.state,
236
+ defaultBranch,
237
+ plan: data.plan ?? "hobby",
238
+ createdAt: data.created_at,
239
+ updatedAt: data.updated_at,
240
+ htmlUrl: data.html_url,
241
+ region: { slug: data.region.slug },
242
+ migrationsDir: output?.migrationsDir ?? (olds && migrationsInputOf(olds))?.dir,
243
+ migrationsTable: output?.migrationsTable ?? (olds && migrationsInputOf(olds))?.table,
244
+ migrationsHashes: output?.migrationsHashes ?? {},
245
+ importHashes: output?.importHashes ?? {},
246
+ clusterSize,
247
+ arch,
248
+ requireApprovalForDeploy: data.require_approval_for_deploy ?? false,
249
+ restrictBranchRegion: data.restrict_branch_region ?? false,
250
+ productionBranchWebConsole: data.production_branch_web_console ?? false,
251
+ deletionProtection: data.deletion_protected ?? false,
252
+ };
253
+ }),
254
+
255
+ reconcile: Effect.fn(function* ({ id, news, output, session }) {
256
+ const { organization } = yield* yield* Credentials;
257
+ const newName = yield* createDatabaseName(id, news.name);
258
+ const clusterSize = toPostgresClusterSku({
259
+ size: news.clusterSize,
260
+ arch: news.arch,
261
+ region: news.region?.slug,
262
+ });
263
+
264
+ // Observe — read live state under either the cached name (rename
265
+ // / refresh) or the freshly-derived name (greenfield). Observing
266
+ // unconditionally absorbs the race between `read` and `reconcile`
267
+ // — if a foreign actor (or a previously-failed-to-persist
268
+ // create) produced a database under the desired name in the
269
+ // meantime, we'll find it here and skip the duplicate create.
270
+ // Adoption routing has already been gated by `read` returning
271
+ // `Unowned`, so by the time we reach reconcile any observed
272
+ // database is fair game.
273
+ const observedName = output?.name ?? newName;
274
+ let observed = yield* planetscale
275
+ .getDatabase({
276
+ organization,
277
+ database: observedName,
278
+ })
279
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
280
+
281
+ // Ensure — if missing, create.
282
+ if (!observed) {
283
+ yield* session.note("Creating database...");
284
+ observed = yield* planetscale.createDatabase({
285
+ organization,
286
+ name: newName,
287
+ region: news.region?.slug,
288
+ kind: "postgresql",
289
+ cluster_size: clusterSize,
290
+ replicas: news.replicas,
291
+ major_version: news.majorVersion,
292
+ });
293
+ }
294
+
295
+ // Wait for the database to finish provisioning before any
296
+ // downstream sync runs. PlanetScale reports `state: "pending"`
297
+ // for a while after createDatabase returns, during which branch
298
+ // operations on this database (including our own settings PATCH)
299
+ // race against the provisioning fiber.
300
+ yield* waitForDatabaseReady(organization, observed.name, session);
301
+
302
+ if (observed.kind !== "postgresql") {
303
+ return yield* new PlanetscaleConflict({
304
+ message:
305
+ `Planetscale database "${observed.name}" has kind "${observed.kind}" but this resource ` +
306
+ `is a PostgresDatabase. Use Planetscale.${observed.kind === "mysql" ? "MySQLDatabase" : observed.kind}() instead, ` +
307
+ `or delete the existing database and retry.`,
308
+ });
309
+ }
310
+
311
+ // Sync — ensure a non-`main` default branch exists before
312
+ // referencing it via `default_branch`. The branch is created
313
+ // empty (no cluster size yet); we'll size it below.
314
+ if (news.defaultBranch && news.defaultBranch !== "main") {
315
+ yield* waitForDatabaseReady(organization, observed.name, session);
316
+ const branchExists = yield* planetscale
317
+ .getBranch({
318
+ organization,
319
+ database: observed.name,
320
+ branch: news.defaultBranch,
321
+ })
322
+ .pipe(
323
+ Effect.map(() => true),
324
+ Effect.catchTag("NotFound", () => Effect.succeed(false)),
325
+ );
326
+ if (!branchExists) {
327
+ yield* planetscale.createBranch({
328
+ organization,
329
+ database: observed.name,
330
+ name: news.defaultBranch,
331
+ parent_branch: "main",
332
+ });
333
+ }
334
+ }
335
+
336
+ // Sync settings — `updateSettings` is upsert-shaped, so we can
337
+ // call it with the full desired payload on every reconcile.
338
+ // Folds the rename in via `new_name` so we don't pay for a
339
+ // separate PATCH.
340
+ const updated = yield* planetscale.updateDatabaseSettings({
341
+ organization,
342
+ database: observed.name,
343
+ new_name: newName !== observed.name ? newName : undefined,
344
+ require_approval_for_deploy: news.requireApprovalForDeploy,
345
+ restrict_branch_region: news.restrictBranchRegion,
346
+ production_branch_web_console: news.productionBranchWebConsole,
347
+ deletion_protected: news.deletionProtection,
348
+ default_branch: news.defaultBranch,
349
+ });
350
+
351
+ // Sync cluster size on the active default branch.
352
+ const branch = news.defaultBranch ?? updated.default_branch ?? "main";
353
+ yield* ensurePostgresProductionBranchClusterSize(
354
+ organization,
355
+ updated.name,
356
+ branch,
357
+ news.clusterSize,
358
+ );
359
+
360
+ const migrationTarget = {
361
+ organization,
362
+ database: updated.name,
363
+ branch,
364
+ };
365
+ const migrationsInput = migrationsInputOf(news);
366
+ if (migrationsInput || news.importFiles?.length) {
367
+ yield* waitForBranchReady(organization, updated.name, branch, session);
368
+ }
369
+ const migrations = migrationsInput
370
+ ? yield* runPostgresMigrations(migrationTarget, migrationsInput, stampedOf(output))
371
+ : undefined;
372
+ const importHashes = news.importFiles?.length
373
+ ? yield* runPostgresImports(
374
+ migrationTarget,
375
+ news.importFiles,
376
+ yield* rootDir,
377
+ output?.importHashes ?? {},
378
+ )
379
+ : {};
380
+
381
+ return {
382
+ id: updated.id,
383
+ name: updated.name,
384
+ organization,
385
+ state: updated.state,
386
+ defaultBranch: updated.default_branch ?? branch,
387
+ plan: updated.plan ?? output?.plan ?? "hobby",
388
+ createdAt: updated.created_at,
389
+ updatedAt: updated.updated_at,
390
+ htmlUrl: updated.html_url,
391
+ region: { slug: updated.region.slug },
392
+ clusterSize: clusterSize,
393
+ ...migrationsAttrs({ input: migrationsInput, run: migrations, output }),
394
+ importHashes,
395
+ arch: news.arch ?? output?.arch ?? "x86",
396
+ requireApprovalForDeploy: updated.require_approval_for_deploy ?? false,
397
+ restrictBranchRegion: updated.restrict_branch_region ?? false,
398
+ productionBranchWebConsole: updated.production_branch_web_console ?? false,
399
+ deletionProtection: updated.deletion_protected ?? false,
400
+ } satisfies PostgresDatabaseAttributes;
401
+ }),
402
+
403
+ delete: Effect.fn(function* ({ output }) {
404
+ yield* deleteUnprotectedDatabase(output.organization, output.name);
405
+ }),
406
+
407
+ list: Effect.fn(function* () {
408
+ const { organization } = yield* yield* Credentials;
409
+
410
+ const databases = yield* planetscale.listDatabases.pages({ organization }).pipe(
411
+ Stream.runCollect,
412
+ Effect.map((chunk) =>
413
+ Array.from(chunk).flatMap((page) => page.data.filter((db) => db.kind === "postgresql")),
414
+ ),
415
+ );
416
+
417
+ const rows = yield* Effect.forEach(
418
+ databases,
419
+ (data) =>
420
+ Effect.gen(function* () {
421
+ const defaultBranch = data.default_branch ?? "main";
422
+ const branch = yield* planetscale
423
+ .getBranch({
424
+ organization,
425
+ database: data.name,
426
+ branch: defaultBranch,
427
+ })
428
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
429
+
430
+ const arch = toPostgresClusterArch(branch?.cluster_architecture);
431
+ const clusterSize = branch?.cluster_name ?? "";
432
+
433
+ const attrs: PostgresDatabase["Attributes"] = {
434
+ id: data.id,
435
+ name: data.name,
436
+ organization,
437
+ state: data.state,
438
+ defaultBranch,
439
+ plan: data.plan ?? "hobby",
440
+ createdAt: data.created_at,
441
+ updatedAt: data.updated_at,
442
+ htmlUrl: data.html_url,
443
+ region: { slug: data.region.slug },
444
+ clusterSize,
445
+ migrationsDir: undefined,
446
+ migrationsTable: undefined,
447
+ migrationsHashes: {},
448
+ importHashes: {},
449
+ arch,
450
+ requireApprovalForDeploy: data.require_approval_for_deploy ?? false,
451
+ restrictBranchRegion: data.restrict_branch_region ?? false,
452
+ productionBranchWebConsole: data.production_branch_web_console ?? false,
453
+ deletionProtection: data.deletion_protected ?? false,
454
+ };
455
+ return attrs;
456
+ }),
457
+ { concurrency: 10 },
458
+ );
459
+
460
+ return rows;
461
+ }),
462
+ });
463
+
464
+ const createDatabaseName = (id: string, name: string | undefined) =>
465
+ Effect.gen(function* () {
466
+ return name ?? (yield* createPhysicalName({ id, lowercase: true, maxLength: 63 }));
467
+ });
468
+
469
+ const rootDir = Effect.sync(() => process.cwd());