@alchemy.run/planetscale 2.0.0-beta.85 → 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.
- package/lib/AuthProvider.d.ts.map +1 -1
- package/lib/AuthProvider.js.map +1 -1
- package/lib/Branch.d.ts +6 -1
- package/lib/Branch.d.ts.map +1 -1
- package/lib/Branch.js +21 -0
- package/lib/Branch.js.map +1 -1
- package/lib/Contributions.d.ts.map +1 -1
- package/lib/Contributions.js.map +1 -1
- package/lib/Credentials.d.ts.map +1 -1
- package/lib/Credentials.js.map +1 -1
- package/lib/Database.d.ts +7 -0
- package/lib/Database.d.ts.map +1 -1
- package/lib/Database.js.map +1 -1
- package/lib/MySQL/MySQLBranch.d.ts +26 -2
- package/lib/MySQL/MySQLBranch.d.ts.map +1 -1
- package/lib/MySQL/MySQLBranch.js +15 -1
- package/lib/MySQL/MySQLBranch.js.map +1 -1
- package/lib/MySQL/MySQLClusterSize.d.ts +1 -1
- package/lib/MySQL/MySQLClusterSize.d.ts.map +1 -1
- package/lib/MySQL/MySQLClusterSize.js.map +1 -1
- package/lib/MySQL/MySQLDatabase.d.ts +28 -2
- package/lib/MySQL/MySQLDatabase.d.ts.map +1 -1
- package/lib/MySQL/MySQLDatabase.js +71 -50
- package/lib/MySQL/MySQLDatabase.js.map +1 -1
- package/lib/MySQL/MySQLMigrations.d.ts.map +1 -1
- package/lib/MySQL/MySQLMigrations.js.map +1 -1
- package/lib/MySQL/MySQLOrigin.d.ts.map +1 -1
- package/lib/MySQL/MySQLOrigin.js.map +1 -1
- package/lib/MySQL/MySQLPassword.d.ts.map +1 -1
- package/lib/MySQL/MySQLPassword.js.map +1 -1
- package/lib/MySQL/index.d.ts.map +1 -1
- package/lib/MySQL/index.js.map +1 -1
- package/lib/OAuthClient.d.ts.map +1 -1
- package/lib/OAuthClient.js.map +1 -1
- package/lib/Postgres/PostgresBranch.d.ts +26 -2
- package/lib/Postgres/PostgresBranch.d.ts.map +1 -1
- package/lib/Postgres/PostgresBranch.js +15 -1
- package/lib/Postgres/PostgresBranch.js.map +1 -1
- package/lib/Postgres/PostgresClusterSize.d.ts.map +1 -1
- package/lib/Postgres/PostgresClusterSize.js.map +1 -1
- package/lib/Postgres/PostgresDatabase.d.ts +28 -2
- package/lib/Postgres/PostgresDatabase.d.ts.map +1 -1
- package/lib/Postgres/PostgresDatabase.js +75 -54
- package/lib/Postgres/PostgresDatabase.js.map +1 -1
- package/lib/Postgres/PostgresDefaultRole.d.ts.map +1 -1
- package/lib/Postgres/PostgresDefaultRole.js.map +1 -1
- package/lib/Postgres/PostgresMigrations.d.ts.map +1 -1
- package/lib/Postgres/PostgresMigrations.js.map +1 -1
- package/lib/Postgres/PostgresOrigin.d.ts.map +1 -1
- package/lib/Postgres/PostgresOrigin.js.map +1 -1
- package/lib/Postgres/PostgresRole.d.ts.map +1 -1
- package/lib/Postgres/PostgresRole.js.map +1 -1
- package/lib/Postgres/index.d.ts.map +1 -1
- package/lib/Postgres/index.js.map +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js +3 -1
- package/lib/Providers.js.map +1 -1
- package/lib/ServiceToken.d.ts +142 -0
- package/lib/ServiceToken.d.ts.map +1 -0
- package/lib/ServiceToken.js +268 -0
- package/lib/ServiceToken.js.map +1 -0
- package/lib/Util.d.ts +3 -3
- package/lib/Util.d.ts.map +1 -1
- package/lib/Util.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/package.json +9 -9
- package/src/Branch.ts +34 -1
- package/src/Database.ts +7 -0
- package/src/MySQL/MySQLBranch.ts +28 -2
- package/src/MySQL/MySQLDatabase.ts +102 -62
- package/src/Postgres/PostgresBranch.ts +28 -2
- package/src/Postgres/PostgresDatabase.ts +102 -61
- package/src/Providers.ts +3 -0
- package/src/ServiceToken.ts +431 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import * as planetscale from "@distilled.cloud/planetscale";
|
|
2
|
+
import { Credentials } from "@distilled.cloud/planetscale/Credentials";
|
|
3
|
+
import { 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 Array from "effect/Array";
|
|
8
|
+
import * as Data from "effect/Data";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Exit from "effect/Exit";
|
|
11
|
+
import * as Redacted from "effect/Redacted";
|
|
12
|
+
import * as Stream from "effect/Stream";
|
|
13
|
+
import type { MySQLDatabase } from "./MySQL/MySQLDatabase.ts";
|
|
14
|
+
import type { PostgresDatabase } from "./Postgres/PostgresDatabase.ts";
|
|
15
|
+
import type { Providers } from "./Providers.ts";
|
|
16
|
+
|
|
17
|
+
/** Database permissions supported by PlanetScale service tokens. */
|
|
18
|
+
export type ServiceTokenDatabaseAccess =
|
|
19
|
+
| "read_database"
|
|
20
|
+
| "write_database"
|
|
21
|
+
| "delete_database"
|
|
22
|
+
| "create_branch"
|
|
23
|
+
| "read_branch"
|
|
24
|
+
| "delete_branch"
|
|
25
|
+
| "delete_production_branch"
|
|
26
|
+
| "promote_branches"
|
|
27
|
+
| "demote_branches"
|
|
28
|
+
| "connect_branch"
|
|
29
|
+
| "connect_production_branch"
|
|
30
|
+
| "connect_production_read_only_branch"
|
|
31
|
+
| "create_branch_password"
|
|
32
|
+
| "create_production_branch_password"
|
|
33
|
+
| "create_production_read_only_branch_password"
|
|
34
|
+
| "delete_branch_password"
|
|
35
|
+
| "delete_production_branch_password"
|
|
36
|
+
| "delete_production_read_only_branch_password"
|
|
37
|
+
| "create_deploy_request"
|
|
38
|
+
| "read_deploy_request"
|
|
39
|
+
| "approve_deploy_request"
|
|
40
|
+
| "review_deploy_request"
|
|
41
|
+
| "create_comment"
|
|
42
|
+
| "read_comment"
|
|
43
|
+
| "read_backups"
|
|
44
|
+
| "write_backups"
|
|
45
|
+
| "delete_backups"
|
|
46
|
+
| "delete_production_branch_backups"
|
|
47
|
+
| "restore_backup"
|
|
48
|
+
| "restore_production_branch_backup"
|
|
49
|
+
| "write_branch_vschema"
|
|
50
|
+
| "write_production_branch_vschema"
|
|
51
|
+
| "write_branch_topology"
|
|
52
|
+
| "write_production_branch_topology"
|
|
53
|
+
| "read_workflow"
|
|
54
|
+
| "write_workflow"
|
|
55
|
+
| "delete_workflow";
|
|
56
|
+
|
|
57
|
+
/** Properties for a service token scoped to one PlanetScale database. */
|
|
58
|
+
export interface ServiceTokenProps {
|
|
59
|
+
/** Name prefix. An instance suffix allows old and replacement tokens to coexist. */
|
|
60
|
+
name?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Organization slug. Defaults to the database resource's organization, then
|
|
63
|
+
* to the configured PlanetScale organization.
|
|
64
|
+
*/
|
|
65
|
+
organization?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The database the token is scoped to: its name, a {@link MySQLDatabase} or
|
|
68
|
+
* {@link PostgresDatabase} resource, or a `.ref(...)` to one deployed
|
|
69
|
+
* elsewhere. Changing it replaces the token.
|
|
70
|
+
*/
|
|
71
|
+
database: string | MySQLDatabase | PostgresDatabase;
|
|
72
|
+
/**
|
|
73
|
+
* Exact database permissions. Permissions not declared here are revoked.
|
|
74
|
+
* PlanetScale also accepts the Vitess-only accesses (`*_vschema`,
|
|
75
|
+
* `*_topology`, `*_workflow`) on a Postgres database.
|
|
76
|
+
*/
|
|
77
|
+
accesses: readonly ServiceTokenDatabaseAccess[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Output attributes of a deployed PlanetScale service token. */
|
|
81
|
+
export interface ServiceTokenAttributes {
|
|
82
|
+
/** Service token ID. */
|
|
83
|
+
id: string;
|
|
84
|
+
/** Physical token name, including the instance suffix. */
|
|
85
|
+
name: string;
|
|
86
|
+
/** Organization that owns the token. */
|
|
87
|
+
organization: string;
|
|
88
|
+
/** Database to which the token is scoped. */
|
|
89
|
+
database: string;
|
|
90
|
+
/** Granted database permissions. */
|
|
91
|
+
accesses: readonly ServiceTokenDatabaseAccess[];
|
|
92
|
+
/** Creation-only secret, retained in Alchemy state for subsequent deploys. */
|
|
93
|
+
token: Redacted.Redacted<string>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type ServiceToken = Resource<
|
|
97
|
+
"Planetscale.ServiceToken",
|
|
98
|
+
ServiceTokenProps,
|
|
99
|
+
ServiceTokenAttributes,
|
|
100
|
+
never,
|
|
101
|
+
Providers
|
|
102
|
+
>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A PlanetScale API service token with exactly the declared permissions on one database.
|
|
106
|
+
*
|
|
107
|
+
* The deployment identity must be allowed to create service tokens and manage
|
|
108
|
+
* their accesses. These API credentials are separate from database passwords.
|
|
109
|
+
*
|
|
110
|
+
* PlanetScale returns the secret only at creation. Alchemy preserves it in
|
|
111
|
+
* state; use a state backend that protects secrets. Existing tokens cannot be
|
|
112
|
+
* adopted because their secret cannot be recovered.
|
|
113
|
+
*
|
|
114
|
+
* Service tokens carry no labels or tags, so a token's physical name, which
|
|
115
|
+
* holds this resource's instance ID, is its only ownership marker. If a
|
|
116
|
+
* create is interrupted before Alchemy saves the token, the next destroy
|
|
117
|
+
* finds it by that name and revokes it, and the next deploy revokes it and
|
|
118
|
+
* mints a replacement, since its secret was never saved. `list` returns
|
|
119
|
+
* nothing: a name alone can't prove which stack made a token, and listing
|
|
120
|
+
* every token would offer an organization's other tokens to account-wide
|
|
121
|
+
* cleanup.
|
|
122
|
+
*
|
|
123
|
+
* Changing the name prefix, organization, or database creates a replacement.
|
|
124
|
+
* Resources that consume the token's outputs are updated before Alchemy deletes
|
|
125
|
+
* the previous token. Changes to `accesses` are reconciled in place.
|
|
126
|
+
*
|
|
127
|
+
* ### Creating a Database Token
|
|
128
|
+
* **Example:** Token for a database declared in the stack
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const database = yield* Planetscale.MySQLDatabase("Database", {
|
|
131
|
+
* clusterSize: "PS_10",
|
|
132
|
+
* });
|
|
133
|
+
*
|
|
134
|
+
* const token = yield* Planetscale.ServiceToken("Sync", {
|
|
135
|
+
* database,
|
|
136
|
+
* accesses: ["read_database", "write_database"],
|
|
137
|
+
* });
|
|
138
|
+
* ```
|
|
139
|
+
*
|
|
140
|
+
* **Example:** Token for a Postgres database
|
|
141
|
+
* ```typescript
|
|
142
|
+
* const database = yield* Planetscale.PostgresDatabase("Database", {
|
|
143
|
+
* clusterSize: "PS_10",
|
|
144
|
+
* });
|
|
145
|
+
*
|
|
146
|
+
* const token = yield* Planetscale.ServiceToken("Backups", {
|
|
147
|
+
* database,
|
|
148
|
+
* accesses: ["read_backups", "write_backups"],
|
|
149
|
+
* });
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* **Example:** Token for an existing database by name
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const token = yield* Planetscale.ServiceToken("Sync", {
|
|
155
|
+
* database: "app-db",
|
|
156
|
+
* accesses: ["read_database"],
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*
|
|
160
|
+
* ### Referencing a Database from Another Stack
|
|
161
|
+
* **Example:** Token for a database another stack owns
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const database = yield* Planetscale.MySQLDatabase.ref("Database", {
|
|
164
|
+
* stack: "shared-data",
|
|
165
|
+
* });
|
|
166
|
+
*
|
|
167
|
+
* const token = yield* Planetscale.ServiceToken("Sync", {
|
|
168
|
+
* database,
|
|
169
|
+
* accesses: ["read_deploy_request", "create_deploy_request"],
|
|
170
|
+
* });
|
|
171
|
+
* ```
|
|
172
|
+
*
|
|
173
|
+
* ### Rotating a Token
|
|
174
|
+
* **Example:** Rename the token to mint a replacement
|
|
175
|
+
* ```typescript
|
|
176
|
+
* // Changing `name` creates a new token; consumers are updated
|
|
177
|
+
* // before the previous token is revoked.
|
|
178
|
+
* const token = yield* Planetscale.ServiceToken("Sync", {
|
|
179
|
+
* name: "sync-2026-11",
|
|
180
|
+
* database,
|
|
181
|
+
* accesses: ["read_database"],
|
|
182
|
+
* });
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @resource
|
|
186
|
+
*/
|
|
187
|
+
export const ServiceToken = Resource<ServiceToken>("Planetscale.ServiceToken");
|
|
188
|
+
|
|
189
|
+
/** A service token cannot satisfy the declared ownership or access contract. */
|
|
190
|
+
export class ServiceTokenError extends Data.TaggedError("ServiceTokenError")<{
|
|
191
|
+
readonly message: string;
|
|
192
|
+
}> {}
|
|
193
|
+
|
|
194
|
+
export const ServiceTokenProvider = () =>
|
|
195
|
+
Provider.succeed(ServiceToken, {
|
|
196
|
+
list: () => Effect.succeed([]),
|
|
197
|
+
read: Effect.fn(function* ({ id, instanceId, olds, output }) {
|
|
198
|
+
if (output) {
|
|
199
|
+
const live = yield* getToken(output.organization, output.id);
|
|
200
|
+
return live ? { ...output, name: live.name ?? output.name } : undefined;
|
|
201
|
+
}
|
|
202
|
+
// An interrupted create leaves a token whose attributes were never
|
|
203
|
+
// saved. Its physical name ends with this instance's suffix, whatever
|
|
204
|
+
// prefix it was made with, so it is this resource's own: report it,
|
|
205
|
+
// with an empty placeholder for the secret that was lost, so destroy
|
|
206
|
+
// can revoke it.
|
|
207
|
+
if (!olds) return undefined;
|
|
208
|
+
const organization = yield* organizationOf(olds);
|
|
209
|
+
const [live] = yield* instanceTokens(organization, id, instanceId);
|
|
210
|
+
return live
|
|
211
|
+
? {
|
|
212
|
+
id: live.id,
|
|
213
|
+
name: live.name,
|
|
214
|
+
organization,
|
|
215
|
+
database: databaseName(olds.database),
|
|
216
|
+
accesses: ordered(olds.accesses),
|
|
217
|
+
token: Redacted.make(""),
|
|
218
|
+
}
|
|
219
|
+
: undefined;
|
|
220
|
+
}),
|
|
221
|
+
diff: Effect.fn(function* ({ id, instanceId, olds, news, output }) {
|
|
222
|
+
if (!output) return undefined;
|
|
223
|
+
const prefix = "name" in news ? news.name : undefined;
|
|
224
|
+
const org = "organization" in news ? news.organization : undefined;
|
|
225
|
+
// A computed identity can resolve to a different database after apply.
|
|
226
|
+
// Replace first so a failed dependent update keeps the old token usable.
|
|
227
|
+
if (
|
|
228
|
+
!("database" in news) ||
|
|
229
|
+
!isResolved(news.database) ||
|
|
230
|
+
!isResolved(prefix) ||
|
|
231
|
+
!isResolved(org)
|
|
232
|
+
) {
|
|
233
|
+
return { action: "replace" } as const;
|
|
234
|
+
}
|
|
235
|
+
const organization =
|
|
236
|
+
org ?? databaseOrganization(news.database) ?? (yield* yield* Credentials).organization;
|
|
237
|
+
const database = databaseName(news.database);
|
|
238
|
+
const name = yield* tokenName(id, instanceId, prefix);
|
|
239
|
+
if (
|
|
240
|
+
output.organization !== organization ||
|
|
241
|
+
output.database !== database ||
|
|
242
|
+
output.name !== name ||
|
|
243
|
+
(olds && olds.name !== prefix)
|
|
244
|
+
) {
|
|
245
|
+
return { action: "replace" } as const;
|
|
246
|
+
}
|
|
247
|
+
const live = yield* getToken(output.organization, output.id);
|
|
248
|
+
if (!live || live.name !== name) return { action: "replace" } as const;
|
|
249
|
+
if (!("accesses" in news) || !isResolved(news.accesses)) {
|
|
250
|
+
return { action: "update" } as const;
|
|
251
|
+
}
|
|
252
|
+
return exactAccess(
|
|
253
|
+
(yield* planetscale.listServiceTokenAccesses({ organization, id: output.id })).data,
|
|
254
|
+
database,
|
|
255
|
+
news.accesses,
|
|
256
|
+
)
|
|
257
|
+
? undefined
|
|
258
|
+
: ({ action: "update" } as const);
|
|
259
|
+
}),
|
|
260
|
+
reconcile: Effect.fn(function* ({ id, instanceId, news, output }) {
|
|
261
|
+
const organization = yield* organizationOf(news);
|
|
262
|
+
const database = databaseName(news.database);
|
|
263
|
+
const name = yield* tokenName(id, instanceId, news.name);
|
|
264
|
+
let observed = output ? yield* getToken(output.organization, output.id) : undefined;
|
|
265
|
+
|
|
266
|
+
// A live token whose secret was never saved can't reach its consumers,
|
|
267
|
+
// who never received it either: revoke it and mint a replacement.
|
|
268
|
+
if (observed && output && Redacted.value(output.token) === "") {
|
|
269
|
+
yield* deleteToken(output.organization, output.id);
|
|
270
|
+
observed = undefined;
|
|
271
|
+
} else if (!observed) {
|
|
272
|
+
for (const orphan of yield* instanceTokens(organization, id, instanceId)) {
|
|
273
|
+
yield* deleteToken(organization, orphan.id);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return yield* Effect.acquireUseRelease(
|
|
278
|
+
observed
|
|
279
|
+
? Effect.succeed(observed)
|
|
280
|
+
: planetscale
|
|
281
|
+
.createServiceToken({ organization, name })
|
|
282
|
+
// A retried create can lose the only response containing the secret.
|
|
283
|
+
.pipe(planetscale.Retry.none),
|
|
284
|
+
Effect.fn(function* (live) {
|
|
285
|
+
const token = observed ? output?.token : live.token;
|
|
286
|
+
if (!token) {
|
|
287
|
+
return yield* new ServiceTokenError({
|
|
288
|
+
message: `The creation-only secret for service token ${live.id} is unavailable.`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
yield* ensureAccess(organization, live.id, database, news.accesses);
|
|
292
|
+
return {
|
|
293
|
+
id: live.id,
|
|
294
|
+
name: live.name ?? name,
|
|
295
|
+
organization,
|
|
296
|
+
database,
|
|
297
|
+
accesses: ordered(news.accesses),
|
|
298
|
+
token,
|
|
299
|
+
};
|
|
300
|
+
}),
|
|
301
|
+
(live, exit) =>
|
|
302
|
+
!observed && Exit.isFailure(exit) ? deleteToken(organization, live.id) : Effect.void,
|
|
303
|
+
);
|
|
304
|
+
}),
|
|
305
|
+
delete: Effect.fn(function* ({ output }) {
|
|
306
|
+
yield* deleteToken(output.organization, output.id);
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Resource references in props reach the provider as the database's
|
|
311
|
+
// attributes, so a database prop is either a name or `{ name, organization }`.
|
|
312
|
+
type DatabaseInput = string | { readonly name: string; readonly organization?: string };
|
|
313
|
+
|
|
314
|
+
const databaseName = (database: ServiceTokenProps["database"]) => {
|
|
315
|
+
const input = database as unknown as DatabaseInput;
|
|
316
|
+
return typeof input === "string" ? input : input.name;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const databaseOrganization = (database: ServiceTokenProps["database"]) => {
|
|
320
|
+
const input = database as unknown as DatabaseInput;
|
|
321
|
+
return typeof input === "string" ? undefined : input.organization;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const organizationOf = Effect.fnUntraced(function* (props: ServiceTokenProps) {
|
|
325
|
+
return (
|
|
326
|
+
props.organization ??
|
|
327
|
+
databaseOrganization(props.database) ??
|
|
328
|
+
(yield* yield* Credentials).organization
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const tokenName = (id: string, instanceId: string, prefix: string | undefined) =>
|
|
333
|
+
createPhysicalName({ id, instanceId, prefix: prefix === undefined ? undefined : `${prefix}-` });
|
|
334
|
+
|
|
335
|
+
const ordered = <A extends string>(accesses: readonly A[]) => Array.dedupe(accesses).toSorted();
|
|
336
|
+
|
|
337
|
+
const getToken = (organization: string, id: string) =>
|
|
338
|
+
planetscale
|
|
339
|
+
.getServiceToken({ organization, id })
|
|
340
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.undefined));
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The organization's tokens made by this resource instance: their names end
|
|
344
|
+
* with its 80-bit instance suffix, whatever prefix each was made with (a
|
|
345
|
+
* truncated name keeps the suffix last, after a hash, with no delimiter).
|
|
346
|
+
*/
|
|
347
|
+
const instanceTokens = Effect.fnUntraced(function* (
|
|
348
|
+
organization: string,
|
|
349
|
+
id: string,
|
|
350
|
+
instanceId: string,
|
|
351
|
+
) {
|
|
352
|
+
const suffix = yield* createPhysicalName({ id, instanceId, prefix: "" });
|
|
353
|
+
return yield* planetscale.listServiceTokens.items({ organization }).pipe(
|
|
354
|
+
Stream.runCollect,
|
|
355
|
+
Effect.map((tokens) =>
|
|
356
|
+
Array.fromIterable(tokens).flatMap(({ id, name }) =>
|
|
357
|
+
name?.endsWith(suffix) ? [{ id, name }] : [],
|
|
358
|
+
),
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const deleteToken = (organization: string, id: string) =>
|
|
364
|
+
planetscale
|
|
365
|
+
.deleteServiceToken({ organization, id })
|
|
366
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.void));
|
|
367
|
+
|
|
368
|
+
const exactAccess = (
|
|
369
|
+
current: planetscale.ListServiceTokenAccessesOutput["data"],
|
|
370
|
+
database: string,
|
|
371
|
+
desired: readonly string[],
|
|
372
|
+
) =>
|
|
373
|
+
current.length === ordered(desired).length &&
|
|
374
|
+
desired.every((access) => current.some((entry) => entry.access === access)) &&
|
|
375
|
+
current.every(
|
|
376
|
+
(entry) =>
|
|
377
|
+
entry.resource.type === "Database" &&
|
|
378
|
+
entry.resource.name === database &&
|
|
379
|
+
desired.includes(entry.access),
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const ensureAccess = Effect.fn(function* (
|
|
383
|
+
organization: string,
|
|
384
|
+
id: string,
|
|
385
|
+
database: string,
|
|
386
|
+
accesses: readonly ServiceTokenDatabaseAccess[],
|
|
387
|
+
) {
|
|
388
|
+
const current = (yield* planetscale.listServiceTokenAccesses({ organization, id })).data;
|
|
389
|
+
for (const entry of current) {
|
|
390
|
+
if (
|
|
391
|
+
entry.resource.type !== "Database" ||
|
|
392
|
+
entry.resource.name !== database ||
|
|
393
|
+
!accesses.some((access) => access === entry.access)
|
|
394
|
+
) {
|
|
395
|
+
yield* planetscale.removeServiceTokenAccess({
|
|
396
|
+
organization,
|
|
397
|
+
id,
|
|
398
|
+
database: entry.resource.type === "Database" ? entry.resource.name : "",
|
|
399
|
+
access: [entry.access],
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const missing = ordered(accesses).filter(
|
|
404
|
+
(access) =>
|
|
405
|
+
!current.some(
|
|
406
|
+
(entry) =>
|
|
407
|
+
entry.resource.type === "Database" &&
|
|
408
|
+
entry.resource.name === database &&
|
|
409
|
+
entry.access === access,
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
if (missing.length > 0) {
|
|
413
|
+
yield* planetscale.addServiceTokenAccess({
|
|
414
|
+
organization,
|
|
415
|
+
id,
|
|
416
|
+
database: database,
|
|
417
|
+
access: missing,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
if (
|
|
421
|
+
!exactAccess(
|
|
422
|
+
(yield* planetscale.listServiceTokenAccesses({ organization, id })).data,
|
|
423
|
+
database,
|
|
424
|
+
accesses,
|
|
425
|
+
)
|
|
426
|
+
) {
|
|
427
|
+
return yield* new ServiceTokenError({
|
|
428
|
+
message: `Service token ${id} did not converge to its declared database permissions.`,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
});
|