@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,547 @@
1
+ import * as planetscale from "@distilled.cloud/planetscale";
2
+ import { Credentials } from "@distilled.cloud/planetscale/Credentials";
3
+ import { deepEqual, isResolved } from "alchemy/Diff";
4
+ import { createPhysicalName } from "alchemy/PhysicalName";
5
+ import * as Provider from "alchemy/Provider";
6
+ import { Resource } from "alchemy/Resource";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Redacted from "effect/Redacted";
9
+ import * as Stream from "effect/Stream";
10
+ import type { Providers } from "../Providers.ts";
11
+ import { isKnownError, PlanetscaleConflict, waitForBranchReady } from "../Util.ts";
12
+ import type { PostgresBranch } from "./PostgresBranch.ts";
13
+ import type { PostgresDatabase } from "./PostgresDatabase.ts";
14
+ import type { PostgresOrigin } from "./PostgresOrigin.ts";
15
+
16
+ /**
17
+ * Built-in PostgreSQL roles that can be inherited from. Custom role names
18
+ * (referencing other Role resources) are also accepted.
19
+ */
20
+ export type InheritedRole =
21
+ | "pscale_managed"
22
+ | "pg_checkpoint"
23
+ | "pg_create_subscription"
24
+ | "pg_maintain"
25
+ | "pg_monitor"
26
+ | "pg_read_all_data"
27
+ | "pg_read_all_settings"
28
+ | "pg_read_all_stats"
29
+ | "pg_signal_backend"
30
+ | "pg_stat_scan_tables"
31
+ | "pg_use_reserved_connections"
32
+ | "pg_write_all_data"
33
+ | "postgres"
34
+ | (string & {});
35
+
36
+ type SDKInheritedRole =
37
+ | "pscale_managed"
38
+ | "pg_checkpoint"
39
+ | "pg_create_subscription"
40
+ | "pg_maintain"
41
+ | "pg_monitor"
42
+ | "pg_read_all_data"
43
+ | "pg_read_all_settings"
44
+ | "pg_read_all_stats"
45
+ | "pg_signal_backend"
46
+ | "pg_stat_scan_tables"
47
+ | "pg_use_reserved_connections"
48
+ | "pg_write_all_data"
49
+ | "postgres";
50
+
51
+ /**
52
+ * Properties for creating or updating a PlanetScale PostgreSQL role.
53
+ *
54
+ * Roles are only meant for PostgreSQL databases. For MySQL,
55
+ * use {@link MySQLPassword} instead.
56
+ */
57
+ export interface PostgresRoleProps {
58
+ /**
59
+ * Role name. If not provided, a physical name will be generated.
60
+ */
61
+ name?: string;
62
+ /**
63
+ * PostgreSQL database — string name or {@link PostgresDatabase} resource.
64
+ */
65
+ database: string | PostgresDatabase;
66
+
67
+ /**
68
+ * Branch — string name or {@link PostgresBranch} resource.
69
+ * @default "main"
70
+ */
71
+ branch?: string | PostgresBranch;
72
+
73
+ /**
74
+ * Time to live in seconds.
75
+ */
76
+ ttl?: number;
77
+
78
+ /**
79
+ * Roles to inherit from. Either a list of built-in role names, or
80
+ * another {@link PostgresRole} resource (whose `inheritedRoles` are reused).
81
+ */
82
+ inheritedRoles: InheritedRole[] | PostgresRole;
83
+
84
+ /**
85
+ * Give the role the REPLICATION attribute, for a logical-replication
86
+ * consumer (Electric, Debezium, a CDC pipeline). PlanetScale only grants
87
+ * it alongside `inheritedRoles: ["postgres"]`. Create-only: changing it
88
+ * replaces the role.
89
+ * @default false
90
+ */
91
+ withReplication?: boolean;
92
+
93
+ /**
94
+ * Successor role to reassign ownership to before dropping. Used during
95
+ * delete.
96
+ * @default "postgres"
97
+ */
98
+ successor?: string | PostgresRole;
99
+ }
100
+
101
+ /**
102
+ * Output attributes of a deployed PlanetScale role.
103
+ */
104
+ export interface PostgresRoleAttributes {
105
+ /** Unique identifier for the role (stable). */
106
+ id: string;
107
+ /** The Postgres role name (assigned by PlanetScale). */
108
+ name: string;
109
+ /** ISO 8601 timestamp at which the role expires. */
110
+ expiresAt: string | null;
111
+ /** Hostname for the database connection. */
112
+ host: string;
113
+ /** Username for database authentication. */
114
+ username: string;
115
+ /** Password for database authentication (Redacted). */
116
+ password: Redacted.Redacted<string>;
117
+ /** The database name. */
118
+ database: string;
119
+ /** The Postgres database name inside the branch. */
120
+ databaseName: string;
121
+ /** Parsed direct (port 5432) connection components ready to feed into Cloudflare Hyperdrive. */
122
+ origin: PostgresOrigin;
123
+ /** Parsed pooled (PSBouncer, port 6432) connection components, e.g. for a Hyperdrive `dev` origin. */
124
+ pooledOrigin: PostgresOrigin;
125
+ /**
126
+ * Private connection host or DNS zone for this branch, supplied by
127
+ * PlanetScale. GCP Private Service Connect requires an endpoint name before
128
+ * this DNS zone.
129
+ */
130
+ privateHost: string;
131
+ /** Service name for this branch supplied by PlanetScale to establish a private connection. */
132
+ privateConnectionServiceName: string;
133
+ /** Direct connection URL for the database (Redacted). */
134
+ connectionUrl: Redacted.Redacted<string>;
135
+ /** Pooled connection URL via PSBouncer (port 6432, Redacted). */
136
+ connectionUrlPooled: Redacted.Redacted<string>;
137
+ /** Inherited roles. */
138
+ inheritedRoles: InheritedRole[];
139
+ /** Whether the role carries the REPLICATION attribute. */
140
+ withReplication: boolean;
141
+ /** The successor role used during delete. */
142
+ successor: string;
143
+ /** Resolved organization slug. */
144
+ organization: string;
145
+ /** Resolved branch name. */
146
+ branch: string;
147
+ /** Time-to-live for the role in seconds. */
148
+ ttl: number | null;
149
+ }
150
+
151
+ /**
152
+ * A PlanetScale role for accessing a PostgreSQL database branch.
153
+ *
154
+ * For MySQL databases, use {@link MySQLPassword} instead.
155
+ *
156
+ * ### Creating a Role
157
+ * **Example:** Postgres admin role
158
+ * ```typescript
159
+ * const admin = yield* Planetscale.PostgresRole("Admin", {
160
+ * database: "my-db",
161
+ * inheritedRoles: ["postgres"],
162
+ * });
163
+ * ```
164
+ *
165
+ * **Example:** Read-only role
166
+ * ```typescript
167
+ * const reader = yield* Planetscale.PostgresRole("Reader", {
168
+ * database: "my-db",
169
+ * inheritedRoles: ["pg_read_all_data", "pg_read_all_settings"],
170
+ * });
171
+ * ```
172
+ *
173
+ * **Example:** Role with TTL
174
+ * ```typescript
175
+ * const tempReader = yield* Planetscale.PostgresRole("TempReader", {
176
+ * database: "my-db",
177
+ * inheritedRoles: ["pg_read_all_data"],
178
+ * ttl: 3600,
179
+ * });
180
+ * ```
181
+ */
182
+ export type PostgresRole = Resource<
183
+ "Planetscale.PostgresRole",
184
+ PostgresRoleProps,
185
+ PostgresRoleAttributes,
186
+ never,
187
+ Providers
188
+ >;
189
+
190
+ /** @resource */
191
+ export const PostgresRole = Resource<PostgresRole>("Planetscale.PostgresRole");
192
+
193
+ export const PostgresRoleProvider = () =>
194
+ Provider.succeed(PostgresRole, {
195
+ stables: ["id"],
196
+
197
+ diff: Effect.fn(function* ({ id, news, olds, output }) {
198
+ if (!isResolved(news)) return undefined;
199
+
200
+ // Successor is the only updatable property — everything else
201
+ // requires replacement.
202
+ if (news.ttl !== olds.ttl) {
203
+ return { action: "replace" } as const;
204
+ }
205
+ const newDb = resolveDatabaseName(news.database);
206
+ const oldDb = olds.database ? resolveDatabaseName(olds.database) : undefined;
207
+ if (oldDb && newDb !== oldDb) {
208
+ return { action: "replace" } as const;
209
+ }
210
+ const newBranch = resolveBranchName(news.branch);
211
+ const oldBranch = resolveBranchName(olds.branch);
212
+ if (newBranch !== oldBranch) {
213
+ return { action: "replace" } as const;
214
+ }
215
+ const newRoles = [...resolveInheritedRoles(news.inheritedRoles)].sort();
216
+ const oldRoles = [...(output?.inheritedRoles ?? [])].sort();
217
+ if (!deepEqual(newRoles, oldRoles)) {
218
+ return { action: "replace" } as const;
219
+ }
220
+ if ((news.withReplication ?? false) !== (output?.withReplication ?? false)) {
221
+ return { action: "replace" } as const;
222
+ }
223
+ const oldName = output?.name ?? (yield* resolveName(id, olds?.name));
224
+ const newName = yield* resolveName(id, news.name);
225
+
226
+ if (newName !== oldName) {
227
+ return { action: "update" } as const;
228
+ }
229
+
230
+ return undefined;
231
+ }),
232
+
233
+ read: Effect.fn(function* ({ output }) {
234
+ if (!output?.id) return undefined;
235
+
236
+ return yield* planetscale
237
+ .getRole({
238
+ branch: output.branch,
239
+ database: output.database,
240
+ organization: output.organization,
241
+ id: output.id,
242
+ })
243
+ .pipe(
244
+ Effect.map((token) =>
245
+ buildAttributes(token, output.password, {
246
+ inheritedRoles: token.inherited_roles as InheritedRole[],
247
+ successor: output.successor,
248
+ organization: output.organization,
249
+ database: output.database,
250
+ branch: token.branch.name,
251
+ }),
252
+ ),
253
+ Effect.catchTags({
254
+ Forbidden: () => Effect.undefined,
255
+ NotFound: () => Effect.undefined,
256
+ }),
257
+ );
258
+ }),
259
+
260
+ reconcile: Effect.fn(function* ({ id, news, output }) {
261
+ const { organization: envOrg } = yield* yield* Credentials;
262
+ const organization = resolveDatabaseOrg(news.database) ?? envOrg;
263
+ const databaseName = resolveDatabaseName(news.database);
264
+ const branchName = resolveBranchName(news.branch);
265
+ const inheritedRoles = resolveInheritedRoles(news.inheritedRoles);
266
+ const successor = resolveSuccessorName(news.successor);
267
+ const desiredName = yield* resolveName(id, news.name);
268
+
269
+ // 1. Observe — fetch live state if we know the role id. Roles
270
+ // can only be looked up by id; without a cached id there is
271
+ // no way to find the live role, so we fall through to ensure.
272
+ const observed = output?.id
273
+ ? yield* planetscale
274
+ .getRole({
275
+ id: output.id,
276
+ branch: branchName,
277
+ organization,
278
+ database: databaseName,
279
+ })
280
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined))
281
+ : undefined;
282
+
283
+ // 2. Ensure — create if missing. Single branch covers both
284
+ // greenfield (no output) and out-of-band-deletion (output
285
+ // cached but cloud lost it). The latter is structurally a
286
+ // rotation: the new role has fresh id/host/username/plaintext
287
+ // that propagate downstream through the dependency graph.
288
+ // There is no path to recover the original plaintext, so
289
+ // recreating is the only way to converge.
290
+ //
291
+ // Planetscale returns the plaintext password value exactly
292
+ // once on create; on an observe-hit we re-use the cached
293
+ // plaintext from output (which is why adoption requires
294
+ // pre-existing state — see `read`).
295
+ let live: planetscale.GetRoleOutput;
296
+ let plaintext: Redacted.Redacted<string>;
297
+ if (observed) {
298
+ live = observed;
299
+ plaintext = output!.password;
300
+ } else {
301
+ const branchInfo = yield* planetscale.getBranch({
302
+ organization,
303
+ database: databaseName,
304
+ branch: branchName,
305
+ });
306
+ if (branchInfo.kind !== "postgresql") {
307
+ return yield* new PlanetscaleConflict({
308
+ message: `Cannot create a Role on MySQL database "${databaseName}". Roles are only supported on PostgreSQL. Use Password for MySQL.`,
309
+ });
310
+ }
311
+ if (!branchInfo.ready) {
312
+ yield* waitForBranchReady(organization, databaseName, branchName);
313
+ }
314
+ const created = yield* planetscale.createRole({
315
+ name: desiredName,
316
+ branch: branchName,
317
+ organization,
318
+ database: databaseName,
319
+ ttl: news.ttl,
320
+ inherited_roles: inheritedRoles as SDKInheritedRole[],
321
+ with_replication: news.withReplication,
322
+ });
323
+ if (!created.password) {
324
+ return yield* Effect.die(
325
+ `Planetscale did not return a password for Role "${desiredName}".`,
326
+ );
327
+ }
328
+ plaintext = created.password;
329
+ live = created;
330
+ }
331
+
332
+ // 3. Sync — only `name` is mutable in place; diff observed
333
+ // against desired and skip the API call when nothing
334
+ // changed. Everything else triggers a replace via diff().
335
+ if (live.name !== desiredName) {
336
+ live = yield* planetscale.updateRole({
337
+ id: live.id,
338
+ branch: branchName,
339
+ organization,
340
+ database: databaseName,
341
+ name: desiredName,
342
+ });
343
+ }
344
+
345
+ // 4. Return — fresh attrs sourced from observed cloud state,
346
+ // with cached or freshly-issued plaintext.
347
+ return buildAttributes(live, plaintext, {
348
+ inheritedRoles: live.inherited_roles as InheritedRole[],
349
+ successor,
350
+ organization,
351
+ database: databaseName,
352
+ branch: branchName,
353
+ });
354
+ }),
355
+
356
+ delete: Effect.fn(function* ({ output }) {
357
+ yield* planetscale
358
+ .deleteRole({
359
+ organization: output.organization,
360
+ database: output.database,
361
+ branch: output.branch,
362
+ id: output.id,
363
+ successor: output.successor,
364
+ })
365
+ .pipe(
366
+ Effect.catchTag("NotFound", () => Effect.void),
367
+ // 422 (UnprocessableEntity): role is still referenced. Log a
368
+ // warning rather than failing — the role will be cleaned up
369
+ // when the database/branch is deleted.
370
+ Effect.catchIf(
371
+ isKnownError("UnprocessableEntity", "Role is still referenced and cannot be dropped."),
372
+ () => Effect.void,
373
+ ),
374
+ );
375
+ }),
376
+
377
+ list: Effect.fn(function* () {
378
+ const { organization } = yield* yield* Credentials;
379
+
380
+ const databases = yield* planetscale.listDatabases.pages({ organization }).pipe(
381
+ Stream.runCollect,
382
+ Effect.map((chunk) =>
383
+ Array.from(chunk).flatMap((page) => page.data.filter((db) => db.kind === "postgresql")),
384
+ ),
385
+ );
386
+
387
+ const rows = yield* Effect.forEach(
388
+ databases,
389
+ (db) =>
390
+ planetscale.listBranches.pages({ organization, database: db.name }).pipe(
391
+ Stream.runCollect,
392
+ Effect.map((branchPages) =>
393
+ Array.from(branchPages).flatMap((page) =>
394
+ page.data.filter((branch) => branch.kind === "postgresql"),
395
+ ),
396
+ ),
397
+ Effect.catchTag("NotFound", () =>
398
+ Effect.succeed([{ name: db.default_branch ?? "main" }]),
399
+ ),
400
+ Effect.flatMap((branches) =>
401
+ Effect.forEach(
402
+ branches,
403
+ (branch) =>
404
+ planetscale.listRoles
405
+ .pages({
406
+ organization,
407
+ database: db.name,
408
+ branch: branch.name,
409
+ })
410
+ .pipe(
411
+ Stream.runCollect,
412
+ Effect.map((rolePages): PostgresRoleAttributes[] =>
413
+ Array.from(rolePages).flatMap((page) =>
414
+ page.data
415
+ .filter((role) => !role.default)
416
+ .map((role) =>
417
+ buildAttributes(role, Redacted.make(""), {
418
+ inheritedRoles: role.inherited_roles as InheritedRole[],
419
+ successor: "postgres",
420
+ organization,
421
+ database: db.name,
422
+ branch: branch.name,
423
+ }),
424
+ ),
425
+ ),
426
+ ),
427
+ Effect.catchTags({
428
+ NotFound: () => Effect.succeed([] as PostgresRoleAttributes[]),
429
+ Forbidden: () => Effect.succeed([] as PostgresRoleAttributes[]),
430
+ }),
431
+ ),
432
+ { concurrency: 10 },
433
+ ).pipe(Effect.map((perBranch) => perBranch.flat())),
434
+ ),
435
+ ),
436
+ { concurrency: 10 },
437
+ );
438
+
439
+ return rows.flat();
440
+ }),
441
+ });
442
+
443
+ // Structural shapes for runtime-resolved Resource references — see notes
444
+ // in Password.ts.
445
+ type DatabaseRef = string | { name: string; organization?: string };
446
+ type BranchRef = string | { name: string };
447
+ type RoleRef = string | { name: string; inheritedRoles: InheritedRole[] };
448
+
449
+ const resolveDatabaseName = (database: string | PostgresDatabase): string => {
450
+ const ref = database as unknown as DatabaseRef;
451
+ return typeof ref === "string" ? ref : ref.name;
452
+ };
453
+
454
+ const resolveDatabaseOrg = (database: string | PostgresDatabase): string | undefined => {
455
+ const ref = database as unknown as DatabaseRef;
456
+ return typeof ref === "string" ? undefined : ref.organization;
457
+ };
458
+
459
+ const resolveBranchName = (branch: string | PostgresBranch | undefined): string => {
460
+ const ref = branch as unknown as BranchRef | undefined;
461
+ return !ref ? "main" : typeof ref === "string" ? ref : ref.name;
462
+ };
463
+
464
+ const resolveSuccessorName = (successor: string | PostgresRole | undefined): string => {
465
+ const ref = successor as unknown as RoleRef | undefined;
466
+ return !ref ? "postgres" : typeof ref === "string" ? ref : ref.name;
467
+ };
468
+
469
+ const resolveInheritedRoles = (inheritedRoles: InheritedRole[] | PostgresRole): InheritedRole[] => {
470
+ if (Array.isArray(inheritedRoles)) {
471
+ return inheritedRoles;
472
+ }
473
+ // At runtime, a Role passed in is its resolved attributes (with
474
+ // `inheritedRoles` as a plain array); statically `Role.inheritedRoles`
475
+ // is `Output<InheritedRole[]>`.
476
+ return (inheritedRoles as unknown as RoleRef as { inheritedRoles: InheritedRole[] })
477
+ .inheritedRoles;
478
+ };
479
+
480
+ const resolveName = (id: string, name: string | undefined) =>
481
+ Effect.gen(function* () {
482
+ return name ?? (yield* createPhysicalName({ id, lowercase: true, maxLength: 63 }));
483
+ });
484
+
485
+ const buildAttributes = (
486
+ role: {
487
+ id: string;
488
+ name: string;
489
+ expires_at: string | null;
490
+ with_replication: boolean;
491
+ access_host_url: string;
492
+ private_access_host_url: string;
493
+ private_connection_service_name: string;
494
+ username: string;
495
+ database_name: string;
496
+ ttl: number | null;
497
+ },
498
+ password: Redacted.Redacted<string>,
499
+ context: {
500
+ inheritedRoles: InheritedRole[];
501
+ successor: string;
502
+ organization: string;
503
+ database: string;
504
+ branch: string;
505
+ },
506
+ ): PostgresRoleAttributes => {
507
+ const passwordValue = Redacted.value(password);
508
+ const connectionUrl = `postgresql://${role.username}:${passwordValue}@${role.access_host_url}:5432/${role.database_name}?sslmode=verify-full`;
509
+ const connectionUrlPooled = `postgresql://${role.username}:${passwordValue}@${role.access_host_url}:6432/${role.database_name}?sslmode=verify-full`;
510
+
511
+ return {
512
+ id: role.id,
513
+ name: role.name,
514
+ expiresAt: role.expires_at,
515
+ host: role.access_host_url,
516
+ username: role.username,
517
+ password,
518
+ connectionUrl: Redacted.make(connectionUrl),
519
+ connectionUrlPooled: Redacted.make(connectionUrlPooled),
520
+ privateHost: role.private_access_host_url,
521
+ privateConnectionServiceName: role.private_connection_service_name,
522
+ inheritedRoles: context.inheritedRoles,
523
+ withReplication: role.with_replication,
524
+ successor: context.successor,
525
+ organization: context.organization,
526
+ database: context.database,
527
+ databaseName: role.database_name,
528
+ origin: {
529
+ scheme: "postgres",
530
+ host: role.access_host_url,
531
+ port: 5432,
532
+ database: role.database_name,
533
+ user: role.username,
534
+ password,
535
+ },
536
+ pooledOrigin: {
537
+ scheme: "postgres",
538
+ host: role.access_host_url,
539
+ port: 6432,
540
+ database: role.database_name,
541
+ user: role.username,
542
+ password,
543
+ },
544
+ branch: context.branch,
545
+ ttl: role.ttl,
546
+ };
547
+ };
@@ -0,0 +1,6 @@
1
+ export * from "./PostgresClusterSize.ts";
2
+ export * from "./PostgresBranch.ts";
3
+ export * from "./PostgresDatabase.ts";
4
+ export * from "./PostgresDefaultRole.ts";
5
+ export * from "./PostgresOrigin.ts";
6
+ export * from "./PostgresRole.ts";
@@ -0,0 +1,67 @@
1
+ import { CredentialsStoreLive } from "alchemy/Auth/Credentials";
2
+ import { ProfileStoreLive } from "alchemy/Auth/Profile";
3
+ import * as Provider from "alchemy/Provider";
4
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
5
+ import * as Layer from "effect/Layer";
6
+ import { PlanetscaleAuth } from "./AuthProvider.ts";
7
+ import * as Credentials from "./Credentials.ts";
8
+ import { MySQLBranch, MySQLBranchProvider } from "./MySQL/MySQLBranch.ts";
9
+ import { MySQLDatabase, MySQLDatabaseProvider } from "./MySQL/MySQLDatabase.ts";
10
+ import { MySQLPassword, MySQLPasswordProvider } from "./MySQL/MySQLPassword.ts";
11
+ import { PostgresBranch, PostgresBranchProvider } from "./Postgres/PostgresBranch.ts";
12
+ import { PostgresDatabase, PostgresDatabaseProvider } from "./Postgres/PostgresDatabase.ts";
13
+ import {
14
+ PostgresDefaultRole,
15
+ PostgresDefaultRoleProvider,
16
+ } from "./Postgres/PostgresDefaultRole.ts";
17
+ import { PostgresRole, PostgresRoleProvider } from "./Postgres/PostgresRole.ts";
18
+
19
+ /**
20
+ * Service tag bundling all PlanetScale providers + auth + credentials. Use
21
+ * `Planetscale.providers()` to materialize the full layer.
22
+ */
23
+ export class Providers extends Provider.ProviderCollection<Providers>()("Planetscale") {}
24
+
25
+ export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
26
+
27
+ /**
28
+ * Build the complete PlanetScale providers Layer. Provide this on a Stack
29
+ * (or via `Effect.provide` at test time) to enable all PlanetScale
30
+ * resources, the AuthProvider registration, and credential resolution.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * Effect.provide(Planetscale.providers())
35
+ * ```
36
+ */
37
+ export const providers = () =>
38
+ Layer.effect(
39
+ Providers,
40
+ Provider.collection([
41
+ MySQLDatabase,
42
+ PostgresDatabase,
43
+ MySQLBranch,
44
+ PostgresBranch,
45
+ MySQLPassword,
46
+ PostgresRole,
47
+ PostgresDefaultRole,
48
+ ]),
49
+ ).pipe(
50
+ Layer.provide(
51
+ Layer.mergeAll(
52
+ MySQLDatabaseProvider(),
53
+ PostgresDatabaseProvider(),
54
+ MySQLBranchProvider(),
55
+ PostgresBranchProvider(),
56
+ MySQLPasswordProvider(),
57
+ PostgresRoleProvider(),
58
+ PostgresDefaultRoleProvider(),
59
+ ),
60
+ ),
61
+ Layer.provideMerge(Credentials.fromAuthProvider()),
62
+ Layer.provideMerge(FetchHttpClient.layer),
63
+ Layer.provideMerge(PlanetscaleAuth),
64
+ Layer.provideMerge(ProfileStoreLive),
65
+ Layer.provideMerge(CredentialsStoreLive),
66
+ Layer.orDie,
67
+ );