@alchemy.run/planetscale 2.0.0-beta.84 → 2.0.0-beta.86

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 (102) hide show
  1. package/lib/AuthProvider.d.ts +1 -1
  2. package/lib/AuthProvider.d.ts.map +1 -1
  3. package/lib/AuthProvider.js +58 -47
  4. package/lib/AuthProvider.js.map +1 -1
  5. package/lib/Branch.d.ts +6 -1
  6. package/lib/Branch.d.ts.map +1 -1
  7. package/lib/Branch.js +23 -2
  8. package/lib/Branch.js.map +1 -1
  9. package/lib/Contributions.d.ts +1 -1
  10. package/lib/Contributions.d.ts.map +1 -1
  11. package/lib/Contributions.js.map +1 -1
  12. package/lib/Credentials.d.ts +1 -1
  13. package/lib/Credentials.d.ts.map +1 -1
  14. package/lib/Credentials.js +6 -12
  15. package/lib/Credentials.js.map +1 -1
  16. package/lib/Database.d.ts +7 -0
  17. package/lib/Database.d.ts.map +1 -1
  18. package/lib/Database.js.map +1 -1
  19. package/lib/MySQL/MySQLBranch.d.ts +26 -2
  20. package/lib/MySQL/MySQLBranch.d.ts.map +1 -1
  21. package/lib/MySQL/MySQLBranch.js +15 -1
  22. package/lib/MySQL/MySQLBranch.js.map +1 -1
  23. package/lib/MySQL/MySQLClusterSize.d.ts +1 -1
  24. package/lib/MySQL/MySQLClusterSize.d.ts.map +1 -1
  25. package/lib/MySQL/MySQLClusterSize.js +1 -1
  26. package/lib/MySQL/MySQLClusterSize.js.map +1 -1
  27. package/lib/MySQL/MySQLDatabase.d.ts +28 -2
  28. package/lib/MySQL/MySQLDatabase.d.ts.map +1 -1
  29. package/lib/MySQL/MySQLDatabase.js +72 -51
  30. package/lib/MySQL/MySQLDatabase.js.map +1 -1
  31. package/lib/MySQL/MySQLMigrations.d.ts +7 -11
  32. package/lib/MySQL/MySQLMigrations.d.ts.map +1 -1
  33. package/lib/MySQL/MySQLMigrations.js +24 -58
  34. package/lib/MySQL/MySQLMigrations.js.map +1 -1
  35. package/lib/MySQL/MySQLOrigin.d.ts.map +1 -1
  36. package/lib/MySQL/MySQLOrigin.js.map +1 -1
  37. package/lib/MySQL/MySQLPassword.d.ts.map +1 -1
  38. package/lib/MySQL/MySQLPassword.js +2 -2
  39. package/lib/MySQL/MySQLPassword.js.map +1 -1
  40. package/lib/MySQL/index.d.ts.map +1 -1
  41. package/lib/MySQL/index.js.map +1 -1
  42. package/lib/OAuthClient.d.ts.map +1 -1
  43. package/lib/OAuthClient.js.map +1 -1
  44. package/lib/Postgres/PostgresBranch.d.ts +26 -2
  45. package/lib/Postgres/PostgresBranch.d.ts.map +1 -1
  46. package/lib/Postgres/PostgresBranch.js +15 -1
  47. package/lib/Postgres/PostgresBranch.js.map +1 -1
  48. package/lib/Postgres/PostgresClusterSize.d.ts.map +1 -1
  49. package/lib/Postgres/PostgresClusterSize.js.map +1 -1
  50. package/lib/Postgres/PostgresDatabase.d.ts +28 -2
  51. package/lib/Postgres/PostgresDatabase.d.ts.map +1 -1
  52. package/lib/Postgres/PostgresDatabase.js +77 -56
  53. package/lib/Postgres/PostgresDatabase.js.map +1 -1
  54. package/lib/Postgres/PostgresDefaultRole.d.ts.map +1 -1
  55. package/lib/Postgres/PostgresDefaultRole.js +2 -2
  56. package/lib/Postgres/PostgresDefaultRole.js.map +1 -1
  57. package/lib/Postgres/PostgresMigrations.d.ts +3 -11
  58. package/lib/Postgres/PostgresMigrations.d.ts.map +1 -1
  59. package/lib/Postgres/PostgresMigrations.js +15 -66
  60. package/lib/Postgres/PostgresMigrations.js.map +1 -1
  61. package/lib/Postgres/PostgresOrigin.d.ts.map +1 -1
  62. package/lib/Postgres/PostgresOrigin.js.map +1 -1
  63. package/lib/Postgres/PostgresRole.d.ts.map +1 -1
  64. package/lib/Postgres/PostgresRole.js +3 -3
  65. package/lib/Postgres/PostgresRole.js.map +1 -1
  66. package/lib/Postgres/index.d.ts.map +1 -1
  67. package/lib/Postgres/index.js.map +1 -1
  68. package/lib/Providers.d.ts +1 -1
  69. package/lib/Providers.d.ts.map +1 -1
  70. package/lib/Providers.js +3 -1
  71. package/lib/Providers.js.map +1 -1
  72. package/lib/ServiceToken.d.ts +142 -0
  73. package/lib/ServiceToken.d.ts.map +1 -0
  74. package/lib/ServiceToken.js +268 -0
  75. package/lib/ServiceToken.js.map +1 -0
  76. package/lib/Util.d.ts +3 -3
  77. package/lib/Util.d.ts.map +1 -1
  78. package/lib/Util.js +1 -1
  79. package/lib/Util.js.map +1 -1
  80. package/lib/index.d.ts +1 -0
  81. package/lib/index.d.ts.map +1 -1
  82. package/lib/index.js +1 -0
  83. package/lib/index.js.map +1 -1
  84. package/package.json +9 -9
  85. package/src/AuthProvider.ts +91 -78
  86. package/src/Branch.ts +36 -3
  87. package/src/Credentials.ts +8 -32
  88. package/src/Database.ts +7 -0
  89. package/src/MySQL/MySQLBranch.ts +28 -2
  90. package/src/MySQL/MySQLClusterSize.ts +1 -1
  91. package/src/MySQL/MySQLDatabase.ts +103 -63
  92. package/src/MySQL/MySQLMigrations.ts +31 -80
  93. package/src/MySQL/MySQLPassword.ts +2 -2
  94. package/src/Postgres/PostgresBranch.ts +28 -2
  95. package/src/Postgres/PostgresDatabase.ts +104 -63
  96. package/src/Postgres/PostgresDefaultRole.ts +2 -2
  97. package/src/Postgres/PostgresMigrations.ts +20 -91
  98. package/src/Postgres/PostgresRole.ts +4 -4
  99. package/src/Providers.ts +3 -0
  100. package/src/ServiceToken.ts +431 -0
  101. package/src/Util.ts +1 -1
  102. package/src/index.ts +1 -0
@@ -180,12 +180,109 @@ export type MySQLDatabase = Resource<
180
180
  MySQLDatabaseProps,
181
181
  MySQLDatabaseAttributes,
182
182
  never,
183
- Providers
183
+ Providers,
184
+ MySQLDatabaseLookup
184
185
  >;
185
186
 
186
- /** @resource */
187
+ /**
188
+ * Props for {@link MySQLDatabase.lookup}: find a MySQL database this
189
+ * stack doesn't manage by its name, in the credentialed organization.
190
+ */
191
+ export interface MySQLDatabaseLookup {
192
+ /** Database name. */
193
+ name: string;
194
+ }
195
+
196
+ /**
197
+ * A PlanetScale MySQL (Vitess) database.
198
+ *
199
+ * ### Looking up an existing MySQL database
200
+ * `MySQLDatabase.lookup` references a MySQL database Alchemy doesn't manage (created in
201
+ * the PlanetScale dashboard or by another stack). It is read during
202
+ * plan/deploy, never created, updated or deleted.
203
+ *
204
+ * **Example:** Branch an existing database
205
+ * ```typescript
206
+ * const db = Planetscale.MySQLDatabase.lookup({ name: "app" });
207
+ * const preview = yield* Planetscale.MySQLBranch("Preview", {
208
+ * database: db,
209
+ * isProduction: false,
210
+ * });
211
+ * ```
212
+ *
213
+ * @resource
214
+ */
187
215
  export const MySQLDatabase = Resource<MySQLDatabase>("Planetscale.MySQLDatabase");
188
216
 
217
+ /** Observe a database by name; `read` and `lookup` share it. */
218
+ const readMySQLDatabase = Effect.fn(function* ({
219
+ id,
220
+ output,
221
+ olds,
222
+ }: {
223
+ id: string;
224
+ output?: MySQLDatabase["Attributes"];
225
+ olds?: Partial<MySQLDatabaseProps>;
226
+ }) {
227
+ const { organization } = yield* yield* Credentials;
228
+ const databaseName = output?.name ?? (yield* createDatabaseName(id, olds?.name));
229
+ return yield* planetscale
230
+ .getDatabase({
231
+ organization,
232
+ database: databaseName,
233
+ })
234
+ .pipe(
235
+ Effect.flatMap(
236
+ Effect.fn(function* (data) {
237
+ if (data.kind !== "mysql") {
238
+ return yield* new PlanetscaleConflict({
239
+ message:
240
+ `Planetscale database "${data.name}" has kind "${data.kind}" but this resource ` +
241
+ `is a MySQLDatabase. Use Planetscale.${data.kind === "postgresql" ? "PostgresDatabase" : data.kind}() instead, ` +
242
+ `or delete the existing database and retry.`,
243
+ });
244
+ }
245
+ // Observe the default keyspace's live replica configuration
246
+ // so adopted databases diff against reality.
247
+ const replicas = yield* observeDefaultKeyspaceReplicas(
248
+ organization,
249
+ data.name,
250
+ data.default_branch ?? "main",
251
+ );
252
+ return {
253
+ id: data.id,
254
+ name: data.name,
255
+ organization,
256
+ state: data.state,
257
+ defaultBranch: data.default_branch ?? "main",
258
+ replicas,
259
+ plan: data.plan ?? "hobby",
260
+ createdAt: data.created_at,
261
+ updatedAt: data.updated_at,
262
+ htmlUrl: data.html_url,
263
+ region: { slug: data.region.slug },
264
+ migrationsDir: output?.migrationsDir ?? (olds && migrationsInputOf(olds))?.dir,
265
+ migrationsTable: output?.migrationsTable ?? (olds && migrationsInputOf(olds))?.table,
266
+ migrationsHashes: output?.migrationsHashes ?? {},
267
+ importHashes: output?.importHashes ?? {},
268
+ clusterSize: output?.clusterSize ?? "",
269
+ requireApprovalForDeploy: data.require_approval_for_deploy ?? false,
270
+ restrictBranchRegion: data.restrict_branch_region ?? false,
271
+ insightsRawQueries: data.insights_raw_queries ?? false,
272
+ productionBranchWebConsole: data.production_branch_web_console ?? false,
273
+ deletionProtection: data.deletion_protected ?? false,
274
+ automaticMigrations: data.automatic_migrations ?? false,
275
+ migrationFramework: data.migration_framework ?? undefined,
276
+ migrationTableName: data.migration_table_name ?? undefined,
277
+ allowDataBranching: data.allow_data_branching ?? false,
278
+ allowForeignKeyConstraints: data.foreign_keys_enabled ?? false,
279
+ };
280
+ }),
281
+ ),
282
+ Effect.catchTag("NotFound", () => Effect.undefined),
283
+ );
284
+ });
285
+
189
286
  export const MySQLDatabaseProvider = () =>
190
287
  Provider.succeed(MySQLDatabase, {
191
288
  stables: ["id", "organization", "region"],
@@ -246,66 +343,9 @@ export const MySQLDatabaseProvider = () =>
246
343
  return stables ? ({ action: "noop", stables } as const) : undefined;
247
344
  }),
248
345
 
249
- read: Effect.fn(function* ({ id, output, olds }) {
250
- const { organization } = yield* yield* Credentials;
251
- const databaseName = output?.name ?? (yield* createDatabaseName(id, olds?.name));
252
- return yield* planetscale
253
- .getDatabase({
254
- organization,
255
- database: databaseName,
256
- })
257
- .pipe(
258
- Effect.flatMap(
259
- Effect.fn(function* (data) {
260
- if (data.kind !== "mysql") {
261
- return yield* new PlanetscaleConflict({
262
- message:
263
- `Planetscale database "${data.name}" has kind "${data.kind}" but this resource ` +
264
- `is a MySQLDatabase. Use Planetscale.${data.kind === "postgresql" ? "PostgresDatabase" : data.kind}() instead, ` +
265
- `or delete the existing database and retry.`,
266
- });
267
- }
268
- // Observe the default keyspace's live replica configuration
269
- // so adopted databases diff against reality.
270
- const replicas = yield* observeDefaultKeyspaceReplicas(
271
- organization,
272
- data.name,
273
- data.default_branch ?? "main",
274
- );
275
- return {
276
- id: data.id,
277
- name: data.name,
278
- organization,
279
- state: data.state,
280
- defaultBranch: data.default_branch ?? "main",
281
- replicas,
282
- plan: data.plan ?? "hobby",
283
- createdAt: data.created_at,
284
- updatedAt: data.updated_at,
285
- htmlUrl: data.html_url,
286
- region: { slug: data.region.slug },
287
- migrationsDir: output?.migrationsDir ?? (olds && migrationsInputOf(olds))?.dir,
288
- migrationsTable:
289
- output?.migrationsTable ?? (olds && migrationsInputOf(olds))?.table,
290
- migrationsHashes: output?.migrationsHashes ?? {},
291
- importHashes: output?.importHashes ?? {},
292
- clusterSize: output?.clusterSize ?? "",
293
- requireApprovalForDeploy: data.require_approval_for_deploy ?? false,
294
- restrictBranchRegion: data.restrict_branch_region ?? false,
295
- insightsRawQueries: data.insights_raw_queries ?? false,
296
- productionBranchWebConsole: data.production_branch_web_console ?? false,
297
- deletionProtection: data.deletion_protected ?? false,
298
- automaticMigrations: data.automatic_migrations ?? false,
299
- migrationFramework: data.migration_framework ?? undefined,
300
- migrationTableName: data.migration_table_name ?? undefined,
301
- allowDataBranching: data.allow_data_branching ?? false,
302
- allowForeignKeyConstraints: data.foreign_keys_enabled ?? false,
303
- };
304
- }),
305
- ),
306
- Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
307
- );
308
- }),
346
+ lookup: ({ name }) => readMySQLDatabase({ id: name, olds: { name } }),
347
+
348
+ read: readMySQLDatabase,
309
349
 
310
350
  reconcile: Effect.fn(function* ({ id, news, output, session }) {
311
351
  const { organization } = yield* yield* Credentials;
@@ -327,7 +367,7 @@ export const MySQLDatabaseProvider = () =>
327
367
  organization,
328
368
  database: observedName,
329
369
  })
330
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
370
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
331
371
 
332
372
  // Ensure — if missing, create.
333
373
  if (!observed) {
@@ -1,34 +1,18 @@
1
1
  import {
2
- makeMySQLMigrationExecutor,
3
2
  runMigrations,
3
+ withMySqlExecutor,
4
4
  type NormalizedMigrationsInput,
5
+ type SqlExecutor,
5
6
  type StampedMigrationsState,
6
7
  } from "@alchemy.run/sql/Migrations";
7
- import { readSqlFile, splitSqlStatements } from "@alchemy.run/sql/SqlFile";
8
+ import { applySqlImports, splitSqlStatements } from "@alchemy.run/sql/SqlFile";
8
9
  import * as ps from "@distilled.cloud/planetscale";
9
- import * as Data from "effect/Data";
10
10
  import * as Effect from "effect/Effect";
11
11
  import * as Redacted from "effect/Redacted";
12
12
  import * as Schedule from "effect/Schedule";
13
- import type { Connection } from "mysql2/promise";
14
13
 
15
14
  const MIGRATION_PASSWORD_TTL_SECONDS = 600;
16
15
 
17
- // `mysql2` is an optional peer dependency — loaded lazily so importing the
18
- // Planetscale provider never requires the driver unless migrations run.
19
- const importMysql = () =>
20
- import("mysql2/promise").catch((cause) => {
21
- throw new Error(
22
- "Failed to load the 'mysql2' driver. Install the optional peer dependency 'mysql2' to run Planetscale MySQL migrations.",
23
- { cause },
24
- );
25
- });
26
-
27
- export class MySQLMigrationError extends Data.TaggedError("Planetscale::MySQLMigrationError")<{
28
- message: string;
29
- cause?: unknown;
30
- }> {}
31
-
32
16
  export interface MySQLMigrationTarget {
33
17
  organization: string;
34
18
  database: string;
@@ -47,69 +31,48 @@ export const runMySQLMigrations = (
47
31
  runMigrations({
48
32
  input,
49
33
  stamped,
50
- withExecutor: (apply) =>
51
- withMySQLConnection(target, (connection) => apply(makeMySQLMigrationExecutor(connection))),
34
+ withExecutor: (apply) => withMySQLExecutor(target, apply),
52
35
  });
53
36
 
37
+ /**
38
+ * Apply `importFiles`, each once and again only when it changes. Vitess
39
+ * rejects drizzle-kit's `--> statement-breakpoint` markers, so statements
40
+ * run one at a time.
41
+ */
54
42
  export const runMySQLImports = (
55
43
  target: MySQLMigrationTarget,
56
44
  importFiles: ReadonlyArray<string>,
57
45
  rootDir: string,
58
46
  previous: Record<string, string>,
59
47
  ) =>
60
- Effect.gen(function* () {
61
- const hashes: Record<string, string> = { ...previous };
62
- for (const filePath of importFiles) {
63
- const file = yield* readSqlFile(rootDir, filePath);
64
- if (previous[filePath] === file.hash) {
65
- hashes[filePath] = file.hash;
66
- continue;
67
- }
68
- yield* runMySQLSql(target, file.sql);
69
- hashes[filePath] = file.hash;
70
- }
71
- const tracked = new Set(importFiles);
72
- for (const key of Object.keys(hashes)) {
73
- if (!tracked.has(key)) delete hashes[key];
74
- }
75
- return hashes;
48
+ applySqlImports({
49
+ importFiles,
50
+ rootDir,
51
+ previous,
52
+ apply: (file) =>
53
+ withMySQLExecutor(target, (executor) =>
54
+ Effect.forEach(splitSqlStatements(file.sql), (statement) => executor.query(statement), {
55
+ discard: true,
56
+ }),
57
+ ),
76
58
  });
77
59
 
78
- const runMySQLSql = (target: MySQLMigrationTarget, sql: string) =>
79
- withMySQLConnection(target, (connection) =>
80
- Effect.gen(function* () {
81
- for (const statement of splitSqlStatements(sql)) {
82
- yield* mysqlQuery(connection, statement);
83
- }
84
- }),
85
- );
86
-
87
- const withMySQLConnection = <A, E, R>(
60
+ /** A verified-TLS `mysql2` session as a short-lived admin password. */
61
+ const withMySQLExecutor = <A, E, R>(
88
62
  target: MySQLMigrationTarget,
89
- use: (connection: Connection) => Effect.Effect<A, E, R>,
63
+ use: (executor: SqlExecutor) => Effect.Effect<A, E, R>,
90
64
  ) =>
91
65
  withTemporaryMySQLPassword(target, (password) =>
92
- Effect.acquireUseRelease(
93
- Effect.tryPromise({
94
- try: async () => {
95
- const { createConnection } = await importMysql();
96
- return createConnection({
97
- host: password.host,
98
- user: password.username,
99
- password: Redacted.value(password.password),
100
- database: target.database,
101
- multipleStatements: true,
102
- ssl: { rejectUnauthorized: true },
103
- });
104
- },
105
- catch: toMigrationError,
106
- }),
66
+ withMySqlExecutor(
67
+ {
68
+ host: password.host,
69
+ user: password.username,
70
+ password: Redacted.value(password.password),
71
+ database: target.database,
72
+ multipleStatements: true,
73
+ ssl: { rejectUnauthorized: true },
74
+ },
107
75
  use,
108
- (connection) =>
109
- Effect.tryPromise({
110
- try: () => connection.end(),
111
- catch: toMigrationError,
112
- }).pipe(Effect.ignore),
113
76
  ),
114
77
  );
115
78
 
@@ -168,15 +131,3 @@ const withTemporaryMySQLPassword = <A, E, R>(
168
131
  ),
169
132
  ),
170
133
  );
171
-
172
- const mysqlQuery = (connection: Connection, sql: string) =>
173
- Effect.tryPromise({
174
- try: () => connection.query(sql).then(() => undefined),
175
- catch: toMigrationError,
176
- });
177
-
178
- const toMigrationError = (cause: unknown) =>
179
- new MySQLMigrationError({
180
- message: cause instanceof Error ? cause.message : String(cause),
181
- cause,
182
- });
@@ -189,7 +189,7 @@ export const MySQLPasswordProvider = () =>
189
189
  branch: output.branch,
190
190
  }),
191
191
  ),
192
- Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
192
+ Effect.catchTag("NotFound", () => Effect.undefined),
193
193
  );
194
194
  }),
195
195
 
@@ -217,7 +217,7 @@ export const MySQLPasswordProvider = () =>
217
217
  branch: branchName,
218
218
  id: output.id,
219
219
  })
220
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)))
220
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined))
221
221
  : undefined;
222
222
 
223
223
  // 2. Ensure — create the password if it doesn't exist live.
@@ -79,10 +79,36 @@ export type PostgresBranch = Resource<
79
79
  PostgresBranchProps,
80
80
  PostgresBranchAttributes,
81
81
  never,
82
- Providers
82
+ Providers,
83
+ PostgresBranchLookup
83
84
  >;
84
85
 
85
- /** @resource */
86
+ /**
87
+ * Props for {@link PostgresBranch.lookup}: find a Postgres branch this
88
+ * stack doesn't manage by its database and name.
89
+ */
90
+ export interface PostgresBranchLookup {
91
+ /** Database name. */
92
+ database: string;
93
+ /** Branch name. */
94
+ name: string;
95
+ }
96
+
97
+ /**
98
+ * A branch of a PlanetScale Postgres database.
99
+ *
100
+ * ### Looking up an existing Postgres branch
101
+ * `PostgresBranch.lookup` references a Postgres branch Alchemy doesn't manage (created in
102
+ * the PlanetScale dashboard or by another stack). It is read during
103
+ * plan/deploy, never created, updated or deleted.
104
+ *
105
+ * **Example:** Reference an existing branch
106
+ * ```typescript
107
+ * const main = Planetscale.PostgresBranch.lookup({ database: "app", name: "main" });
108
+ * ```
109
+ *
110
+ * @resource
111
+ */
86
112
  export const PostgresBranch = Resource<PostgresBranch>("Planetscale.PostgresBranch");
87
113
 
88
114
  export const PostgresBranchProvider = () =>
@@ -124,12 +124,110 @@ export type PostgresDatabase = Resource<
124
124
  PostgresDatabaseProps,
125
125
  PostgresDatabaseAttributes,
126
126
  never,
127
- Providers
127
+ Providers,
128
+ PostgresDatabaseLookup
128
129
  >;
129
130
 
130
- /** @resource */
131
+ /**
132
+ * Props for {@link PostgresDatabase.lookup}: find a Postgres database this
133
+ * stack doesn't manage by its name, in the credentialed organization.
134
+ */
135
+ export interface PostgresDatabaseLookup {
136
+ /** Database name. */
137
+ name: string;
138
+ }
139
+
140
+ /**
141
+ * A PlanetScale Postgres database.
142
+ *
143
+ * ### Looking up an existing Postgres database
144
+ * `PostgresDatabase.lookup` references a Postgres database Alchemy doesn't manage (created in
145
+ * the PlanetScale dashboard or by another stack). It is read during
146
+ * plan/deploy, never created, updated or deleted.
147
+ *
148
+ * **Example:** Branch an existing database
149
+ * ```typescript
150
+ * const db = Planetscale.PostgresDatabase.lookup({ name: "app" });
151
+ * const preview = yield* Planetscale.PostgresBranch("Preview", {
152
+ * database: db,
153
+ * isProduction: false,
154
+ * });
155
+ * ```
156
+ *
157
+ * @resource
158
+ */
131
159
  export const PostgresDatabase = Resource<PostgresDatabase>("Planetscale.PostgresDatabase");
132
160
 
161
+ /** Observe a database by name; `read` and `lookup` share it. */
162
+ const readPostgresDatabase = Effect.fn(function* ({
163
+ id,
164
+ output,
165
+ olds,
166
+ }: {
167
+ id: string;
168
+ output?: PostgresDatabase["Attributes"];
169
+ olds?: Partial<PostgresDatabaseProps>;
170
+ }) {
171
+ const { organization } = yield* yield* Credentials;
172
+ const databaseName = output?.name ?? (yield* createDatabaseName(id, olds?.name));
173
+
174
+ const data = yield* planetscale
175
+ .getDatabase({
176
+ organization,
177
+ database: databaseName,
178
+ })
179
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
180
+
181
+ if (!data) return undefined;
182
+
183
+ if (data.kind !== "postgresql") {
184
+ return yield* new PlanetscaleConflict({
185
+ message:
186
+ `Planetscale database "${data.name}" has kind "${data.kind}" but this resource ` +
187
+ `is a PostgresDatabase. Use Planetscale.${data.kind === "mysql" ? "MySQLDatabase" : data.kind}() instead, ` +
188
+ `or delete the existing database and retry.`,
189
+ });
190
+ }
191
+
192
+ const defaultBranch = data.default_branch ?? "main";
193
+
194
+ // Observe `arch` and `clusterSize` from the default branch rather
195
+ // than echoing input.
196
+ const branch = yield* planetscale
197
+ .getBranch({
198
+ organization,
199
+ database: data.name,
200
+ branch: defaultBranch,
201
+ })
202
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
203
+
204
+ const arch = toPostgresClusterArch(branch!.cluster_architecture);
205
+ const clusterSize = branch!.cluster_name;
206
+
207
+ return {
208
+ id: data.id,
209
+ name: data.name,
210
+ organization,
211
+ state: data.state,
212
+ defaultBranch,
213
+ plan: data.plan ?? "hobby",
214
+ createdAt: data.created_at,
215
+ updatedAt: data.updated_at,
216
+ htmlUrl: data.html_url,
217
+ region: { slug: data.region.slug },
218
+ migrationsDir: output?.migrationsDir ?? (olds && migrationsInputOf(olds))?.dir,
219
+ migrationsTable: output?.migrationsTable ?? (olds && migrationsInputOf(olds))?.table,
220
+ migrationsHashes: output?.migrationsHashes ?? {},
221
+ importHashes: output?.importHashes ?? {},
222
+ clusterSize,
223
+ arch,
224
+ requireApprovalForDeploy: data.require_approval_for_deploy ?? false,
225
+ restrictBranchRegion: data.restrict_branch_region ?? false,
226
+ productionBranchWebConsole: data.production_branch_web_console ?? false,
227
+ deletionProtection: data.deletion_protected ?? false,
228
+ };
229
+ });
230
+
133
231
  export const PostgresDatabaseProvider = () =>
134
232
  Provider.succeed(PostgresDatabase, {
135
233
  stables: ["id", "organization", "region"],
@@ -191,66 +289,9 @@ export const PostgresDatabaseProvider = () =>
191
289
  return stables ? ({ action: "noop", stables } as const) : undefined;
192
290
  }),
193
291
 
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.succeed(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";
292
+ lookup: ({ name }) => readPostgresDatabase({ id: name, olds: { name } }),
217
293
 
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.succeed(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
- }),
294
+ read: readPostgresDatabase,
254
295
 
255
296
  reconcile: Effect.fn(function* ({ id, news, output, session }) {
256
297
  const { organization } = yield* yield* Credentials;
@@ -276,7 +317,7 @@ export const PostgresDatabaseProvider = () =>
276
317
  organization,
277
318
  database: observedName,
278
319
  })
279
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
320
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
280
321
 
281
322
  // Ensure — if missing, create.
282
323
  if (!observed) {
@@ -425,7 +466,7 @@ export const PostgresDatabaseProvider = () =>
425
466
  database: data.name,
426
467
  branch: defaultBranch,
427
468
  })
428
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
469
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
429
470
 
430
471
  const arch = toPostgresClusterArch(branch?.cluster_architecture);
431
472
  const clusterSize = branch?.cluster_name ?? "";
@@ -153,7 +153,7 @@ export const PostgresDefaultRoleProvider = () =>
153
153
  privateHost: role.private_access_host_url,
154
154
  privateConnectionServiceName: role.private_connection_service_name,
155
155
  })),
156
- Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
156
+ Effect.catchTag("NotFound", () => Effect.undefined),
157
157
  );
158
158
  }),
159
159
 
@@ -175,7 +175,7 @@ export const PostgresDefaultRoleProvider = () =>
175
175
  database: databaseName,
176
176
  branch: branchName,
177
177
  })
178
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
178
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
179
179
 
180
180
  // 2. Sync (no-op) — we own the role and it still exists in the
181
181
  // cloud. Default roles have nothing mutable in place: their