@alchemy.run/planetscale 0.0.0 → 2.0.0-beta.84
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.
- package/LICENSE +201 -0
- package/README.md +83 -4
- package/lib/AuthProvider.d.ts +64 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +384 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Branch.d.ts +128 -0
- package/lib/Branch.d.ts.map +1 -0
- package/lib/Branch.js +420 -0
- package/lib/Branch.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/Credentials.d.ts +33 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +65 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Database.d.ts +138 -0
- package/lib/Database.d.ts.map +1 -0
- package/lib/Database.js +1 -0
- package/lib/Database.js.map +1 -0
- package/lib/MySQL/MySQLBranch.d.ts +87 -0
- package/lib/MySQL/MySQLBranch.d.ts.map +1 -0
- package/lib/MySQL/MySQLBranch.js +15 -0
- package/lib/MySQL/MySQLBranch.js.map +1 -0
- package/lib/MySQL/MySQLClusterSize.d.ts +32 -0
- package/lib/MySQL/MySQLClusterSize.d.ts.map +1 -0
- package/lib/MySQL/MySQLClusterSize.js +111 -0
- package/lib/MySQL/MySQLClusterSize.js.map +1 -0
- package/lib/MySQL/MySQLDatabase.d.ts +141 -0
- package/lib/MySQL/MySQLDatabase.d.ts.map +1 -0
- package/lib/MySQL/MySQLDatabase.js +294 -0
- package/lib/MySQL/MySQLDatabase.js.map +1 -0
- package/lib/MySQL/MySQLMigrations.d.ts +24 -0
- package/lib/MySQL/MySQLMigrations.d.ts.map +1 -0
- package/lib/MySQL/MySQLMigrations.js +106 -0
- package/lib/MySQL/MySQLMigrations.js.map +1 -0
- package/lib/MySQL/MySQLOrigin.d.ts +15 -0
- package/lib/MySQL/MySQLOrigin.d.ts.map +1 -0
- package/lib/MySQL/MySQLOrigin.js +2 -0
- package/lib/MySQL/MySQLOrigin.js.map +1 -0
- package/lib/MySQL/MySQLPassword.d.ts +116 -0
- package/lib/MySQL/MySQLPassword.d.ts.map +1 -0
- package/lib/MySQL/MySQLPassword.js +212 -0
- package/lib/MySQL/MySQLPassword.js.map +1 -0
- package/lib/MySQL/index.d.ts +6 -0
- package/lib/MySQL/index.d.ts.map +1 -0
- package/lib/MySQL/index.js +6 -0
- package/lib/MySQL/index.js.map +1 -0
- package/lib/OAuthClient.d.ts +32 -0
- package/lib/OAuthClient.d.ts.map +1 -0
- package/lib/OAuthClient.js +52 -0
- package/lib/OAuthClient.js.map +1 -0
- package/lib/Postgres/PostgresBranch.d.ts +75 -0
- package/lib/Postgres/PostgresBranch.d.ts.map +1 -0
- package/lib/Postgres/PostgresBranch.js +15 -0
- package/lib/Postgres/PostgresBranch.js.map +1 -0
- package/lib/Postgres/PostgresClusterSize.d.ts +77 -0
- package/lib/Postgres/PostgresClusterSize.d.ts.map +1 -0
- package/lib/Postgres/PostgresClusterSize.js +89 -0
- package/lib/Postgres/PostgresClusterSize.js.map +1 -0
- package/lib/Postgres/PostgresDatabase.d.ts +95 -0
- package/lib/Postgres/PostgresDatabase.d.ts.map +1 -0
- package/lib/Postgres/PostgresDatabase.js +294 -0
- package/lib/Postgres/PostgresDatabase.js.map +1 -0
- package/lib/Postgres/PostgresDefaultRole.d.ts +94 -0
- package/lib/Postgres/PostgresDefaultRole.d.ts.map +1 -0
- package/lib/Postgres/PostgresDefaultRole.js +187 -0
- package/lib/Postgres/PostgresDefaultRole.js.map +1 -0
- package/lib/Postgres/PostgresMigrations.d.ts +24 -0
- package/lib/Postgres/PostgresMigrations.d.ts.map +1 -0
- package/lib/Postgres/PostgresMigrations.js +116 -0
- package/lib/Postgres/PostgresMigrations.js.map +1 -0
- package/lib/Postgres/PostgresOrigin.d.ts +15 -0
- package/lib/Postgres/PostgresOrigin.d.ts.map +1 -0
- package/lib/Postgres/PostgresOrigin.js +2 -0
- package/lib/Postgres/PostgresOrigin.js.map +1 -0
- package/lib/Postgres/PostgresRole.d.ts +142 -0
- package/lib/Postgres/PostgresRole.d.ts.map +1 -0
- package/lib/Postgres/PostgresRole.js +268 -0
- package/lib/Postgres/PostgresRole.js.map +1 -0
- package/lib/Postgres/index.d.ts +7 -0
- package/lib/Postgres/index.d.ts.map +1 -0
- package/lib/Postgres/index.js +7 -0
- package/lib/Postgres/index.js.map +1 -0
- package/lib/Providers.d.ts +24 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +40 -0
- package/lib/Providers.js.map +1 -0
- package/lib/Util.d.ts +93 -0
- package/lib/Util.d.ts.map +1 -0
- package/lib/Util.js +135 -0
- package/lib/Util.js.map +1 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +9 -0
- package/lib/index.js.map +1 -0
- package/package.json +79 -3
- package/src/AuthProvider.ts +571 -0
- package/src/Branch.ts +657 -0
- package/src/Contributions.ts +9 -0
- package/src/Credentials.ts +116 -0
- package/src/Database.ts +152 -0
- package/src/MySQL/MySQLBranch.ts +109 -0
- package/src/MySQL/MySQLClusterSize.ts +185 -0
- package/src/MySQL/MySQLDatabase.ts +521 -0
- package/src/MySQL/MySQLMigrations.ts +182 -0
- package/src/MySQL/MySQLOrigin.ts +15 -0
- package/src/MySQL/MySQLPassword.ts +428 -0
- package/src/MySQL/index.ts +5 -0
- package/src/OAuthClient.ts +59 -0
- package/src/Postgres/PostgresBranch.ts +97 -0
- package/src/Postgres/PostgresClusterSize.ts +262 -0
- package/src/Postgres/PostgresDatabase.ts +469 -0
- package/src/Postgres/PostgresDefaultRole.ts +340 -0
- package/src/Postgres/PostgresMigrations.ts +193 -0
- package/src/Postgres/PostgresOrigin.ts +15 -0
- package/src/Postgres/PostgresRole.ts +547 -0
- package/src/Postgres/index.ts +6 -0
- package/src/Providers.ts +67 -0
- package/src/Util.ts +205 -0
- 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.succeed(undefined),
|
|
255
|
+
NotFound: () => Effect.succeed(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.succeed(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 | planetscale.CreateRoleOutput;
|
|
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
|
+
};
|
package/src/Providers.ts
ADDED
|
@@ -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
|
+
);
|