alchemy 0.63.0 → 0.64.0
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/bin/alchemy.js +97 -87
- package/bin/commands/init.ts +1 -1
- package/bin/services/github-workflow.ts +2 -2
- package/lib/alchemy.d.ts +4 -0
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +10 -4
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.d.ts +3 -0
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +11 -1
- package/lib/apply.js.map +1 -1
- package/lib/cloudflare/bindings.d.ts +3 -1
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bucket.d.ts +2 -1
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/bundle/cloudflare-internal.d.ts +6 -0
- package/lib/cloudflare/bundle/cloudflare-internal.d.ts.map +1 -0
- package/lib/cloudflare/bundle/cloudflare-internal.js +39 -0
- package/lib/cloudflare/bundle/cloudflare-internal.js.map +1 -0
- package/lib/cloudflare/bundle/nodejs-builtin-modules.d.ts +2 -0
- package/lib/cloudflare/bundle/nodejs-builtin-modules.d.ts.map +1 -0
- package/lib/cloudflare/bundle/nodejs-builtin-modules.js +9 -0
- package/lib/cloudflare/bundle/nodejs-builtin-modules.js.map +1 -0
- package/lib/cloudflare/bundle/plugin-als-external.d.ts +6 -0
- package/lib/cloudflare/bundle/plugin-als-external.d.ts.map +1 -0
- package/lib/cloudflare/bundle/plugin-als-external.js +12 -0
- package/lib/cloudflare/bundle/plugin-als-external.js.map +1 -0
- package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts +4 -1
- package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts.map +1 -1
- package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js +23 -23
- package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js.map +1 -1
- package/lib/cloudflare/bundle/plugin-node-compat.d.ts +11 -0
- package/lib/cloudflare/bundle/plugin-node-compat.d.ts.map +1 -0
- package/lib/cloudflare/bundle/plugin-node-compat.js +93 -0
- package/lib/cloudflare/bundle/plugin-node-compat.js.map +1 -0
- package/lib/cloudflare/cloudflare-env-proxy.js +1 -1
- package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/d1-local-migrations.js.map +1 -1
- package/lib/cloudflare/durable-object-namespace.d.ts +1 -0
- package/lib/cloudflare/durable-object-namespace.d.ts.map +1 -1
- package/lib/cloudflare/hyperdrive.js +1 -1
- package/lib/cloudflare/hyperdrive.js.map +1 -1
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +80 -82
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-bundle.d.ts +6 -0
- package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/worker-bundle.js +34 -10
- package/lib/cloudflare/worker-bundle.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +2 -1
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +24 -6
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +2 -4
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +16 -14
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/context.d.ts +3 -1
- package/lib/context.d.ts.map +1 -1
- package/lib/context.js +2 -2
- package/lib/context.js.map +1 -1
- package/lib/destroy.d.ts +8 -0
- package/lib/destroy.d.ts.map +1 -1
- package/lib/destroy.js +20 -4
- package/lib/destroy.js.map +1 -1
- package/lib/neon/project.d.ts.map +1 -1
- package/lib/neon/project.js +4 -5
- package/lib/neon/project.js.map +1 -1
- package/lib/planetscale/api/client.gen.d.ts +1750 -0
- package/lib/planetscale/api/client.gen.d.ts.map +1 -0
- package/lib/planetscale/api/client.gen.js +2401 -0
- package/lib/planetscale/api/client.gen.js.map +1 -0
- package/lib/planetscale/api/index.d.ts +3 -0
- package/lib/planetscale/api/index.d.ts.map +1 -0
- package/lib/planetscale/api/index.js +4 -0
- package/lib/planetscale/api/index.js.map +1 -0
- package/lib/planetscale/api/types.gen.d.ts +12156 -0
- package/lib/planetscale/api/types.gen.d.ts.map +1 -0
- package/lib/planetscale/api/types.gen.js +2 -0
- package/lib/planetscale/api/types.gen.js.map +1 -0
- package/lib/planetscale/branch.d.ts +5 -8
- package/lib/planetscale/branch.d.ts.map +1 -1
- package/lib/planetscale/branch.js +111 -97
- package/lib/planetscale/branch.js.map +1 -1
- package/lib/planetscale/database.d.ts +2 -6
- package/lib/planetscale/database.d.ts.map +1 -1
- package/lib/planetscale/database.js +157 -118
- package/lib/planetscale/database.js.map +1 -1
- package/lib/planetscale/index.d.ts +1 -1
- package/lib/planetscale/index.d.ts.map +1 -1
- package/lib/planetscale/index.js +1 -1
- package/lib/planetscale/index.js.map +1 -1
- package/lib/planetscale/password.d.ts +4 -7
- package/lib/planetscale/password.d.ts.map +1 -1
- package/lib/planetscale/password.js +71 -56
- package/lib/planetscale/password.js.map +1 -1
- package/lib/planetscale/utils.d.ts +8 -6
- package/lib/planetscale/utils.d.ts.map +1 -1
- package/lib/planetscale/utils.js +85 -70
- package/lib/planetscale/utils.js.map +1 -1
- package/lib/scope.d.ts +5 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +11 -3
- package/lib/scope.js.map +1 -1
- package/lib/upstash/redis.js +1 -1
- package/lib/upstash/redis.js.map +1 -1
- package/lib/util/dedent.d.ts +3 -14
- package/lib/util/dedent.d.ts.map +1 -1
- package/lib/util/dedent.js +28 -52
- package/lib/util/dedent.js.map +1 -1
- package/lib/util/idempotent-spawn.d.ts.map +1 -1
- package/lib/util/idempotent-spawn.js +21 -2
- package/lib/util/idempotent-spawn.js.map +1 -1
- package/lib/util/xml.js +1 -1
- package/lib/util/xml.js.map +1 -1
- package/package.json +4 -3
- package/src/alchemy.ts +14 -6
- package/src/apply.ts +13 -1
- package/src/cloudflare/bindings.ts +3 -1
- package/src/cloudflare/bucket.ts +3 -1
- package/src/cloudflare/bundle/cloudflare-internal.ts +42 -0
- package/src/cloudflare/bundle/nodejs-builtin-modules.ts +14 -0
- package/src/cloudflare/bundle/plugin-als-external.ts +13 -0
- package/src/cloudflare/bundle/plugin-hybrid-node-compat.ts +29 -28
- package/src/cloudflare/bundle/plugin-node-compat.ts +114 -0
- package/src/cloudflare/cloudflare-env-proxy.ts +1 -1
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/d1-local-migrations.ts +2 -2
- package/src/cloudflare/durable-object-namespace.ts +1 -1
- package/src/cloudflare/hyperdrive.ts +1 -1
- package/src/cloudflare/website.ts +88 -89
- package/src/cloudflare/worker-bundle.ts +46 -9
- package/src/cloudflare/worker-metadata.ts +3 -1
- package/src/cloudflare/worker.ts +35 -7
- package/src/cloudflare/wrangler.json.ts +108 -110
- package/src/context.ts +5 -3
- package/src/destroy.ts +26 -8
- package/src/neon/project.ts +4 -5
- package/src/planetscale/api/client.gen.ts +2882 -0
- package/src/planetscale/api/index.ts +4 -0
- package/src/planetscale/api/types.gen.ts +13041 -0
- package/src/planetscale/branch.ts +152 -169
- package/src/planetscale/database.ts +195 -204
- package/src/planetscale/index.ts +1 -1
- package/src/planetscale/password.ts +86 -85
- package/src/planetscale/utils.ts +141 -122
- package/src/scope.ts +12 -3
- package/src/upstash/redis.ts +1 -1
- package/src/util/dedent.ts +33 -61
- package/src/util/idempotent-spawn.ts +23 -2
- package/src/util/xml.ts +1 -1
- package/templates/astro/alchemy.run.ts +1 -3
- package/templates/hono/alchemy.run.ts +0 -1
- package/templates/nuxt/alchemy.run.ts +1 -3
- package/templates/react-router/alchemy.run.ts +1 -3
- package/templates/rwsdk/alchemy.run.ts +0 -2
- package/templates/sveltekit/alchemy.run.ts +1 -3
- package/templates/tanstack-start/alchemy.run.ts +1 -3
- package/templates/typescript/alchemy.run.ts +0 -1
- package/templates/vite/alchemy.run.ts +0 -1
- package/workers/cloudflare-state-store.js +58 -58
- package/lib/cloudflare/bundle/externals.d.ts +0 -3
- package/lib/cloudflare/bundle/externals.d.ts.map +0 -1
- package/lib/cloudflare/bundle/externals.js +0 -35
- package/lib/cloudflare/bundle/externals.js.map +0 -1
- package/lib/cloudflare/bundle/plugin-compat-warning.d.ts +0 -10
- package/lib/cloudflare/bundle/plugin-compat-warning.d.ts.map +0 -1
- package/lib/cloudflare/bundle/plugin-compat-warning.js +0 -67
- package/lib/cloudflare/bundle/plugin-compat-warning.js.map +0 -1
- package/lib/planetscale/api.d.ts +0 -53
- package/lib/planetscale/api.d.ts.map +0 -1
- package/lib/planetscale/api.js +0 -84
- package/lib/planetscale/api.js.map +0 -1
- package/src/cloudflare/bundle/externals.ts +0 -36
- package/src/cloudflare/bundle/plugin-compat-warning.ts +0 -94
- package/src/planetscale/api.ts +0 -115
|
@@ -1,17 +1,17 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import { alchemy } from "../alchemy.ts";
|
|
2
3
|
import type { Context } from "../context.ts";
|
|
3
4
|
import { Resource } from "../resource.ts";
|
|
4
5
|
import type { Secret } from "../secret.ts";
|
|
5
|
-
import { logger } from "../util/logger.ts";
|
|
6
6
|
import { lowercaseId } from "../util/nanoid.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { PlanetScaleClient, type PlanetScaleProps } from "./api/client.gen.ts";
|
|
8
8
|
import type { Branch } from "./branch.ts";
|
|
9
9
|
import type { Database } from "./database.ts";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Properties for creating or updating a PlanetScale Branch
|
|
13
13
|
*/
|
|
14
|
-
export interface PasswordProps {
|
|
14
|
+
export interface PasswordProps extends PlanetScaleProps {
|
|
15
15
|
/**
|
|
16
16
|
* The name of the password
|
|
17
17
|
*
|
|
@@ -21,7 +21,7 @@ export interface PasswordProps {
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* The organization ID where the password will be created
|
|
24
|
-
* Required when using string database name, optional when using Database resource
|
|
24
|
+
* Required when using string database name, optional when using Database or Branch resource
|
|
25
25
|
*/
|
|
26
26
|
organizationId?: string;
|
|
27
27
|
|
|
@@ -34,15 +34,10 @@ export interface PasswordProps {
|
|
|
34
34
|
/**
|
|
35
35
|
* The branch where the password will be created
|
|
36
36
|
* Can be either a branch name (string) or Branch resource
|
|
37
|
-
*
|
|
37
|
+
* @default "main"
|
|
38
38
|
*/
|
|
39
39
|
branch?: string | Branch;
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
* PlanetScale API token (overrides environment variable)
|
|
43
|
-
*/
|
|
44
|
-
apiKey?: Secret;
|
|
45
|
-
|
|
46
41
|
/**
|
|
47
42
|
* The password
|
|
48
43
|
*/
|
|
@@ -265,109 +260,115 @@ export const Password = Resource(
|
|
|
265
260
|
id: string,
|
|
266
261
|
props: PasswordProps,
|
|
267
262
|
): Promise<Password> {
|
|
268
|
-
const apiKey =
|
|
269
|
-
props.apiKey?.unencrypted || process.env.PLANETSCALE_API_TOKEN;
|
|
270
|
-
if (!apiKey) {
|
|
271
|
-
throw new Error("PLANETSCALE_API_TOKEN environment variable is required");
|
|
272
|
-
}
|
|
273
263
|
const nameSlug = this.isReplacement
|
|
274
264
|
? lowercaseId()
|
|
275
265
|
: (this.output?.nameSlug ?? lowercaseId());
|
|
276
266
|
const name = `${(props.name ?? this.output?.name ?? this.scope.createPhysicalName(id)).toLowerCase()}-${nameSlug}`;
|
|
277
267
|
|
|
278
|
-
const api = new
|
|
279
|
-
const
|
|
280
|
-
props.branch == null
|
|
281
|
-
? "main"
|
|
282
|
-
: typeof props.branch === "string"
|
|
283
|
-
? props.branch
|
|
284
|
-
: props.branch.name;
|
|
285
|
-
const databaseName =
|
|
268
|
+
const api = new PlanetScaleClient(props);
|
|
269
|
+
const database =
|
|
286
270
|
typeof props.database === "string" ? props.database : props.database.name;
|
|
271
|
+
const branch =
|
|
272
|
+
typeof props.branch === "string"
|
|
273
|
+
? props.branch
|
|
274
|
+
: (props.branch?.name ?? "main");
|
|
275
|
+
const organization =
|
|
276
|
+
props.organizationId ??
|
|
277
|
+
((typeof props.branch !== "string" && props.branch?.organizationId) ||
|
|
278
|
+
(typeof props.database !== "string" && props.database.organizationId));
|
|
279
|
+
|
|
280
|
+
if (!organization) {
|
|
281
|
+
throw new Error("Organization ID is required");
|
|
282
|
+
}
|
|
287
283
|
|
|
288
284
|
if (this.phase === "delete") {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
285
|
+
if (this.output?.id) {
|
|
286
|
+
const res = await api.organizations.databases.branches.passwords.delete(
|
|
287
|
+
{
|
|
288
|
+
path: {
|
|
289
|
+
organization,
|
|
290
|
+
database,
|
|
291
|
+
branch,
|
|
292
|
+
id: this.output.id,
|
|
293
|
+
},
|
|
294
|
+
result: "full",
|
|
295
|
+
},
|
|
296
|
+
);
|
|
294
297
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
298
|
+
if (res.error && res.error.status !== 404) {
|
|
299
|
+
throw new Error(`Failed to delete branch "${branch}"`, {
|
|
300
|
+
cause: res.error,
|
|
301
|
+
});
|
|
300
302
|
}
|
|
301
|
-
} catch (error) {
|
|
302
|
-
logger.error("Error deleting password:", error);
|
|
303
|
-
throw error;
|
|
304
303
|
}
|
|
305
304
|
return this.destroy();
|
|
306
305
|
}
|
|
307
306
|
if (this.phase === "update") {
|
|
307
|
+
// Only name and cidrs can be updated in place; all other properties require replacement.
|
|
308
308
|
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
Array.isArray(props.cidrs) &&
|
|
313
|
-
this.output.cidrs.length === props.cidrs.length &&
|
|
314
|
-
this.output.cidrs.every((cidr, i) => cidr === props.cidrs![i])))
|
|
309
|
+
diff({ ...props, name }, this.output).some(
|
|
310
|
+
(prop) => prop !== "name" && prop !== "cidrs",
|
|
311
|
+
)
|
|
315
312
|
) {
|
|
316
313
|
return this.replace();
|
|
317
314
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
await api.organizations.databases.branches.passwords.patch({
|
|
316
|
+
path: {
|
|
317
|
+
organization,
|
|
318
|
+
database,
|
|
319
|
+
branch,
|
|
320
|
+
id: this.output.id,
|
|
321
|
+
},
|
|
322
|
+
body: {
|
|
321
323
|
name,
|
|
322
324
|
cidrs: props.cidrs,
|
|
323
325
|
},
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
if (!updateResponse.ok) {
|
|
327
|
-
throw new Error(
|
|
328
|
-
`Failed to update password: ${updateResponse.statusText} ${await updateResponse.text()}`,
|
|
329
|
-
);
|
|
330
|
-
}
|
|
326
|
+
});
|
|
331
327
|
|
|
332
328
|
return this({
|
|
333
329
|
...this.output,
|
|
334
330
|
...props,
|
|
331
|
+
name,
|
|
335
332
|
});
|
|
336
333
|
}
|
|
337
334
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
`Failed to create password: ${createResponse.statusText} ${await createResponse.text()}`,
|
|
353
|
-
);
|
|
354
|
-
}
|
|
335
|
+
const data = await api.organizations.databases.branches.passwords.post({
|
|
336
|
+
path: {
|
|
337
|
+
organization,
|
|
338
|
+
database,
|
|
339
|
+
branch,
|
|
340
|
+
},
|
|
341
|
+
body: {
|
|
342
|
+
name,
|
|
343
|
+
role: props.role,
|
|
344
|
+
replica: props.replica,
|
|
345
|
+
ttl: props.ttl,
|
|
346
|
+
cidrs: props.cidrs,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
355
349
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
name: `${props.name}-${nameSlug}`,
|
|
367
|
-
});
|
|
368
|
-
} catch (error) {
|
|
369
|
-
logger.error("Error managing password:", error);
|
|
370
|
-
throw error;
|
|
371
|
-
}
|
|
350
|
+
return this({
|
|
351
|
+
id: data.id,
|
|
352
|
+
expiresAt: data.expires_at,
|
|
353
|
+
host: data.access_host_url,
|
|
354
|
+
username: data.username,
|
|
355
|
+
password: alchemy.secret(data.plain_text),
|
|
356
|
+
nameSlug,
|
|
357
|
+
...props,
|
|
358
|
+
name: `${props.name}-${nameSlug}`,
|
|
359
|
+
});
|
|
372
360
|
},
|
|
373
361
|
);
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Returns an array of keys in `b` that are different from `a`.
|
|
365
|
+
*/
|
|
366
|
+
const diff = <T>(a: T, b: NoInfer<T>) => {
|
|
367
|
+
const keys: (keyof T)[] = [];
|
|
368
|
+
for (const key in a) {
|
|
369
|
+
if (!isDeepStrictEqual(a[key], b[key])) {
|
|
370
|
+
keys.push(key);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return keys;
|
|
374
|
+
};
|
package/src/planetscale/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PlanetScaleClient } from "./api/client.gen.ts";
|
|
2
2
|
|
|
3
3
|
export type PlanetScaleClusterSize =
|
|
4
4
|
| "PS_DEV"
|
|
@@ -25,165 +25,184 @@ export type PlanetScaleClusterSize =
|
|
|
25
25
|
* Wait for a database to be ready with exponential backoff
|
|
26
26
|
*/
|
|
27
27
|
export async function waitForDatabaseReady(
|
|
28
|
-
api:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
api: PlanetScaleClient,
|
|
29
|
+
organization: string,
|
|
30
|
+
database: string,
|
|
31
|
+
branch?: string,
|
|
32
32
|
): Promise<void> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (Date.now() - startTime >= 60000) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
`Timeout waiting for database ${databaseName} to be ready`,
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
57
|
-
waitMs = Math.min(waitMs * 2, 1000); // Cap at 1s intervals
|
|
58
|
-
}
|
|
33
|
+
await poll({
|
|
34
|
+
description: `database "${database}" ${branch ? `branch "${branch}"` : ""} ready`,
|
|
35
|
+
fn: async () => {
|
|
36
|
+
if (branch) {
|
|
37
|
+
return await api.organizations.databases.branches.get({
|
|
38
|
+
path: { organization, database, name: branch },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return await api.organizations.databases.get({
|
|
42
|
+
path: { organization, name: database },
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
predicate: (item) => item.ready,
|
|
46
|
+
});
|
|
59
47
|
}
|
|
60
48
|
|
|
61
49
|
/**
|
|
62
|
-
*
|
|
50
|
+
* Polls a keyspace until it is finished resizing.
|
|
63
51
|
*/
|
|
64
52
|
export async function waitForKeyspaceReady(
|
|
65
|
-
api:
|
|
66
|
-
|
|
67
|
-
|
|
53
|
+
api: PlanetScaleClient,
|
|
54
|
+
organization: string,
|
|
55
|
+
database: string,
|
|
68
56
|
branch: string,
|
|
69
57
|
keyspace: string,
|
|
70
58
|
): Promise<void> {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const ks = await res.json<{
|
|
85
|
-
data: [
|
|
86
|
-
{
|
|
87
|
-
state: string;
|
|
88
|
-
},
|
|
89
|
-
];
|
|
90
|
-
}>();
|
|
91
|
-
// once it's fully ready, we can proceed
|
|
92
|
-
if (ks.data.every((item) => item.state !== "resizing")) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (Date.now() - start > 600_000) {
|
|
97
|
-
throw new Error(`Timeout waiting for keyspace "${keyspace}" to be ready`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
101
|
-
delay = Math.min(delay * 2, 1_000);
|
|
102
|
-
}
|
|
59
|
+
await poll({
|
|
60
|
+
description: `keyspace "${keyspace}" ready`,
|
|
61
|
+
fn: () =>
|
|
62
|
+
api.organizations.databases.branches.keyspaces.resizes.list({
|
|
63
|
+
path: { organization, database, branch, name: keyspace },
|
|
64
|
+
}),
|
|
65
|
+
predicate: (response) =>
|
|
66
|
+
response.data.every((item) => item.state !== "resizing"),
|
|
67
|
+
initialDelay: 100,
|
|
68
|
+
maxDelay: 1000,
|
|
69
|
+
});
|
|
103
70
|
}
|
|
104
71
|
|
|
105
72
|
/**
|
|
106
|
-
*
|
|
73
|
+
* Ensure a branch is production and has the correct cluster size.
|
|
74
|
+
* If a branch is not production, it will be promoted to production because
|
|
75
|
+
* cluster sizes can only be configured for production branches.
|
|
107
76
|
*/
|
|
108
|
-
export async function
|
|
109
|
-
api:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
77
|
+
export async function ensureProductionBranchClusterSize(
|
|
78
|
+
api: PlanetScaleClient,
|
|
79
|
+
organization: string,
|
|
80
|
+
database: string,
|
|
81
|
+
branch: string,
|
|
113
82
|
expectedClusterSize: PlanetScaleClusterSize,
|
|
114
83
|
isDBReady: boolean,
|
|
115
84
|
): Promise<void> {
|
|
116
85
|
if (!isDBReady) {
|
|
117
|
-
await waitForDatabaseReady(api,
|
|
86
|
+
await waitForDatabaseReady(api, organization, database);
|
|
118
87
|
}
|
|
119
88
|
|
|
120
89
|
// 1. Ensure branch is production
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
90
|
+
const branchData = await api.organizations.databases.branches.get({
|
|
91
|
+
path: {
|
|
92
|
+
organization,
|
|
93
|
+
database,
|
|
94
|
+
name: branch,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
128
97
|
if (!branchData.production) {
|
|
129
98
|
if (!branchData.ready) {
|
|
130
|
-
await waitForDatabaseReady(api,
|
|
131
|
-
}
|
|
132
|
-
const promoteRes = await api.post(
|
|
133
|
-
`/organizations/${organizationId}/databases/${databaseName}/branches/${branchName}/promote`,
|
|
134
|
-
);
|
|
135
|
-
if (!promoteRes.ok) {
|
|
136
|
-
throw new Error(`Unable to promote branch: ${promoteRes.statusText}`);
|
|
99
|
+
await waitForDatabaseReady(api, organization, database, branch);
|
|
137
100
|
}
|
|
101
|
+
await api.organizations.databases.branches.promote.post({
|
|
102
|
+
path: {
|
|
103
|
+
organization,
|
|
104
|
+
database,
|
|
105
|
+
name: branch,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
138
108
|
}
|
|
139
109
|
// 2. Load default keyspace
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const defaultKsData = ksList.find((x) => x.name === databaseName); // Default keypsace is always the same name as the database
|
|
151
|
-
if (ksList.length === 0 || !defaultKsData) {
|
|
152
|
-
throw new Error(`No default keyspace found for branch ${branchName}`);
|
|
110
|
+
const keyspaces = await api.organizations.databases.branches.keyspaces.list({
|
|
111
|
+
path: {
|
|
112
|
+
organization,
|
|
113
|
+
database,
|
|
114
|
+
branch,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const defaultKeyspace = keyspaces.data.find((x) => x.name === database); // Default keyspace is always the same name as the database
|
|
118
|
+
if (!defaultKeyspace) {
|
|
119
|
+
throw new Error(`No default keyspace found for branch ${branch}`);
|
|
153
120
|
}
|
|
154
|
-
const defaultKs = defaultKsData.name;
|
|
155
|
-
let currentSize = defaultKsData.cluster_name as PlanetScaleClusterSize;
|
|
156
121
|
|
|
157
122
|
// 3. Wait until any in-flight resize is done
|
|
158
123
|
await waitForKeyspaceReady(
|
|
159
124
|
api,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
125
|
+
organization,
|
|
126
|
+
database,
|
|
127
|
+
branch,
|
|
128
|
+
defaultKeyspace.name,
|
|
164
129
|
);
|
|
165
130
|
|
|
166
131
|
// 4. If size mismatch, trigger resize and wait again
|
|
167
132
|
// Ideally this would use the undocumented Keyspaces API, but there seems to be a missing oauth scope that we cannot add via the console yet
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
}
|
|
133
|
+
if (defaultKeyspace.cluster_name !== expectedClusterSize) {
|
|
134
|
+
await api.organizations.databases.branches.cluster.patch({
|
|
135
|
+
path: {
|
|
136
|
+
organization,
|
|
137
|
+
database,
|
|
138
|
+
name: branch,
|
|
139
|
+
},
|
|
140
|
+
body: { cluster_size: expectedClusterSize },
|
|
141
|
+
});
|
|
179
142
|
|
|
180
143
|
// Poll until the resize completes
|
|
181
144
|
await waitForKeyspaceReady(
|
|
182
145
|
api,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
146
|
+
organization,
|
|
147
|
+
database,
|
|
148
|
+
branch,
|
|
149
|
+
defaultKeyspace.name,
|
|
187
150
|
);
|
|
188
151
|
}
|
|
189
152
|
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Polls a function until it returns a result that satisfies the predicate.
|
|
156
|
+
*/
|
|
157
|
+
async function poll<T>(input: {
|
|
158
|
+
/**
|
|
159
|
+
* A description of the operation being polled.
|
|
160
|
+
*/
|
|
161
|
+
description: string;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The function to poll.
|
|
165
|
+
*/
|
|
166
|
+
fn: () => Promise<T>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A predicate that determines if the operation has completed.
|
|
170
|
+
*/
|
|
171
|
+
predicate: (result: T) => boolean;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The initial delay between polls.
|
|
175
|
+
* @default 250ms
|
|
176
|
+
*/
|
|
177
|
+
initialDelay?: number;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The maximum delay between polls.
|
|
181
|
+
* @default 2_500ms (2.5 seconds)
|
|
182
|
+
*/
|
|
183
|
+
maxDelay?: number;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The timeout for the poll in milliseconds.
|
|
187
|
+
* @default 1_000_000 (~16 minutes)
|
|
188
|
+
*/
|
|
189
|
+
timeout?: number;
|
|
190
|
+
}): Promise<T> {
|
|
191
|
+
const start = Date.now();
|
|
192
|
+
let delay = input.initialDelay ?? 250;
|
|
193
|
+
while (true) {
|
|
194
|
+
const result = await input.fn();
|
|
195
|
+
if (input.predicate(result)) {
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
if (Date.now() - start >= (input.timeout ?? 1_000_000)) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Timed out waiting for ${input.description} after ${Math.round(
|
|
201
|
+
(input.timeout ?? 1_000_000) / 1000,
|
|
202
|
+
)}s`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
206
|
+
delay = Math.min(delay * 2, input.maxDelay ?? 5_000);
|
|
207
|
+
}
|
|
208
|
+
}
|
package/src/scope.ts
CHANGED
|
@@ -288,6 +288,11 @@ export class Scope {
|
|
|
288
288
|
this.dataMutex = new AsyncMutex();
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
public async has(id: string, type?: string): Promise<boolean> {
|
|
292
|
+
const state = await this.state.get(id);
|
|
293
|
+
return state !== undefined && (type === undefined || state.kind === type);
|
|
294
|
+
}
|
|
295
|
+
|
|
291
296
|
public createPhysicalName(id: string, delimiter = "-"): string {
|
|
292
297
|
const app = this.appName;
|
|
293
298
|
const stage = this.stage;
|
|
@@ -489,9 +494,9 @@ export class Scope {
|
|
|
489
494
|
return null;
|
|
490
495
|
}
|
|
491
496
|
|
|
492
|
-
public async finalize(force?: boolean) {
|
|
497
|
+
public async finalize(options?: { force?: boolean; noop?: boolean }) {
|
|
493
498
|
const shouldForce =
|
|
494
|
-
force ||
|
|
499
|
+
options?.force ||
|
|
495
500
|
this.parent === undefined ||
|
|
496
501
|
this?.parent?.scopeName === this.root.scopeName;
|
|
497
502
|
if (this.phase === "read") {
|
|
@@ -520,7 +525,10 @@ export class Scope {
|
|
|
520
525
|
await this.destroyPendingDeletions();
|
|
521
526
|
await Promise.all(
|
|
522
527
|
Array.from(this.children.values()).map((child) =>
|
|
523
|
-
child.finalize(
|
|
528
|
+
child.finalize({
|
|
529
|
+
force: shouldForce,
|
|
530
|
+
noop: options?.noop,
|
|
531
|
+
}),
|
|
524
532
|
),
|
|
525
533
|
);
|
|
526
534
|
}
|
|
@@ -541,6 +549,7 @@ export class Scope {
|
|
|
541
549
|
quiet: this.quiet,
|
|
542
550
|
strategy: this.destroyStrategy,
|
|
543
551
|
force: shouldForce,
|
|
552
|
+
noop: options?.noop,
|
|
544
553
|
});
|
|
545
554
|
this.rootTelemetryClient?.record({
|
|
546
555
|
event: "app.success",
|
package/src/upstash/redis.ts
CHANGED
|
@@ -182,7 +182,7 @@ export const UpstashRedis = Resource(
|
|
|
182
182
|
|
|
183
183
|
const eviction = props.eviction ?? false;
|
|
184
184
|
|
|
185
|
-
// @ts-
|
|
185
|
+
// @ts-expect-error This is overridden during update/create
|
|
186
186
|
let database: UpstashDatabaseResponse = {};
|
|
187
187
|
|
|
188
188
|
if (this.phase === "update") {
|