@alchemy.run/planetscale 2.0.0-beta.84 → 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 (52) 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.js +2 -2
  6. package/lib/Branch.js.map +1 -1
  7. package/lib/Contributions.d.ts +1 -1
  8. package/lib/Credentials.d.ts +1 -1
  9. package/lib/Credentials.d.ts.map +1 -1
  10. package/lib/Credentials.js +6 -12
  11. package/lib/Credentials.js.map +1 -1
  12. package/lib/MySQL/MySQLClusterSize.d.ts +1 -1
  13. package/lib/MySQL/MySQLClusterSize.d.ts.map +1 -1
  14. package/lib/MySQL/MySQLClusterSize.js +1 -1
  15. package/lib/MySQL/MySQLClusterSize.js.map +1 -1
  16. package/lib/MySQL/MySQLDatabase.js +2 -2
  17. package/lib/MySQL/MySQLDatabase.js.map +1 -1
  18. package/lib/MySQL/MySQLMigrations.d.ts +7 -11
  19. package/lib/MySQL/MySQLMigrations.d.ts.map +1 -1
  20. package/lib/MySQL/MySQLMigrations.js +24 -58
  21. package/lib/MySQL/MySQLMigrations.js.map +1 -1
  22. package/lib/MySQL/MySQLPassword.js +2 -2
  23. package/lib/MySQL/MySQLPassword.js.map +1 -1
  24. package/lib/Postgres/PostgresDatabase.js +4 -4
  25. package/lib/Postgres/PostgresDatabase.js.map +1 -1
  26. package/lib/Postgres/PostgresDefaultRole.js +2 -2
  27. package/lib/Postgres/PostgresDefaultRole.js.map +1 -1
  28. package/lib/Postgres/PostgresMigrations.d.ts +3 -11
  29. package/lib/Postgres/PostgresMigrations.d.ts.map +1 -1
  30. package/lib/Postgres/PostgresMigrations.js +15 -66
  31. package/lib/Postgres/PostgresMigrations.js.map +1 -1
  32. package/lib/Postgres/PostgresRole.js +3 -3
  33. package/lib/Postgres/PostgresRole.js.map +1 -1
  34. package/lib/Providers.d.ts +1 -1
  35. package/lib/Providers.d.ts.map +1 -1
  36. package/lib/Util.d.ts +3 -3
  37. package/lib/Util.d.ts.map +1 -1
  38. package/lib/Util.js +1 -1
  39. package/lib/Util.js.map +1 -1
  40. package/package.json +8 -8
  41. package/src/AuthProvider.ts +91 -78
  42. package/src/Branch.ts +2 -2
  43. package/src/Credentials.ts +8 -32
  44. package/src/MySQL/MySQLClusterSize.ts +1 -1
  45. package/src/MySQL/MySQLDatabase.ts +2 -2
  46. package/src/MySQL/MySQLMigrations.ts +31 -80
  47. package/src/MySQL/MySQLPassword.ts +2 -2
  48. package/src/Postgres/PostgresDatabase.ts +4 -4
  49. package/src/Postgres/PostgresDefaultRole.ts +2 -2
  50. package/src/Postgres/PostgresMigrations.ts +20 -91
  51. package/src/Postgres/PostgresRole.ts +4 -4
  52. package/src/Util.ts +1 -1
@@ -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.
@@ -200,7 +200,7 @@ export const PostgresDatabaseProvider = () =>
200
200
  organization,
201
201
  database: databaseName,
202
202
  })
203
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
203
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
204
204
 
205
205
  if (!data) return undefined;
206
206
 
@@ -223,7 +223,7 @@ export const PostgresDatabaseProvider = () =>
223
223
  database: data.name,
224
224
  branch: defaultBranch,
225
225
  })
226
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
226
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
227
227
 
228
228
  const arch = toPostgresClusterArch(branch!.cluster_architecture);
229
229
  const clusterSize = branch!.cluster_name;
@@ -276,7 +276,7 @@ export const PostgresDatabaseProvider = () =>
276
276
  organization,
277
277
  database: observedName,
278
278
  })
279
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
279
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
280
280
 
281
281
  // Ensure — if missing, create.
282
282
  if (!observed) {
@@ -425,7 +425,7 @@ export const PostgresDatabaseProvider = () =>
425
425
  database: data.name,
426
426
  branch: defaultBranch,
427
427
  })
428
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
428
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
429
429
 
430
430
  const arch = toPostgresClusterArch(branch?.cluster_architecture);
431
431
  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
@@ -1,36 +1,19 @@
1
1
  import {
2
- makePgMigrationExecutor,
3
2
  runMigrations,
3
+ withPgExecutor,
4
4
  type NormalizedMigrationsInput,
5
+ type SqlExecutor,
5
6
  type StampedMigrationsState,
6
7
  } from "@alchemy.run/sql/Migrations";
7
- import { readSqlFile } from "@alchemy.run/sql/SqlFile";
8
+ import { stripSslQueryParams } from "@alchemy.run/sql/PostgresDriver";
9
+ import { applySqlImports } from "@alchemy.run/sql/SqlFile";
8
10
  import * as ps from "@distilled.cloud/planetscale";
9
- import * as Data from "effect/Data";
10
11
  import * as Effect from "effect/Effect";
11
12
  import * as Redacted from "effect/Redacted";
12
13
  import * as Schedule from "effect/Schedule";
13
- import type { Client } from "pg";
14
-
15
- // `pg` is an optional peer dependency — loaded lazily so importing the
16
- // Planetscale provider never requires the driver unless migrations run.
17
- const importPg = () =>
18
- import("pg").catch((cause) => {
19
- throw new Error(
20
- "Failed to load the 'pg' driver. Install the optional peer dependency 'pg' to run Planetscale Postgres migrations.",
21
- { cause },
22
- );
23
- });
24
14
 
25
15
  const MIGRATION_ROLE_TTL_SECONDS = 600;
26
16
 
27
- export class PostgresMigrationError extends Data.TaggedError(
28
- "Planetscale::PostgresMigrationError",
29
- )<{
30
- message: string;
31
- cause?: unknown;
32
- }> {}
33
-
34
17
  export interface PostgresMigrationTarget {
35
18
  organization: string;
36
19
  database: string;
@@ -49,67 +32,36 @@ export const runPostgresMigrations = (
49
32
  runMigrations({
50
33
  input,
51
34
  stamped,
52
- withExecutor: (apply) =>
53
- withPostgresClient(target, (client) => apply(makePgMigrationExecutor(client))),
35
+ withExecutor: (apply) => withPostgresExecutor(target, apply),
54
36
  });
55
37
 
38
+ /** Apply `importFiles`, each once and again only when it changes. */
56
39
  export const runPostgresImports = (
57
40
  target: PostgresMigrationTarget,
58
41
  importFiles: ReadonlyArray<string>,
59
42
  rootDir: string,
60
43
  previous: Record<string, string>,
61
44
  ) =>
62
- Effect.gen(function* () {
63
- const hashes: Record<string, string> = { ...previous };
64
- for (const filePath of importFiles) {
65
- const file = yield* readSqlFile(rootDir, filePath);
66
- if (previous[filePath] === file.hash) {
67
- hashes[filePath] = file.hash;
68
- continue;
69
- }
70
- yield* runPostgresSql(target, file.sql);
71
- hashes[filePath] = file.hash;
72
- }
73
- const tracked = new Set(importFiles);
74
- for (const key of Object.keys(hashes)) {
75
- if (!tracked.has(key)) delete hashes[key];
76
- }
77
- return hashes;
45
+ applySqlImports({
46
+ importFiles,
47
+ rootDir,
48
+ previous,
49
+ apply: (file) =>
50
+ withPostgresExecutor(target, (executor) => executor.query(file.sql)).pipe(Effect.asVoid),
78
51
  });
79
52
 
80
- const runPostgresSql = (target: PostgresMigrationTarget, sql: string) =>
81
- withPostgresClient(target, (client) => pgExec(client, sql));
82
-
83
- const withPostgresClient = <A, E, R>(
53
+ /** A verified-TLS `pg` session as a short-lived migration role. */
54
+ const withPostgresExecutor = <A, E, R>(
84
55
  target: PostgresMigrationTarget,
85
- use: (client: Client) => Effect.Effect<A, E, R>,
56
+ use: (executor: SqlExecutor) => Effect.Effect<A, E, R>,
86
57
  ) =>
87
58
  withTemporaryPostgresRole(target, (role) =>
88
- Effect.acquireUseRelease(
89
- Effect.gen(function* () {
90
- const { Client } = yield* Effect.tryPromise({
91
- try: importPg,
92
- catch: toMigrationError,
93
- });
94
- const client = yield* Effect.sync(
95
- () =>
96
- new Client({
97
- connectionString: role.connectionUrl.pipe(Redacted.value, stripPgSslQueryParams),
98
- ssl: { rejectUnauthorized: true },
99
- }),
100
- );
101
- yield* Effect.tryPromise({
102
- try: () => client.connect(),
103
- catch: toMigrationError,
104
- });
105
- return client;
106
- }),
59
+ withPgExecutor(
60
+ {
61
+ connectionString: role.connectionUrl.pipe(Redacted.value, stripSslQueryParams),
62
+ ssl: { rejectUnauthorized: true },
63
+ },
107
64
  use,
108
- (client) =>
109
- Effect.tryPromise({
110
- try: () => client.end(),
111
- catch: toMigrationError,
112
- }).pipe(Effect.ignore),
113
65
  ),
114
66
  );
115
67
 
@@ -168,26 +120,3 @@ const withTemporaryPostgresRole = <A, E, R>(
168
120
  ),
169
121
  ),
170
122
  );
171
-
172
- const pgExec = (client: Client, sql: string, values?: ReadonlyArray<unknown>) =>
173
- Effect.tryPromise({
174
- try: () => client.query(sql, values as Array<unknown>).then(() => undefined),
175
- catch: toMigrationError,
176
- });
177
-
178
- const toMigrationError = (cause: unknown) =>
179
- new PostgresMigrationError({
180
- message: cause instanceof Error ? cause.message : String(cause),
181
- cause,
182
- });
183
-
184
- const stripPgSslQueryParams = (uri: string): string => {
185
- try {
186
- const url = new URL(uri);
187
- url.searchParams.delete("sslmode");
188
- url.searchParams.delete("channel_binding");
189
- return url.toString();
190
- } catch {
191
- return uri;
192
- }
193
- };
@@ -251,8 +251,8 @@ export const PostgresRoleProvider = () =>
251
251
  }),
252
252
  ),
253
253
  Effect.catchTags({
254
- Forbidden: () => Effect.succeed(undefined),
255
- NotFound: () => Effect.succeed(undefined),
254
+ Forbidden: () => Effect.undefined,
255
+ NotFound: () => Effect.undefined,
256
256
  }),
257
257
  );
258
258
  }),
@@ -277,7 +277,7 @@ export const PostgresRoleProvider = () =>
277
277
  organization,
278
278
  database: databaseName,
279
279
  })
280
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)))
280
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined))
281
281
  : undefined;
282
282
 
283
283
  // 2. Ensure — create if missing. Single branch covers both
@@ -292,7 +292,7 @@ export const PostgresRoleProvider = () =>
292
292
  // once on create; on an observe-hit we re-use the cached
293
293
  // plaintext from output (which is why adoption requires
294
294
  // pre-existing state — see `read`).
295
- let live: planetscale.GetRoleOutput | planetscale.CreateRoleOutput;
295
+ let live: planetscale.GetRoleOutput;
296
296
  let plaintext: Redacted.Redacted<string>;
297
297
  if (observed) {
298
298
  live = observed;
package/src/Util.ts CHANGED
@@ -48,7 +48,7 @@ export const deleteUnprotectedDatabase = Effect.fn(function* (
48
48
  ) {
49
49
  const live = yield* ps
50
50
  .getDatabase({ organization, database })
51
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
51
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
52
52
  if (!live) return;
53
53
  if (live.deletion_protected) {
54
54
  return yield* new PlanetscaleDeletionProtected({