alchemy 0.63.1 → 0.65.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 +7664 -5857
- package/bin/commands/init.ts +130 -9
- package/bin/constants.ts +4 -0
- package/bin/services/cdp-manager/cdp-proxy.ts +69 -0
- package/bin/services/cdp-manager/server.ts +173 -0
- package/bin/services/execute-alchemy.ts +83 -4
- package/bin/types.ts +1 -0
- 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/bundle/plugin-wasm.d.ts.map +1 -1
- package/lib/cloudflare/bundle/plugin-wasm.js +20 -11
- package/lib/cloudflare/bundle/plugin-wasm.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/durable-object-namespace.js.map +1 -1
- package/lib/cloudflare/hyperdrive.d.ts +7 -2
- package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
- package/lib/cloudflare/hyperdrive.js +39 -57
- package/lib/cloudflare/hyperdrive.js.map +1 -1
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/next.d.ts +13 -0
- package/lib/cloudflare/next.d.ts.map +1 -0
- package/lib/cloudflare/next.js +91 -0
- package/lib/cloudflare/next.js.map +1 -0
- 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-assets.d.ts +3 -3
- package/lib/cloudflare/worker-assets.d.ts.map +1 -1
- package/lib/cloudflare/worker-assets.js +22 -47
- package/lib/cloudflare/worker-assets.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 +1 -0
- 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 +4 -6
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +20 -15
- 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/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 +122 -97
- package/lib/planetscale/branch.js.map +1 -1
- package/lib/planetscale/database.d.ts +24 -11
- package/lib/planetscale/database.d.ts.map +1 -1
- package/lib/planetscale/database.js +163 -118
- package/lib/planetscale/database.js.map +1 -1
- package/lib/planetscale/index.d.ts +2 -1
- package/lib/planetscale/index.d.ts.map +1 -1
- package/lib/planetscale/index.js +2 -1
- package/lib/planetscale/index.js.map +1 -1
- package/lib/planetscale/password.d.ts +7 -8
- package/lib/planetscale/password.d.ts.map +1 -1
- package/lib/planetscale/password.js +74 -57
- package/lib/planetscale/password.js.map +1 -1
- package/lib/planetscale/role.d.ts +126 -0
- package/lib/planetscale/role.d.ts.map +1 -0
- package/lib/planetscale/role.js +144 -0
- package/lib/planetscale/role.js.map +1 -0
- package/lib/planetscale/utils.d.ts +23 -7
- package/lib/planetscale/utils.d.ts.map +1 -1
- package/lib/planetscale/utils.js +169 -75
- 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/secret.d.ts +0 -3
- package/lib/secret.d.ts.map +1 -1
- package/lib/secret.js +0 -2
- package/lib/secret.js.map +1 -1
- package/lib/state/cloudflare-state-store.js +3 -3
- package/lib/state/cloudflare-state-store.js.map +1 -1
- package/lib/test/bun.d.ts +2 -2
- package/lib/test/bun.d.ts.map +1 -1
- package/lib/test/vitest.d.ts +2 -2
- package/lib/test/vitest.d.ts.map +1 -1
- package/lib/test/vitest.js +4 -4
- package/lib/test/vitest.js.map +1 -1
- package/lib/util/content-type.js +2 -2
- package/lib/util/content-type.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 +33 -45
- package/lib/util/dedent.js.map +1 -1
- package/lib/util/memoize.d.ts.map +1 -1
- package/lib/util/memoize.js +11 -1
- package/lib/util/memoize.js.map +1 -1
- package/package.json +8 -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/bundle/plugin-wasm.ts +34 -14
- 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 -0
- package/src/cloudflare/hyperdrive.ts +136 -95
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/next.ts +123 -0
- package/src/cloudflare/website.ts +88 -89
- package/src/cloudflare/worker-assets.ts +43 -105
- package/src/cloudflare/worker-bundle.ts +46 -9
- package/src/cloudflare/worker-metadata.ts +2 -0
- package/src/cloudflare/worker.ts +35 -7
- package/src/cloudflare/wrangler.json.ts +114 -113
- package/src/context.ts +5 -3
- package/src/destroy.ts +26 -8
- 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 +170 -162
- package/src/planetscale/database.ts +263 -243
- package/src/planetscale/index.ts +2 -1
- package/src/planetscale/password.ts +89 -86
- package/src/planetscale/role.ts +278 -0
- package/src/planetscale/utils.ts +289 -128
- package/src/scope.ts +12 -3
- package/src/secret.ts +0 -4
- package/src/state/cloudflare-state-store.ts +4 -4
- package/src/test/bun.ts +2 -2
- package/src/test/vitest.ts +6 -6
- package/src/util/content-type.ts +2 -2
- package/src/util/dedent.ts +34 -50
- package/src/util/memoize.ts +13 -1
- package/templates/astro/alchemy.run.ts +1 -3
- package/templates/hono/alchemy.run.ts +0 -1
- package/templates/nextjs/README.md +85 -0
- package/templates/nextjs/_env +1 -0
- package/templates/nextjs/_env.example +1 -0
- package/templates/nextjs/_gitignore +46 -0
- package/templates/nextjs/alchemy.run.ts +16 -0
- package/templates/nextjs/eslint.config.mjs +16 -0
- package/templates/nextjs/next-env.d.ts +5 -0
- package/templates/nextjs/next.config.ts +10 -0
- package/templates/nextjs/open-next.config.ts +5 -0
- package/templates/nextjs/package.json +31 -0
- package/templates/nextjs/src/app/api/hello/route.ts +19 -0
- package/templates/nextjs/src/app/favicon.ico +0 -0
- package/templates/nextjs/src/app/globals.css +72 -0
- package/templates/nextjs/src/app/layout.tsx +19 -0
- package/templates/nextjs/src/app/page.tsx +55 -0
- package/templates/nextjs/tsconfig.json +33 -0
- package/templates/nextjs/types/env.d.ts +14 -0
- 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
package/src/planetscale/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PlanetScaleClient } from "./api/client.gen.ts";
|
|
2
|
+
import type { DatabaseBranch } from "./api/types.gen.ts";
|
|
2
3
|
|
|
3
4
|
export type PlanetScaleClusterSize =
|
|
4
5
|
| "PS_DEV"
|
|
@@ -22,168 +23,328 @@ export type PlanetScaleClusterSize =
|
|
|
22
23
|
| (string & {});
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
+
* Ensures that the given cluster size is in the correct format.
|
|
26
27
|
*/
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
):
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (data.ready === true) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
28
|
+
export function sanitizeClusterSize(input: {
|
|
29
|
+
size: PlanetScaleClusterSize;
|
|
30
|
+
kind?: "mysql" | "postgresql";
|
|
31
|
+
arch?: "x86" | "arm";
|
|
32
|
+
region?: string;
|
|
33
|
+
}): string {
|
|
34
|
+
// NAS-backed Postgres cluster sizes are formatted as `PS_<size>_<provider>_<arch>`,
|
|
35
|
+
// where <provider> is either "AWS" or "GCP", and <arch> is either "ARM" or "X86".
|
|
36
|
+
// Postgres clusters backed by PlanetScale Metal are more complex (e.g. "M6_160_AWS_INTEL_D_METAL_118"),
|
|
37
|
+
// so to avoid messing with those, we just check for AWS or GCP in the size.
|
|
38
|
+
if (input.kind === "postgresql" && !input.size.match(/(AWS|GCP)/)) {
|
|
39
|
+
// Infer the provider from the region.
|
|
40
|
+
// Not all AWS regions start with "aws-", but all GCP regions start with "gcp-".
|
|
41
|
+
const provider = input.region?.startsWith("gcp") ? "GCP" : "AWS";
|
|
42
|
+
const arch = (input.arch ?? "x86").toUpperCase();
|
|
43
|
+
return `${input.size}_${provider}_${arch}`;
|
|
44
|
+
}
|
|
45
|
+
return input.size;
|
|
46
|
+
}
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Polls a branch until it is ready.
|
|
50
|
+
*/
|
|
51
|
+
export async function waitForBranchReady(
|
|
52
|
+
api: PlanetScaleClient,
|
|
53
|
+
organization: string,
|
|
54
|
+
database: string,
|
|
55
|
+
branch: string,
|
|
56
|
+
): Promise<DatabaseBranch> {
|
|
57
|
+
return await poll({
|
|
58
|
+
description: `branch "${branch}" ready`,
|
|
59
|
+
fn: () =>
|
|
60
|
+
api.organizations.databases.branches.get({
|
|
61
|
+
path: { organization, database, name: branch },
|
|
62
|
+
}),
|
|
63
|
+
predicate: (branch) => branch.ready,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Polls a database until it is ready.
|
|
69
|
+
*/
|
|
70
|
+
export async function waitForDatabaseReady(
|
|
71
|
+
api: PlanetScaleClient,
|
|
72
|
+
organization: string,
|
|
73
|
+
database: string,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
await poll({
|
|
76
|
+
description: `database "${database}" ready`,
|
|
77
|
+
fn: () =>
|
|
78
|
+
api.organizations.databases.get({
|
|
79
|
+
path: { organization, name: database },
|
|
80
|
+
}),
|
|
81
|
+
predicate: (database) => database.ready,
|
|
82
|
+
});
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
/**
|
|
62
|
-
*
|
|
86
|
+
* Polls a keyspace until it is finished resizing.
|
|
63
87
|
*/
|
|
64
88
|
export async function waitForKeyspaceReady(
|
|
65
|
-
api:
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
api: PlanetScaleClient,
|
|
90
|
+
organization: string,
|
|
91
|
+
database: string,
|
|
68
92
|
branch: string,
|
|
69
93
|
keyspace: string,
|
|
70
94
|
): Promise<void> {
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
await poll({
|
|
96
|
+
description: `keyspace "${keyspace}" ready`,
|
|
97
|
+
fn: () =>
|
|
98
|
+
api.organizations.databases.branches.keyspaces.resizes.list({
|
|
99
|
+
path: { organization, database, branch, name: keyspace },
|
|
100
|
+
}),
|
|
101
|
+
predicate: (response) =>
|
|
102
|
+
response.data.every((item) => item.state !== "resizing"),
|
|
103
|
+
initialDelay: 100,
|
|
104
|
+
maxDelay: 1000,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
73
107
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Ensure a branch is production and has the correct cluster size.
|
|
110
|
+
* If a branch is not production, it will be promoted to production because
|
|
111
|
+
* cluster sizes can only be configured for production branches.
|
|
112
|
+
*/
|
|
113
|
+
export async function ensureProductionBranchClusterSize(
|
|
114
|
+
api: PlanetScaleClient,
|
|
115
|
+
organization: string,
|
|
116
|
+
database: string,
|
|
117
|
+
branch: string,
|
|
118
|
+
kind: "mysql" | "postgresql",
|
|
119
|
+
expectedClusterSize: PlanetScaleClusterSize,
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
switch (kind) {
|
|
122
|
+
case "mysql": {
|
|
123
|
+
// Vitess databases must be promoted before resizing
|
|
124
|
+
await ensureProductionBranch(api, organization, database, branch);
|
|
125
|
+
await ensureMySQLClusterSize(
|
|
126
|
+
api,
|
|
127
|
+
organization,
|
|
128
|
+
database,
|
|
129
|
+
branch,
|
|
130
|
+
expectedClusterSize,
|
|
81
131
|
);
|
|
132
|
+
break;
|
|
82
133
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
134
|
+
case "postgresql": {
|
|
135
|
+
// Postgres databases must be resized first before promoting, otherwise 500 error
|
|
136
|
+
await ensurePostgresClusterSize(
|
|
137
|
+
api,
|
|
138
|
+
organization,
|
|
139
|
+
database,
|
|
140
|
+
branch,
|
|
141
|
+
expectedClusterSize,
|
|
142
|
+
);
|
|
143
|
+
await ensureProductionBranch(api, organization, database, branch);
|
|
144
|
+
break;
|
|
94
145
|
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
95
148
|
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Checks if a branch is production and promotes it if it is not.
|
|
151
|
+
*/
|
|
152
|
+
async function ensureProductionBranch(
|
|
153
|
+
api: PlanetScaleClient,
|
|
154
|
+
organization: string,
|
|
155
|
+
database: string,
|
|
156
|
+
name: string,
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
const data = await api.organizations.databases.branches.get({
|
|
159
|
+
path: {
|
|
160
|
+
organization,
|
|
161
|
+
database,
|
|
162
|
+
name,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
if (!data.production) {
|
|
166
|
+
if (!data.ready) {
|
|
167
|
+
await waitForBranchReady(api, organization, database, name);
|
|
98
168
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
169
|
+
await api.organizations.databases.branches.promote.post({
|
|
170
|
+
path: { organization, database, name },
|
|
171
|
+
});
|
|
102
172
|
}
|
|
103
173
|
}
|
|
104
174
|
|
|
105
175
|
/**
|
|
106
|
-
*
|
|
176
|
+
* Ensures that a MySQL branch has the correct cluster size.
|
|
107
177
|
*/
|
|
108
|
-
|
|
109
|
-
api:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
178
|
+
async function ensureMySQLClusterSize(
|
|
179
|
+
api: PlanetScaleClient,
|
|
180
|
+
organization: string,
|
|
181
|
+
database: string,
|
|
182
|
+
branch: string,
|
|
113
183
|
expectedClusterSize: PlanetScaleClusterSize,
|
|
114
|
-
isDBReady: boolean,
|
|
115
184
|
): Promise<void> {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
let branchData = await branchRes.json<any>();
|
|
128
|
-
if (!branchData.production) {
|
|
129
|
-
if (!branchData.ready) {
|
|
130
|
-
await waitForDatabaseReady(api, organizationId, databaseName, branchName);
|
|
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}`);
|
|
137
|
-
}
|
|
185
|
+
// 1. Load default keyspace
|
|
186
|
+
const keyspaces = await api.organizations.databases.branches.keyspaces.list({
|
|
187
|
+
path: {
|
|
188
|
+
organization,
|
|
189
|
+
database,
|
|
190
|
+
branch,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
const defaultKeyspace = keyspaces.data.find((x) => x.name === database); // Default keyspace is always the same name as the database
|
|
194
|
+
if (!defaultKeyspace) {
|
|
195
|
+
throw new Error(`No default keyspace found for branch ${branch}`);
|
|
138
196
|
}
|
|
139
|
-
// 2. Load default keyspace
|
|
140
|
-
const ksListRes = await api.get(
|
|
141
|
-
`/organizations/${organizationId}/databases/${databaseName}/branches/${branchName}/keyspaces`,
|
|
142
|
-
);
|
|
143
|
-
if (!ksListRes.ok) {
|
|
144
|
-
throw new Error(`Failed to list keyspaces: ${ksListRes.statusText}`);
|
|
145
|
-
}
|
|
146
|
-
const ksList = (await ksListRes.json<any>()).data as Array<{
|
|
147
|
-
name: string;
|
|
148
|
-
cluster_name: string;
|
|
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}`);
|
|
153
|
-
}
|
|
154
|
-
const defaultKs = defaultKsData.name;
|
|
155
|
-
let currentSize = defaultKsData.cluster_name as PlanetScaleClusterSize;
|
|
156
197
|
|
|
157
|
-
//
|
|
198
|
+
// 2. Wait until any in-flight resize is done
|
|
158
199
|
await waitForKeyspaceReady(
|
|
159
200
|
api,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
201
|
+
organization,
|
|
202
|
+
database,
|
|
203
|
+
branch,
|
|
204
|
+
defaultKeyspace.name,
|
|
164
205
|
);
|
|
165
206
|
|
|
166
|
-
//
|
|
207
|
+
// 3. If size mismatch, trigger resize and wait again
|
|
167
208
|
// 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
|
-
}
|
|
209
|
+
if (defaultKeyspace.cluster_name !== expectedClusterSize) {
|
|
210
|
+
await api.organizations.databases.branches.cluster.patch({
|
|
211
|
+
path: {
|
|
212
|
+
organization,
|
|
213
|
+
database,
|
|
214
|
+
name: branch,
|
|
215
|
+
},
|
|
216
|
+
body: { cluster_size: expectedClusterSize },
|
|
217
|
+
});
|
|
179
218
|
|
|
180
219
|
// Poll until the resize completes
|
|
181
220
|
await waitForKeyspaceReady(
|
|
182
221
|
api,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
222
|
+
organization,
|
|
223
|
+
database,
|
|
224
|
+
branch,
|
|
225
|
+
defaultKeyspace.name,
|
|
187
226
|
);
|
|
188
227
|
}
|
|
189
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Ensures that a Postgres branch has the correct cluster size.
|
|
232
|
+
*/
|
|
233
|
+
async function ensurePostgresClusterSize(
|
|
234
|
+
api: PlanetScaleClient,
|
|
235
|
+
organization: string,
|
|
236
|
+
database: string,
|
|
237
|
+
branch: string,
|
|
238
|
+
expectedClusterSize: PlanetScaleClusterSize,
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
const data = await api.organizations.databases.branches.get({
|
|
241
|
+
path: {
|
|
242
|
+
organization,
|
|
243
|
+
database,
|
|
244
|
+
name: branch,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
if (data.cluster_name === expectedClusterSize) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
await waitForPendingPostgresChanges(api, organization, database, branch);
|
|
251
|
+
const change = await api.organizations.databases.branches.changes.patch({
|
|
252
|
+
path: {
|
|
253
|
+
organization,
|
|
254
|
+
database,
|
|
255
|
+
branch,
|
|
256
|
+
},
|
|
257
|
+
body: {
|
|
258
|
+
cluster_size: expectedClusterSize,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
await waitForPendingPostgresChanges(
|
|
262
|
+
api,
|
|
263
|
+
organization,
|
|
264
|
+
database,
|
|
265
|
+
branch,
|
|
266
|
+
change.id,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Polls for a pending Postgres change to be completed.
|
|
272
|
+
*/
|
|
273
|
+
async function waitForPendingPostgresChanges(
|
|
274
|
+
api: PlanetScaleClient,
|
|
275
|
+
organization: string,
|
|
276
|
+
database: string,
|
|
277
|
+
branch: string,
|
|
278
|
+
changeId?: string,
|
|
279
|
+
) {
|
|
280
|
+
await poll({
|
|
281
|
+
description: `changes for branch "${branch}"`,
|
|
282
|
+
fn: () =>
|
|
283
|
+
api.organizations.databases.branches.changes.list({
|
|
284
|
+
path: { organization, database, branch },
|
|
285
|
+
}),
|
|
286
|
+
predicate: (response) =>
|
|
287
|
+
response.data.every(
|
|
288
|
+
(change) =>
|
|
289
|
+
change.state === "completed" ||
|
|
290
|
+
change.state === "canceled" ||
|
|
291
|
+
(changeId && change.id === changeId),
|
|
292
|
+
),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Polls a function until it returns a result that satisfies the predicate.
|
|
298
|
+
*/
|
|
299
|
+
async function poll<T>(input: {
|
|
300
|
+
/**
|
|
301
|
+
* A description of the operation being polled.
|
|
302
|
+
*/
|
|
303
|
+
description: string;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The function to poll.
|
|
307
|
+
*/
|
|
308
|
+
fn: () => Promise<T>;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* A predicate that determines if the operation has completed.
|
|
312
|
+
*/
|
|
313
|
+
predicate: (result: T) => boolean;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The initial delay between polls.
|
|
317
|
+
* @default 250ms
|
|
318
|
+
*/
|
|
319
|
+
initialDelay?: number;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The maximum delay between polls.
|
|
323
|
+
* @default 2_500ms (2.5 seconds)
|
|
324
|
+
*/
|
|
325
|
+
maxDelay?: number;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The timeout for the poll in milliseconds.
|
|
329
|
+
* @default 1_000_000 (~16 minutes)
|
|
330
|
+
*/
|
|
331
|
+
timeout?: number;
|
|
332
|
+
}): Promise<T> {
|
|
333
|
+
const start = Date.now();
|
|
334
|
+
let delay = input.initialDelay ?? 250;
|
|
335
|
+
while (true) {
|
|
336
|
+
const result = await input.fn();
|
|
337
|
+
if (input.predicate(result)) {
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
if (Date.now() - start >= (input.timeout ?? 1_000_000)) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Timed out waiting for ${input.description} after ${Math.round(
|
|
343
|
+
(input.timeout ?? 1_000_000) / 1000,
|
|
344
|
+
)}s`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
348
|
+
delay = Math.min(delay * 2, input.maxDelay ?? 5_000);
|
|
349
|
+
}
|
|
350
|
+
}
|
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/secret.ts
CHANGED
|
@@ -17,8 +17,6 @@ function nextName() {
|
|
|
17
17
|
return `alchemy:anonymous-secret-${i++}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const SecretSymbol = Symbol.for("alchemy::Secret");
|
|
21
|
-
|
|
22
20
|
/**
|
|
23
21
|
* Internal wrapper for sensitive values like API keys and credentials.
|
|
24
22
|
* When stored in alchemy state files, the value is automatically encrypted
|
|
@@ -55,8 +53,6 @@ const SecretSymbol = Symbol.for("alchemy::Secret");
|
|
|
55
53
|
* }
|
|
56
54
|
*/
|
|
57
55
|
export class Secret<T = string> {
|
|
58
|
-
[SecretSymbol] = true;
|
|
59
|
-
|
|
60
56
|
/**
|
|
61
57
|
* @internal
|
|
62
58
|
*/
|
|
@@ -47,13 +47,13 @@ export class CloudflareStateStore extends StateStoreProxy {
|
|
|
47
47
|
options: CloudflareStateStoreOptions & { stateToken: Secret<string> };
|
|
48
48
|
constructor(scope: Scope, options: CloudflareStateStoreOptions = {}) {
|
|
49
49
|
super(scope);
|
|
50
|
-
|
|
51
|
-
options.stateToken ?? alchemy.secret(process.env.ALCHEMY_STATE_TOKEN);
|
|
52
|
-
if (!stateToken) {
|
|
50
|
+
if (!options.stateToken && !process.env.ALCHEMY_STATE_TOKEN) {
|
|
53
51
|
throw new Error(
|
|
54
|
-
"Missing token for
|
|
52
|
+
"Missing token for CloudflareStateStore. Please set ALCHEMY_STATE_TOKEN in the environment or set the `stateToken` option in the CloudflareStateStore constructor. See https://alchemy.run/guides/cloudflare-state-store/",
|
|
55
53
|
);
|
|
56
54
|
}
|
|
55
|
+
const stateToken =
|
|
56
|
+
options.stateToken ?? alchemy.secret(process.env.ALCHEMY_STATE_TOKEN);
|
|
57
57
|
this.options = {
|
|
58
58
|
...options,
|
|
59
59
|
stateToken: stateToken,
|
package/src/test/bun.ts
CHANGED
|
@@ -53,9 +53,9 @@ type test = {
|
|
|
53
53
|
*/
|
|
54
54
|
skipIf(condition: boolean): test;
|
|
55
55
|
|
|
56
|
-
beforeAll(fn: (scope: Scope) => Promise<void
|
|
56
|
+
beforeAll(fn: (scope: Scope) => Promise<void>, timeout?: number): void;
|
|
57
57
|
|
|
58
|
-
afterAll(fn: (scope: Scope) => Promise<void
|
|
58
|
+
afterAll(fn: (scope: Scope) => Promise<void>, timeout?: number): void;
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Current test scope
|
package/src/test/vitest.ts
CHANGED
|
@@ -57,9 +57,9 @@ type test = {
|
|
|
57
57
|
*/
|
|
58
58
|
skipIf(condition: boolean): test;
|
|
59
59
|
|
|
60
|
-
beforeAll(fn: (scope: Scope) => Promise<void
|
|
60
|
+
beforeAll(fn: (scope: Scope) => Promise<void>, timeout?: number): void;
|
|
61
61
|
|
|
62
|
-
afterAll(fn: (scope: Scope) => Promise<void
|
|
62
|
+
afterAll(fn: (scope: Scope) => Promise<void>, timeout?: number): void;
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* Current test scope
|
|
@@ -134,12 +134,12 @@ export function test(
|
|
|
134
134
|
password: process.env.ALCHEMY_PASSWORD,
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
test.beforeAll = (fn: (scope: Scope) => Promise<void
|
|
138
|
-
return beforeAll(() => scope.run(() => fn(scope)));
|
|
137
|
+
test.beforeAll = (fn: (scope: Scope) => Promise<void>, timeout?: number) => {
|
|
138
|
+
return beforeAll(() => scope.run(() => fn(scope)), timeout);
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
-
test.afterAll = (fn: (scope: Scope) => Promise<void
|
|
142
|
-
return afterAll(() => scope.run(() => fn(scope)));
|
|
141
|
+
test.afterAll = (fn: (scope: Scope) => Promise<void>, timeout?: number) => {
|
|
142
|
+
return afterAll(() => scope.run(() => fn(scope)), timeout);
|
|
143
143
|
};
|
|
144
144
|
|
|
145
145
|
return test as test;
|
package/src/util/content-type.ts
CHANGED
|
@@ -11,10 +11,10 @@ const mimeTypes: Record<string, string> = {
|
|
|
11
11
|
".jpeg": "image/jpeg",
|
|
12
12
|
".jpg": "image/jpeg",
|
|
13
13
|
".js.map": "application/source-map",
|
|
14
|
-
".js": "
|
|
14
|
+
".js": "text/javascript",
|
|
15
15
|
".json": "application/json",
|
|
16
16
|
".md": "text/markdown",
|
|
17
|
-
".mjs": "
|
|
17
|
+
".mjs": "text/javascript",
|
|
18
18
|
".mp3": "audio/mpeg",
|
|
19
19
|
".mp4": "video/mp4",
|
|
20
20
|
".otf": "font/otf",
|