alchemy 0.64.0 → 0.65.1
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 +7570 -5773
- 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/cloudflare/bundle/plugin-node-compat.js +2 -2
- package/lib/cloudflare/bundle/plugin-node-compat.js.map +1 -1
- 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/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/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/wrangler.json.d.ts +2 -2
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +4 -1
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/planetscale/branch.d.ts.map +1 -1
- package/lib/planetscale/branch.js +36 -25
- package/lib/planetscale/branch.js.map +1 -1
- package/lib/planetscale/database.d.ts +23 -6
- package/lib/planetscale/database.d.ts.map +1 -1
- package/lib/planetscale/database.js +11 -5
- package/lib/planetscale/database.js.map +1 -1
- package/lib/planetscale/index.d.ts +1 -0
- package/lib/planetscale/index.d.ts.map +1 -1
- package/lib/planetscale/index.js +1 -0
- package/lib/planetscale/index.js.map +1 -1
- package/lib/planetscale/password.d.ts +3 -1
- package/lib/planetscale/password.d.ts.map +1 -1
- package/lib/planetscale/password.js +3 -1
- 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 +17 -3
- package/lib/planetscale/utils.d.ts.map +1 -1
- package/lib/planetscale/utils.js +110 -31
- package/lib/planetscale/utils.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.map +1 -1
- package/lib/util/dedent.js +30 -18
- package/lib/util/dedent.js.map +1 -1
- package/lib/util/memoize.d.ts +1 -1
- package/lib/util/memoize.d.ts.map +1 -1
- package/lib/util/memoize.js +13 -3
- package/lib/util/memoize.js.map +1 -1
- package/package.json +5 -1
- package/src/cloudflare/bundle/plugin-node-compat.ts +2 -2
- package/src/cloudflare/bundle/plugin-wasm.ts +34 -14
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/hyperdrive.ts +136 -95
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/next.ts +123 -0
- package/src/cloudflare/worker-assets.ts +43 -105
- package/src/cloudflare/wrangler.json.ts +6 -3
- package/src/planetscale/branch.ts +52 -27
- package/src/planetscale/database.ts +75 -46
- package/src/planetscale/index.ts +1 -0
- package/src/planetscale/password.ts +3 -1
- package/src/planetscale/role.ts +278 -0
- package/src/planetscale/utils.ts +172 -30
- 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 +33 -21
- package/src/util/memoize.ts +15 -3
- 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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { getPackageManagerRunner } from "../util/detect-package-manager.ts";
|
|
5
|
+
import { exists } from "../util/exists.ts";
|
|
6
|
+
import ignore from "../util/ignore-matcher.ts";
|
|
7
|
+
import { logger } from "../util/logger.ts";
|
|
8
|
+
import type { Assets } from "./assets.ts";
|
|
9
|
+
import type { Bindings } from "./bindings.ts";
|
|
10
|
+
import { Website, type WebsiteProps } from "./website.ts";
|
|
11
|
+
import type { Worker } from "./worker.ts";
|
|
12
|
+
|
|
13
|
+
export interface NextjsProps<B extends Bindings> extends WebsiteProps<B> {}
|
|
14
|
+
|
|
15
|
+
export type Nextjs<B extends Bindings> = B extends { ASSETS: any }
|
|
16
|
+
? never
|
|
17
|
+
: Worker<B & { ASSETS: Assets }>;
|
|
18
|
+
|
|
19
|
+
export async function Nextjs<const B extends Bindings>(
|
|
20
|
+
id: string,
|
|
21
|
+
props: NextjsProps<B> = {},
|
|
22
|
+
): Promise<Nextjs<B>> {
|
|
23
|
+
const runner = await getPackageManagerRunner();
|
|
24
|
+
const isIgnored = await isFileIgnored("wrangler.jsonc", props.cwd);
|
|
25
|
+
if (!isIgnored) {
|
|
26
|
+
logger.warn(
|
|
27
|
+
[
|
|
28
|
+
pc.bold(
|
|
29
|
+
`The ${pc.cyan("wrangler.jsonc")} file for Next.js ${pc.cyan(`"${id}"`)} is not ignored.`,
|
|
30
|
+
),
|
|
31
|
+
"This may cause unexpected behavior. Please add the file to your .gitignore.",
|
|
32
|
+
].join(" "),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return await Website(id, {
|
|
36
|
+
...props,
|
|
37
|
+
entrypoint: props.entrypoint ?? ".open-next/worker.js",
|
|
38
|
+
wrangler: {
|
|
39
|
+
path: "wrangler.jsonc",
|
|
40
|
+
secrets: isIgnored,
|
|
41
|
+
...(props.wrangler ?? {}),
|
|
42
|
+
},
|
|
43
|
+
build: normalizeCommand(props.build, {
|
|
44
|
+
command: `${runner} opennextjs-cloudflare build`,
|
|
45
|
+
env: {
|
|
46
|
+
NEXTJS_ENV: "production",
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
dev: normalizeCommand(props.dev, {
|
|
50
|
+
command: `${runner} next dev`,
|
|
51
|
+
env: {
|
|
52
|
+
NEXTJS_ENV: "development",
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
// OpenNext generates the files, but relies on us to bundle them.
|
|
57
|
+
noBundle: props.noBundle ?? false,
|
|
58
|
+
|
|
59
|
+
spa: false,
|
|
60
|
+
compatibilityFlags: [
|
|
61
|
+
"nodejs_compat",
|
|
62
|
+
"global_fetch_strictly_public",
|
|
63
|
+
...(props.compatibilityFlags ?? []),
|
|
64
|
+
],
|
|
65
|
+
assets: props.assets ?? ".open-next/assets",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a file is ignored by .gitignore.
|
|
71
|
+
*
|
|
72
|
+
* @param filename - The name of the file to check.
|
|
73
|
+
* @param cwd - The current working directory.
|
|
74
|
+
* @returns True if the file is ignored, false otherwise.
|
|
75
|
+
*/
|
|
76
|
+
const isFileIgnored = async (filename: string, cwd?: string) => {
|
|
77
|
+
const matcher = ignore();
|
|
78
|
+
|
|
79
|
+
// Read .gitignore from current directory and walk up the directory tree
|
|
80
|
+
let currentDir = path.resolve(cwd ?? process.cwd());
|
|
81
|
+
const gitignorePatterns: string[] = [];
|
|
82
|
+
|
|
83
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
84
|
+
const gitignorePath = path.join(currentDir, ".gitignore");
|
|
85
|
+
try {
|
|
86
|
+
const gitignoreContent = await fs.readFile(gitignorePath, "utf8");
|
|
87
|
+
// unshift to prioritize local .gitignore
|
|
88
|
+
gitignorePatterns.unshift(
|
|
89
|
+
...gitignoreContent
|
|
90
|
+
.split("\n")
|
|
91
|
+
.filter((line) => line.trim() && !line.startsWith("#")),
|
|
92
|
+
);
|
|
93
|
+
} catch {
|
|
94
|
+
// .gitignore doesn't exist in this directory, continue up
|
|
95
|
+
}
|
|
96
|
+
if (await exists(path.join(currentDir, ".git"))) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
currentDir = path.dirname(currentDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
matcher.add(gitignorePatterns);
|
|
103
|
+
|
|
104
|
+
return matcher.ignores(filename);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const normalizeCommand = (
|
|
108
|
+
input: WebsiteProps<any>["build"],
|
|
109
|
+
defaults: {
|
|
110
|
+
command: string;
|
|
111
|
+
env: Record<string, string>;
|
|
112
|
+
},
|
|
113
|
+
): WebsiteProps<any>["build"] => {
|
|
114
|
+
return {
|
|
115
|
+
command:
|
|
116
|
+
typeof input === "string" ? input : (input?.command ?? defaults.command),
|
|
117
|
+
env: {
|
|
118
|
+
...defaults.env,
|
|
119
|
+
...(typeof input === "object" ? (input?.env ?? {}) : {}),
|
|
120
|
+
},
|
|
121
|
+
memoize: typeof input === "object" ? input?.memoize : undefined,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
@@ -2,10 +2,10 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AsyncQueue } from "../util/async-queue.ts";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { withExponentialBackoff } from "../util/retry.ts";
|
|
6
|
+
import { extractCloudflareResult } from "./api-response.ts";
|
|
7
7
|
import type { CloudflareApi } from "./api.ts";
|
|
8
|
-
import type { Assets } from "./assets.ts";
|
|
8
|
+
import type { AssetFile, Assets } from "./assets.ts";
|
|
9
9
|
import type { AssetsConfig, WorkerProps } from "./worker.ts";
|
|
10
10
|
|
|
11
11
|
export interface AssetUploadResult {
|
|
@@ -21,32 +21,6 @@ interface FileMetadata {
|
|
|
21
21
|
size: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* Response from the assets upload session API
|
|
26
|
-
*/
|
|
27
|
-
interface UploadSessionResponse {
|
|
28
|
-
result: {
|
|
29
|
-
jwt: string;
|
|
30
|
-
buckets: string[][];
|
|
31
|
-
};
|
|
32
|
-
success: boolean;
|
|
33
|
-
errors: any[];
|
|
34
|
-
messages: any[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Response from the file upload API
|
|
39
|
-
*/
|
|
40
|
-
interface UploadResponse {
|
|
41
|
-
result: {
|
|
42
|
-
jwt: string;
|
|
43
|
-
buckets?: string[][];
|
|
44
|
-
};
|
|
45
|
-
success: boolean;
|
|
46
|
-
errors: any[];
|
|
47
|
-
messages: any[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
24
|
/**
|
|
51
25
|
* Uploads assets to Cloudflare and returns a completion token
|
|
52
26
|
*
|
|
@@ -73,64 +47,41 @@ export async function uploadAssets(
|
|
|
73
47
|
// Process the assets configuration once at the beginning
|
|
74
48
|
const processedConfig = createAssetConfig(assetConfig);
|
|
75
49
|
|
|
76
|
-
const { manifest,
|
|
50
|
+
const { manifest, filesByHash } = await prepareAssetManifest(assets);
|
|
77
51
|
|
|
78
52
|
// Start the upload session
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
53
|
+
const uploadSession = await extractCloudflareResult<{
|
|
54
|
+
jwt: string;
|
|
55
|
+
buckets: string[][];
|
|
56
|
+
}>(
|
|
57
|
+
`create assets upload session for worker "${workerName}"`,
|
|
58
|
+
api.post(
|
|
59
|
+
namespace
|
|
60
|
+
? `/accounts/${api.accountId}/workers/dispatch/namespaces/${namespace}/scripts/${workerName}/assets-upload-session`
|
|
61
|
+
: `/accounts/${api.accountId}/workers/scripts/${workerName}/assets-upload-session`,
|
|
62
|
+
{ manifest },
|
|
63
|
+
),
|
|
87
64
|
);
|
|
88
|
-
|
|
89
|
-
if (!uploadSessionResponse.ok) {
|
|
90
|
-
throw new Error(
|
|
91
|
-
`Failed to start assets upload session: ${uploadSessionResponse.status} ${uploadSessionResponse.statusText} - ${await uploadSessionResponse.text()}`,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const sessionData =
|
|
96
|
-
(await uploadSessionResponse.json()) as UploadSessionResponse;
|
|
97
|
-
|
|
98
|
-
if (!sessionData?.success) {
|
|
99
|
-
if (sessionData?.errors) {
|
|
100
|
-
throw new CloudflareApiError(
|
|
101
|
-
`Failed to start assets upload session:\n${sessionData.errors
|
|
102
|
-
.map((error) => `- ${error.code}: ${error.message}`)
|
|
103
|
-
.join("\n")}`,
|
|
104
|
-
uploadSessionResponse,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
throw new CloudflareApiError(
|
|
108
|
-
`Failed to start assets upload session: ${uploadSessionResponse.statusText}`,
|
|
109
|
-
uploadSessionResponse,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
65
|
// If there are no buckets, assets are already uploaded or empty
|
|
114
|
-
if (!
|
|
66
|
+
if (!uploadSession.buckets || uploadSession.buckets.length === 0) {
|
|
115
67
|
return {
|
|
116
|
-
completionToken:
|
|
68
|
+
completionToken: uploadSession.jwt,
|
|
117
69
|
assetConfig: processedConfig,
|
|
118
70
|
};
|
|
119
71
|
}
|
|
120
72
|
|
|
121
73
|
// Upload the files in batches as specified by the API
|
|
122
|
-
let completionToken =
|
|
123
|
-
const buckets =
|
|
74
|
+
let completionToken = uploadSession.jwt;
|
|
75
|
+
const buckets = uploadSession.buckets;
|
|
124
76
|
|
|
125
77
|
const queue = new AsyncQueue(3);
|
|
126
78
|
await Promise.all(
|
|
127
79
|
buckets.map((bucket) =>
|
|
128
80
|
queue.add(async () => {
|
|
129
|
-
const jwt = await
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
filePathsByHash,
|
|
81
|
+
const jwt = await withExponentialBackoff(
|
|
82
|
+
async () =>
|
|
83
|
+
await uploadBucket(api, uploadSession.jwt, bucket, filesByHash),
|
|
84
|
+
() => true,
|
|
134
85
|
);
|
|
135
86
|
if (jwt) {
|
|
136
87
|
completionToken = jwt;
|
|
@@ -183,68 +134,55 @@ export function createAssetConfig(config?: AssetsConfig): AssetsConfig {
|
|
|
183
134
|
* Prepares the asset manifest for the assets upload session
|
|
184
135
|
*
|
|
185
136
|
* @param assets Assets resource containing files to upload
|
|
186
|
-
* @returns Asset manifest and
|
|
137
|
+
* @returns Asset manifest and files by hash
|
|
187
138
|
*/
|
|
188
139
|
export async function prepareAssetManifest(assets: Assets) {
|
|
189
140
|
const manifest: Record<string, FileMetadata> = {};
|
|
190
|
-
const
|
|
141
|
+
const filesByHash = new Map<string, AssetFile>();
|
|
191
142
|
await Promise.all(
|
|
192
143
|
assets.files.map(async (file) => {
|
|
193
144
|
const { hash, size } = await calculateFileMetadata(file.filePath);
|
|
194
145
|
const key = file.path.startsWith("/") ? file.path : `/${file.path}`;
|
|
195
146
|
manifest[key] = { hash, size };
|
|
196
|
-
|
|
147
|
+
filesByHash.set(hash, file);
|
|
197
148
|
}),
|
|
198
149
|
);
|
|
199
|
-
return { manifest,
|
|
150
|
+
return { manifest, filesByHash };
|
|
200
151
|
}
|
|
201
152
|
|
|
202
153
|
async function uploadBucket(
|
|
203
154
|
api: CloudflareApi,
|
|
204
155
|
jwt: string,
|
|
205
156
|
bucket: string[],
|
|
206
|
-
|
|
157
|
+
filesByHash: Map<string, AssetFile>,
|
|
207
158
|
) {
|
|
208
159
|
const formData = new FormData();
|
|
209
160
|
await Promise.all(
|
|
210
161
|
bucket.map(async (fileHash) => {
|
|
211
|
-
const
|
|
212
|
-
if (!
|
|
162
|
+
const file = filesByHash.get(fileHash);
|
|
163
|
+
if (!file) {
|
|
213
164
|
throw new Error(`Could not find file with hash ${fileHash}`);
|
|
214
165
|
}
|
|
215
|
-
const fileContent = await fs.readFile(filePath);
|
|
166
|
+
const fileContent = await fs.readFile(file.filePath);
|
|
216
167
|
const blob = new Blob([fileContent.toString("base64")], {
|
|
217
|
-
type:
|
|
168
|
+
type: file.contentType,
|
|
218
169
|
});
|
|
219
170
|
formData.append(fileHash, blob, fileHash);
|
|
220
171
|
}),
|
|
221
172
|
);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
173
|
+
const uploadResult = await extractCloudflareResult<{ jwt?: string }>(
|
|
174
|
+
"upload asset files",
|
|
175
|
+
api.post(
|
|
176
|
+
`/accounts/${api.accountId}/workers/assets/upload?base64=true`,
|
|
177
|
+
formData,
|
|
178
|
+
{
|
|
179
|
+
headers: {
|
|
180
|
+
Authorization: `Bearer ${jwt}`,
|
|
181
|
+
},
|
|
229
182
|
},
|
|
230
|
-
|
|
183
|
+
),
|
|
231
184
|
);
|
|
232
|
-
|
|
233
|
-
if (!uploadResponse.ok) {
|
|
234
|
-
throw new Error(
|
|
235
|
-
`Failed to upload asset files: ${uploadResponse.status} ${uploadResponse.statusText}`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const uploadData = (await uploadResponse.json()) as UploadResponse;
|
|
240
|
-
if (!uploadData.success) {
|
|
241
|
-
const error = uploadData.errors[0];
|
|
242
|
-
const message = error.message;
|
|
243
|
-
const code = error.code;
|
|
244
|
-
throw new Error(`Failed to upload asset files: ${message} (Code: ${code})`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return uploadData.result.jwt;
|
|
185
|
+
return uploadResult.jwt;
|
|
248
186
|
}
|
|
249
187
|
|
|
250
188
|
/**
|
|
@@ -353,8 +353,8 @@ export interface WranglerJsonSpec {
|
|
|
353
353
|
* Queue bindings
|
|
354
354
|
*/
|
|
355
355
|
queues?: {
|
|
356
|
-
producers
|
|
357
|
-
consumers
|
|
356
|
+
producers?: { queue: string; binding: string }[];
|
|
357
|
+
consumers?: {
|
|
358
358
|
queue: string;
|
|
359
359
|
max_batch_size?: number;
|
|
360
360
|
max_concurrency?: number;
|
|
@@ -845,7 +845,10 @@ function processBindings(
|
|
|
845
845
|
}
|
|
846
846
|
|
|
847
847
|
if (queues.consumers.length > 0) {
|
|
848
|
-
spec.queues = queues;
|
|
848
|
+
(spec.queues ??= {}).consumers = queues.consumers;
|
|
849
|
+
}
|
|
850
|
+
if (queues.producers.length > 0) {
|
|
851
|
+
(spec.queues ??= {}).producers = queues.producers;
|
|
849
852
|
}
|
|
850
853
|
|
|
851
854
|
if (vectorizeIndexes.length > 0) {
|
|
@@ -4,7 +4,8 @@ import type { PlanetScaleProps } from "./api/client.gen.ts";
|
|
|
4
4
|
import { PlanetScaleClient } from "./api/client.gen.ts";
|
|
5
5
|
import {
|
|
6
6
|
ensureProductionBranchClusterSize,
|
|
7
|
-
|
|
7
|
+
sanitizeClusterSize,
|
|
8
|
+
waitForBranchReady,
|
|
8
9
|
type PlanetScaleClusterSize,
|
|
9
10
|
} from "./utils.ts";
|
|
10
11
|
|
|
@@ -159,6 +160,11 @@ export const Branch = Resource(
|
|
|
159
160
|
|
|
160
161
|
const branchName =
|
|
161
162
|
props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
|
|
163
|
+
const parentBranchName = !props.parentBranch
|
|
164
|
+
? "main"
|
|
165
|
+
: typeof props.parentBranch === "string"
|
|
166
|
+
? props.parentBranch
|
|
167
|
+
: props.parentBranch.name;
|
|
162
168
|
|
|
163
169
|
if (this.phase === "update" && this.output.name !== branchName) {
|
|
164
170
|
// TODO(sam): maybe we don't need to replace? just branch again? or rename?
|
|
@@ -188,14 +194,8 @@ export const Branch = Resource(
|
|
|
188
194
|
return this.destroy();
|
|
189
195
|
}
|
|
190
196
|
|
|
191
|
-
const parentBranchName = !props.parentBranch
|
|
192
|
-
? "main"
|
|
193
|
-
: typeof props.parentBranch === "string"
|
|
194
|
-
? props.parentBranch
|
|
195
|
-
: props.parentBranch.name;
|
|
196
|
-
|
|
197
197
|
if (typeof props.parentBranch !== "string" && props.parentBranch) {
|
|
198
|
-
await
|
|
198
|
+
await waitForBranchReady(
|
|
199
199
|
api,
|
|
200
200
|
props.organizationId,
|
|
201
201
|
props.databaseName,
|
|
@@ -262,17 +262,28 @@ export const Branch = Resource(
|
|
|
262
262
|
});
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
265
|
+
const clusterSize = props.clusterSize
|
|
266
|
+
? sanitizeClusterSize({
|
|
267
|
+
size: props.clusterSize,
|
|
268
|
+
kind: data.kind,
|
|
269
|
+
arch: data.cluster_architecture === "aarch64" ? "arm" : "x86",
|
|
270
|
+
region: data.region.slug,
|
|
271
|
+
})
|
|
272
|
+
: undefined;
|
|
273
|
+
if (clusterSize && data.cluster_name !== clusterSize) {
|
|
274
|
+
if (!props.isProduction) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Cannot change cluster size of non-production branch ${branchName}`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
await ensureProductionBranchClusterSize(
|
|
280
|
+
api,
|
|
281
|
+
props.organizationId,
|
|
282
|
+
props.databaseName,
|
|
283
|
+
branchName,
|
|
284
|
+
data.kind,
|
|
285
|
+
clusterSize,
|
|
286
|
+
);
|
|
276
287
|
}
|
|
277
288
|
|
|
278
289
|
return this({
|
|
@@ -284,7 +295,21 @@ export const Branch = Resource(
|
|
|
284
295
|
htmlUrl: data.html_url,
|
|
285
296
|
});
|
|
286
297
|
}
|
|
287
|
-
|
|
298
|
+
let clusterSize: string | undefined;
|
|
299
|
+
const parent = await waitForBranchReady(
|
|
300
|
+
api,
|
|
301
|
+
props.organizationId,
|
|
302
|
+
props.databaseName,
|
|
303
|
+
parentBranchName,
|
|
304
|
+
);
|
|
305
|
+
if (props.clusterSize) {
|
|
306
|
+
clusterSize = sanitizeClusterSize({
|
|
307
|
+
size: props.clusterSize,
|
|
308
|
+
kind: parent.kind,
|
|
309
|
+
arch: parent.cluster_architecture === "aarch64" ? "arm" : "x86",
|
|
310
|
+
region: parent.region.slug,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
288
313
|
|
|
289
314
|
// Branch doesn't exist, create it
|
|
290
315
|
const data = await api.organizations.databases.branches.post({
|
|
@@ -297,14 +322,15 @@ export const Branch = Resource(
|
|
|
297
322
|
parent_branch: parentBranchName,
|
|
298
323
|
backup_id: props.backupId,
|
|
299
324
|
seed_data: props.seedData,
|
|
300
|
-
|
|
325
|
+
// This is ignored unless props.backupId is provided
|
|
326
|
+
cluster_size: clusterSize,
|
|
301
327
|
},
|
|
302
328
|
});
|
|
303
329
|
|
|
304
330
|
// Handle safe migrations if specified
|
|
305
331
|
if (props.safeMigrations !== undefined) {
|
|
306
|
-
// We can't change the migrations mode if the
|
|
307
|
-
await
|
|
332
|
+
// We can't change the migrations mode if the branch is not ready
|
|
333
|
+
await waitForBranchReady(
|
|
308
334
|
api,
|
|
309
335
|
props.organizationId,
|
|
310
336
|
props.databaseName,
|
|
@@ -322,15 +348,14 @@ export const Branch = Resource(
|
|
|
322
348
|
}
|
|
323
349
|
|
|
324
350
|
// Handle cluster size update if specified
|
|
325
|
-
if (
|
|
326
|
-
// Wait for database to be ready before modifying cluster size
|
|
351
|
+
if (clusterSize) {
|
|
327
352
|
await ensureProductionBranchClusterSize(
|
|
328
353
|
api,
|
|
329
354
|
props.organizationId,
|
|
330
355
|
props.databaseName,
|
|
331
356
|
branchName,
|
|
332
|
-
|
|
333
|
-
|
|
357
|
+
data.kind,
|
|
358
|
+
clusterSize,
|
|
334
359
|
);
|
|
335
360
|
}
|
|
336
361
|
|
|
@@ -5,13 +5,11 @@ import { PlanetScaleClient } from "./api/client.gen.ts";
|
|
|
5
5
|
import {
|
|
6
6
|
ensureProductionBranchClusterSize,
|
|
7
7
|
type PlanetScaleClusterSize,
|
|
8
|
+
sanitizeClusterSize,
|
|
8
9
|
waitForDatabaseReady,
|
|
9
10
|
} from "./utils.ts";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
* Properties for creating or updating a PlanetScale Database
|
|
13
|
-
*/
|
|
14
|
-
export interface DatabaseProps extends PlanetScaleProps {
|
|
12
|
+
interface BaseDatabaseProps extends PlanetScaleProps {
|
|
15
13
|
/**
|
|
16
14
|
* The name of the database
|
|
17
15
|
*
|
|
@@ -88,54 +86,79 @@ export interface DatabaseProps extends PlanetScaleProps {
|
|
|
88
86
|
* The database cluster size (required)
|
|
89
87
|
*/
|
|
90
88
|
clusterSize: PlanetScaleClusterSize;
|
|
91
|
-
}
|
|
92
89
|
|
|
93
|
-
/**
|
|
94
|
-
* Represents a PlanetScale Database
|
|
95
|
-
*/
|
|
96
|
-
export interface Database
|
|
97
|
-
extends Resource<"planetscale::Database">,
|
|
98
|
-
DatabaseProps {
|
|
99
90
|
/**
|
|
100
|
-
* The
|
|
91
|
+
* The engine kind for the database
|
|
92
|
+
* @default "mysql"
|
|
101
93
|
*/
|
|
102
|
-
|
|
94
|
+
kind?: "mysql" | "postgresql";
|
|
103
95
|
|
|
104
96
|
/**
|
|
105
|
-
* The
|
|
97
|
+
* The CPU architecture for the database. Only available for PostgreSQL databases.
|
|
106
98
|
*/
|
|
107
|
-
|
|
99
|
+
arch?: "x86" | "arm";
|
|
100
|
+
}
|
|
108
101
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Properties for creating or updating a PlanetScale Database
|
|
104
|
+
*/
|
|
105
|
+
export type DatabaseProps = BaseDatabaseProps &
|
|
106
|
+
(
|
|
107
|
+
| {
|
|
108
|
+
kind?: "mysql";
|
|
109
|
+
arch?: undefined;
|
|
110
|
+
}
|
|
111
|
+
| {
|
|
112
|
+
kind: "postgresql";
|
|
113
|
+
arch?: "x86" | "arm";
|
|
114
|
+
}
|
|
115
|
+
);
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Represents a PlanetScale Database
|
|
119
|
+
*/
|
|
120
|
+
export type Database = Resource<"planetscale::Database"> &
|
|
121
|
+
DatabaseProps & {
|
|
122
|
+
/**
|
|
123
|
+
* The unique identifier of the database
|
|
124
|
+
*/
|
|
125
|
+
id: string;
|
|
118
126
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The name of the database
|
|
129
|
+
*/
|
|
130
|
+
name: string;
|
|
123
131
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
/**
|
|
133
|
+
* The current state of the database
|
|
134
|
+
*/
|
|
135
|
+
state: string;
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
/**
|
|
138
|
+
* The default branch name
|
|
139
|
+
*/
|
|
140
|
+
defaultBranch: string;
|
|
133
141
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
/**
|
|
143
|
+
* The plan type
|
|
144
|
+
*/
|
|
145
|
+
plan: string;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Time at which the database was created
|
|
149
|
+
*/
|
|
150
|
+
createdAt: string;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Time at which the database was last updated
|
|
154
|
+
*/
|
|
155
|
+
updatedAt: string;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* HTML URL to access the database
|
|
159
|
+
*/
|
|
160
|
+
htmlUrl: string;
|
|
161
|
+
};
|
|
139
162
|
|
|
140
163
|
/**
|
|
141
164
|
* Create, manage and delete PlanetScale databases
|
|
@@ -182,6 +205,12 @@ export const Database = Resource(
|
|
|
182
205
|
|
|
183
206
|
const databaseName =
|
|
184
207
|
props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
|
|
208
|
+
const clusterSize = sanitizeClusterSize({
|
|
209
|
+
size: props.clusterSize,
|
|
210
|
+
kind: props.kind,
|
|
211
|
+
arch: props.arch,
|
|
212
|
+
region: props.region?.slug,
|
|
213
|
+
});
|
|
185
214
|
|
|
186
215
|
if (this.phase === "update" && this.output.name !== databaseName) {
|
|
187
216
|
await api.organizations.databases.patch({
|
|
@@ -278,8 +307,8 @@ export const Database = Resource(
|
|
|
278
307
|
props.organizationId,
|
|
279
308
|
databaseName,
|
|
280
309
|
props.defaultBranch || "main",
|
|
281
|
-
|
|
282
|
-
|
|
310
|
+
updateResponse.kind,
|
|
311
|
+
clusterSize,
|
|
283
312
|
);
|
|
284
313
|
|
|
285
314
|
return this({
|
|
@@ -307,8 +336,8 @@ export const Database = Resource(
|
|
|
307
336
|
body: {
|
|
308
337
|
name: databaseName,
|
|
309
338
|
region: props.region?.slug,
|
|
310
|
-
kind:
|
|
311
|
-
cluster_size:
|
|
339
|
+
kind: props.kind,
|
|
340
|
+
cluster_size: clusterSize,
|
|
312
341
|
},
|
|
313
342
|
});
|
|
314
343
|
|
|
@@ -362,8 +391,8 @@ export const Database = Resource(
|
|
|
362
391
|
props.organizationId,
|
|
363
392
|
databaseName,
|
|
364
393
|
props.defaultBranch || "main",
|
|
365
|
-
|
|
366
|
-
|
|
394
|
+
data.kind,
|
|
395
|
+
clusterSize,
|
|
367
396
|
);
|
|
368
397
|
|
|
369
398
|
// Update database to use new branch as default
|