wrangler 2.4.1 → 2.4.3
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/package.json +1 -1
- package/src/__tests__/d1.test.ts +8 -2
- package/src/__tests__/queues.test.ts +64 -1
- package/src/__tests__/type-generation.test.ts +3 -2
- package/src/bundle.ts +113 -98
- package/src/config/validation.ts +1 -1
- package/src/d1/migrations/apply.tsx +183 -0
- package/src/d1/migrations/create.tsx +77 -0
- package/src/d1/migrations/helpers.ts +145 -0
- package/src/d1/migrations/index.tsx +3 -0
- package/src/d1/migrations/list.tsx +79 -0
- package/src/d1/utils.ts +1 -1
- package/src/dev/dev.tsx +1 -0
- package/src/dev/local.tsx +4 -2
- package/src/dev/start-server.ts +8 -1
- package/src/dev/use-esbuild.ts +4 -0
- package/src/index.tsx +7 -2
- package/src/pages/functions/buildPlugin.ts +1 -0
- package/src/pages/functions/buildWorker.ts +1 -0
- package/src/publish/publish.ts +1 -0
- package/src/queues/cli/commands/index.ts +3 -0
- package/src/queues/utils.ts +18 -0
- package/src/type-generation.ts +19 -19
- package/src/user/user.tsx +11 -5
- package/templates/middleware/loader-sw.ts +45 -31
- package/templates/middleware/middleware-miniflare3-json-error.ts +20 -0
- package/wrangler-dist/cli.js +1158 -1083
- package/src/d1/migrations.tsx +0 -446
package/package.json
CHANGED
package/src/__tests__/d1.test.ts
CHANGED
|
@@ -34,7 +34,10 @@ describe("d1", () => {
|
|
|
34
34
|
-h, --help Show help [boolean]
|
|
35
35
|
-v, --version Show version number [boolean]
|
|
36
36
|
|
|
37
|
-
🚧
|
|
37
|
+
🚧 D1 is currently in open alpha and is not recommended for production data and traffic.
|
|
38
|
+
Please report any bugs to https://github.com/cloudflare/wrangler2/issues/new/choose.
|
|
39
|
+
To request features, visit https://community.cloudflare.com/c/developers/d1.
|
|
40
|
+
To give feedback, visit https://discord.gg/cloudflaredev"
|
|
38
41
|
`);
|
|
39
42
|
});
|
|
40
43
|
|
|
@@ -68,7 +71,10 @@ describe("d1", () => {
|
|
|
68
71
|
-h, --help Show help [boolean]
|
|
69
72
|
-v, --version Show version number [boolean]
|
|
70
73
|
|
|
71
|
-
🚧
|
|
74
|
+
🚧 D1 is currently in open alpha and is not recommended for production data and traffic.
|
|
75
|
+
Please report any bugs to https://github.com/cloudflare/wrangler2/issues/new/choose.
|
|
76
|
+
To request features, visit https://community.cloudflare.com/c/developers/d1.
|
|
77
|
+
To give feedback, visit https://discord.gg/cloudflaredev"
|
|
72
78
|
`);
|
|
73
79
|
});
|
|
74
80
|
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { type QueueResponse, type PostConsumerBody } from "../queues/client";
|
|
2
2
|
import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createFetchResult,
|
|
5
|
+
setMockRawResponse,
|
|
6
|
+
setMockResponse,
|
|
7
|
+
unsetAllMocks,
|
|
8
|
+
} from "./helpers/mock-cfetch";
|
|
4
9
|
import { mockConsoleMethods } from "./helpers/mock-console";
|
|
5
10
|
import { runInTempDir } from "./helpers/run-in-tmp";
|
|
6
11
|
import { runWrangler } from "./helpers/run-wrangler";
|
|
@@ -157,6 +162,35 @@ describe("wrangler", () => {
|
|
|
157
162
|
`);
|
|
158
163
|
expect(requests.count).toEqual(1);
|
|
159
164
|
});
|
|
165
|
+
|
|
166
|
+
it("should show link to dash when not enabled", async () => {
|
|
167
|
+
const queueName = "testQueue";
|
|
168
|
+
setMockRawResponse(
|
|
169
|
+
"/accounts/:accountId/workers/queues",
|
|
170
|
+
([_url, accountId]) => {
|
|
171
|
+
expect(accountId).toEqual("some-account-id");
|
|
172
|
+
return createFetchResult(null, false, [
|
|
173
|
+
{ message: "workers.api.error.unauthorized", code: 10023 },
|
|
174
|
+
]);
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
await expect(
|
|
178
|
+
runWrangler(`queues create ${queueName}`)
|
|
179
|
+
).rejects.toThrowError();
|
|
180
|
+
expect(std.out).toMatchInlineSnapshot(`
|
|
181
|
+
"Creating queue testQueue.
|
|
182
|
+
Queues is not currently enabled on this account. Go to https://dash.cloudflare.com/some-account-id/workers/queues to enable it.
|
|
183
|
+
|
|
184
|
+
[31mX [41;31m[[41;97mERROR[41;31m][0m [1mA request to the Cloudflare API (/accounts/some-account-id/workers/queues) failed.[0m
|
|
185
|
+
|
|
186
|
+
workers.api.error.unauthorized [code: 10023]
|
|
187
|
+
|
|
188
|
+
If you think this is a bug, please open an issue at:
|
|
189
|
+
[4mhttps://github.com/cloudflare/wrangler2/issues/new/choose[0m
|
|
190
|
+
|
|
191
|
+
"
|
|
192
|
+
`);
|
|
193
|
+
});
|
|
160
194
|
});
|
|
161
195
|
|
|
162
196
|
describe("delete", () => {
|
|
@@ -309,6 +343,35 @@ describe("wrangler", () => {
|
|
|
309
343
|
Added consumer to queue testQueue."
|
|
310
344
|
`);
|
|
311
345
|
});
|
|
346
|
+
|
|
347
|
+
it("should show link to dash when not enabled", async () => {
|
|
348
|
+
const queueName = "testQueue";
|
|
349
|
+
setMockRawResponse(
|
|
350
|
+
`/accounts/:accountId/workers/queues/${queueName}/consumers`,
|
|
351
|
+
([_url, accountId]) => {
|
|
352
|
+
expect(accountId).toEqual("some-account-id");
|
|
353
|
+
return createFetchResult(null, false, [
|
|
354
|
+
{ message: "workers.api.error.unauthorized", code: 10023 },
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
await expect(
|
|
359
|
+
runWrangler(`queues consumer add ${queueName} testScript`)
|
|
360
|
+
).rejects.toThrowError();
|
|
361
|
+
expect(std.out).toMatchInlineSnapshot(`
|
|
362
|
+
"Adding consumer to queue testQueue.
|
|
363
|
+
Queues is not currently enabled on this account. Go to https://dash.cloudflare.com/some-account-id/workers/queues to enable it.
|
|
364
|
+
|
|
365
|
+
[31mX [41;31m[[41;97mERROR[41;31m][0m [1mA request to the Cloudflare API (/accounts/some-account-id/workers/queues/testQueue/consumers) failed.[0m
|
|
366
|
+
|
|
367
|
+
workers.api.error.unauthorized [code: 10023]
|
|
368
|
+
|
|
369
|
+
If you think this is a bug, please open an issue at:
|
|
370
|
+
[4mhttps://github.com/cloudflare/wrangler2/issues/new/choose[0m
|
|
371
|
+
|
|
372
|
+
"
|
|
373
|
+
`);
|
|
374
|
+
});
|
|
312
375
|
});
|
|
313
376
|
|
|
314
377
|
describe("delete", () => {
|
|
@@ -216,8 +216,9 @@ describe("generateTypes()", () => {
|
|
|
216
216
|
|
|
217
217
|
await runWrangler("types");
|
|
218
218
|
expect(std.out).toMatchInlineSnapshot(`
|
|
219
|
-
"
|
|
220
|
-
|
|
219
|
+
"export {};
|
|
220
|
+
declare global {
|
|
221
|
+
const testing_unsafe: any;
|
|
221
222
|
}
|
|
222
223
|
"
|
|
223
224
|
`);
|
package/src/bundle.ts
CHANGED
|
@@ -28,6 +28,19 @@ type StaticAssetsConfig =
|
|
|
28
28
|
})
|
|
29
29
|
| undefined;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* When applying the middleware facade for service workers, we need to inject
|
|
33
|
+
* some code at the top of the final output bundle. Applying an inject too early
|
|
34
|
+
* will allow esbuild to reorder the code. Additionally, we need to make sure
|
|
35
|
+
* user code is bundled in the final esbuild step with `watch` correctly
|
|
36
|
+
* configured, so code changes are detected.
|
|
37
|
+
*
|
|
38
|
+
* This type is used as the return type for the `MiddlewareFn` type representing
|
|
39
|
+
* a facade-applying function. Returned injects should be injected with the
|
|
40
|
+
* final esbuild step.
|
|
41
|
+
*/
|
|
42
|
+
type EntryWithInject = Entry & { inject?: string[] };
|
|
43
|
+
|
|
31
44
|
/**
|
|
32
45
|
* RegExp matching against esbuild's error text when it is unable to resolve
|
|
33
46
|
* a Node built-in module. If we detect this when node_compat is disabled,
|
|
@@ -97,6 +110,7 @@ export async function bundleWorker(
|
|
|
97
110
|
targetConsumer: "dev" | "publish";
|
|
98
111
|
local: boolean;
|
|
99
112
|
testScheduled?: boolean;
|
|
113
|
+
experimentalLocal?: boolean;
|
|
100
114
|
inject?: string[];
|
|
101
115
|
loader?: Record<string, string>;
|
|
102
116
|
sourcemap?: esbuild.CommonOptions["sourcemap"];
|
|
@@ -124,6 +138,7 @@ export async function bundleWorker(
|
|
|
124
138
|
firstPartyWorkerDevFacade,
|
|
125
139
|
targetConsumer,
|
|
126
140
|
testScheduled,
|
|
141
|
+
experimentalLocal,
|
|
127
142
|
inject: injectOption,
|
|
128
143
|
loader,
|
|
129
144
|
sourcemap,
|
|
@@ -197,8 +212,29 @@ export async function bundleWorker(
|
|
|
197
212
|
path: "templates/middleware/middleware-scheduled.ts",
|
|
198
213
|
});
|
|
199
214
|
}
|
|
215
|
+
if (experimentalLocal) {
|
|
216
|
+
// In Miniflare 3, we bind the user's worker as a service binding in a
|
|
217
|
+
// special entry worker that handles things like injecting `Request.cf`,
|
|
218
|
+
// live-reload, and the pretty-error page.
|
|
219
|
+
//
|
|
220
|
+
// Unfortunately, due to a bug in `workerd`, errors thrown asynchronously by
|
|
221
|
+
// native APIs don't have `stack`s. This means Miniflare can't extract the
|
|
222
|
+
// `stack` trace from dispatching to the user worker service binding by
|
|
223
|
+
// `try/catch`.
|
|
224
|
+
//
|
|
225
|
+
// As a stop-gap solution, if the `MF-Experimental-Error-Stack` header is
|
|
226
|
+
// truthy on responses, the body will be interpreted as a JSON-error of the
|
|
227
|
+
// form `{ message?: string, name?: string, stack?: string }`.
|
|
228
|
+
//
|
|
229
|
+
// This middleware wraps the user's worker in a `try/catch`, and rewrites
|
|
230
|
+
// errors in this format so a pretty-error page can be shown.
|
|
231
|
+
middlewareToLoad.push({
|
|
232
|
+
path: "templates/middleware/middleware-miniflare3-json-error.ts",
|
|
233
|
+
dev: true,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
200
236
|
|
|
201
|
-
type MiddlewareFn = (
|
|
237
|
+
type MiddlewareFn = (currentEntry: Entry) => Promise<EntryWithInject>;
|
|
202
238
|
const middleware: (false | undefined | MiddlewareFn)[] = [
|
|
203
239
|
// serve static assets
|
|
204
240
|
serveAssetsFromWorker &&
|
|
@@ -259,23 +295,22 @@ export async function bundleWorker(
|
|
|
259
295
|
(m) =>
|
|
260
296
|
(targetConsumer === "dev" && m.dev !== false) ||
|
|
261
297
|
(m.publish && targetConsumer === "publish")
|
|
262
|
-
)
|
|
263
|
-
moduleCollector.plugin
|
|
298
|
+
)
|
|
264
299
|
);
|
|
265
300
|
}),
|
|
266
301
|
].filter(Boolean);
|
|
267
302
|
|
|
268
|
-
|
|
303
|
+
const inject: string[] = injectOption ?? [];
|
|
304
|
+
if (checkFetch) inject.push(checkedFetchFileToInject);
|
|
269
305
|
|
|
306
|
+
let inputEntry: EntryWithInject = entry;
|
|
270
307
|
for (const middlewareFn of middleware as MiddlewareFn[]) {
|
|
271
308
|
inputEntry = await middlewareFn(inputEntry);
|
|
309
|
+
if (inputEntry.inject !== undefined) inject.push(...inputEntry.inject);
|
|
272
310
|
}
|
|
273
311
|
|
|
274
312
|
// At this point, inputEntry points to the entry point we want to build.
|
|
275
313
|
|
|
276
|
-
const inject: string[] = injectOption ?? [];
|
|
277
|
-
if (checkFetch) inject.push(checkedFetchFileToInject);
|
|
278
|
-
|
|
279
314
|
const buildOptions: esbuild.BuildOptions & { metafile: true } = {
|
|
280
315
|
entryPoints: [inputEntry.file],
|
|
281
316
|
bundle: true,
|
|
@@ -315,11 +350,7 @@ export async function bundleWorker(
|
|
|
315
350
|
...(loader || {}),
|
|
316
351
|
},
|
|
317
352
|
plugins: [
|
|
318
|
-
|
|
319
|
-
// so we only run here for modules or with no middleware to load
|
|
320
|
-
...(entry.format === "modules" || middlewareToLoad.length === 0
|
|
321
|
-
? [moduleCollector.plugin]
|
|
322
|
-
: []),
|
|
353
|
+
moduleCollector.plugin,
|
|
323
354
|
...(nodeCompat
|
|
324
355
|
? [NodeGlobalsPolyfills({ buffer: true }), NodeModulesPolyfills()]
|
|
325
356
|
: []),
|
|
@@ -456,14 +487,16 @@ interface MiddlewareLoader {
|
|
|
456
487
|
async function applyMiddlewareLoaderFacade(
|
|
457
488
|
entry: Entry,
|
|
458
489
|
tmpDirPath: string,
|
|
459
|
-
middleware: MiddlewareLoader[]
|
|
460
|
-
|
|
461
|
-
): Promise<Entry> {
|
|
490
|
+
middleware: MiddlewareLoader[] // a list of paths to middleware files
|
|
491
|
+
): Promise<EntryWithInject> {
|
|
462
492
|
// Firstly we need to insert the middleware array into the project,
|
|
463
493
|
// and then we load the middleware - this insertion and loading is
|
|
464
494
|
// different for each format.
|
|
465
495
|
|
|
466
|
-
//
|
|
496
|
+
// Make sure we resolve all files relative to the actual temporary directory,
|
|
497
|
+
// otherwise we'll have issues with source maps
|
|
498
|
+
tmpDirPath = fs.realpathSync(tmpDirPath);
|
|
499
|
+
|
|
467
500
|
const targetPathInsertion = path.join(
|
|
468
501
|
tmpDirPath,
|
|
469
502
|
"middleware-insertion.entry.js"
|
|
@@ -506,7 +539,7 @@ async function applyMiddlewareLoaderFacade(
|
|
|
506
539
|
);
|
|
507
540
|
|
|
508
541
|
await esbuild.build({
|
|
509
|
-
entryPoints: [
|
|
542
|
+
entryPoints: [dynamicFacadePath],
|
|
510
543
|
bundle: true,
|
|
511
544
|
sourcemap: true,
|
|
512
545
|
format: "esm",
|
|
@@ -523,98 +556,80 @@ async function applyMiddlewareLoaderFacade(
|
|
|
523
556
|
],
|
|
524
557
|
outfile: targetPathInsertion,
|
|
525
558
|
});
|
|
526
|
-
} else {
|
|
527
|
-
// We handle service workers slightly differently as we have to overwrite
|
|
528
|
-
// the event listeners and reimplement them
|
|
529
559
|
|
|
560
|
+
let targetPathLoader = path.join(tmpDirPath, path.basename(entry.file));
|
|
561
|
+
if (path.extname(entry.file) === "") targetPathLoader += ".js";
|
|
562
|
+
const loaderPath = path.resolve(
|
|
563
|
+
getBasePath(),
|
|
564
|
+
"templates/middleware/loader-modules.ts"
|
|
565
|
+
);
|
|
530
566
|
await esbuild.build({
|
|
531
|
-
entryPoints: [
|
|
567
|
+
entryPoints: [loaderPath],
|
|
532
568
|
bundle: true,
|
|
533
569
|
sourcemap: true,
|
|
534
|
-
define: {
|
|
535
|
-
"process.env.NODE_ENV": `"${process.env["NODE_ENV" + ""]}"`,
|
|
536
|
-
},
|
|
537
570
|
format: "esm",
|
|
538
|
-
|
|
539
|
-
|
|
571
|
+
plugins: [
|
|
572
|
+
esbuildAliasExternalPlugin({
|
|
573
|
+
__ENTRY_POINT__: targetPathInsertion,
|
|
574
|
+
"./common": path.resolve(
|
|
575
|
+
getBasePath(),
|
|
576
|
+
"templates/middleware/common.ts"
|
|
577
|
+
),
|
|
578
|
+
}),
|
|
579
|
+
],
|
|
580
|
+
outfile: targetPathLoader,
|
|
540
581
|
});
|
|
541
|
-
|
|
582
|
+
return {
|
|
583
|
+
...entry,
|
|
584
|
+
file: targetPathLoader,
|
|
585
|
+
};
|
|
586
|
+
} else {
|
|
542
587
|
const imports = middlewareIdentifiers
|
|
543
|
-
.map(
|
|
544
|
-
(m, i) =>
|
|
545
|
-
`import ${m} from "${toUrlPath(
|
|
546
|
-
path.resolve(getBasePath(), middleware[i].path)
|
|
547
|
-
)}";`
|
|
548
|
-
)
|
|
588
|
+
.map((m) => `import ${m} from "${m}";`)
|
|
549
589
|
.join("\n");
|
|
550
|
-
|
|
551
|
-
// We add the new modules with imports and then register using the
|
|
552
|
-
// addMiddleware function (which gets rewritten in the next build step)
|
|
553
|
-
|
|
554
|
-
// We choose to run middleware inserted in wrangler before user inserted
|
|
555
|
-
// middleware in the stack
|
|
556
|
-
// To do this, we either need to execute the addMiddleware function first
|
|
557
|
-
// before any user middleware, or use a separate handling function.
|
|
558
|
-
// We choose to do the latter as to prepend, we would have to load the entire
|
|
559
|
-
// script into memory as a prepend function doesn't exist or work in the same
|
|
560
|
-
// way that an append function does.
|
|
561
|
-
|
|
562
|
-
fs.copyFileSync(targetPathInsertion, dynamicFacadePath);
|
|
563
|
-
fs.appendFileSync(
|
|
564
|
-
dynamicFacadePath,
|
|
565
|
-
`
|
|
590
|
+
const contents = `import { __facade_registerInternal__ } from "__LOADER__";
|
|
566
591
|
${imports}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// STEP 2: Load the middleware
|
|
573
|
-
// We want to get the filename of the orginal entry point
|
|
574
|
-
let targetPathLoader = path.join(tmpDirPath, path.basename(entry.file));
|
|
575
|
-
if (path.extname(entry.file) === "") targetPathLoader += ".js";
|
|
576
|
-
|
|
577
|
-
const loaderPath =
|
|
578
|
-
entry.format === "modules"
|
|
579
|
-
? path.resolve(getBasePath(), "templates/middleware/loader-modules.ts")
|
|
580
|
-
: dynamicFacadePath;
|
|
592
|
+
__facade_registerInternal__([${middlewareIdentifiers.join(",")}]);`;
|
|
593
|
+
fs.writeFileSync(dynamicFacadePath, contents);
|
|
581
594
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
addEventListener: "__facade_addEventListener__",
|
|
594
|
-
removeEventListener: "__facade_removeEventListener__",
|
|
595
|
-
dispatchEvent: "__facade_dispatchEvent__",
|
|
596
|
-
addMiddleware: "__facade_register__",
|
|
597
|
-
addMiddlewareInternal: "__facade_registerInternal__",
|
|
598
|
-
},
|
|
599
|
-
}
|
|
600
|
-
: {
|
|
601
|
-
plugins: [
|
|
602
|
-
esbuildAliasExternalPlugin({
|
|
603
|
-
__ENTRY_POINT__: targetPathInsertion,
|
|
604
|
-
"./common": path.resolve(
|
|
595
|
+
await esbuild.build({
|
|
596
|
+
entryPoints: [dynamicFacadePath],
|
|
597
|
+
bundle: true,
|
|
598
|
+
sourcemap: true,
|
|
599
|
+
format: "iife",
|
|
600
|
+
plugins: [
|
|
601
|
+
{
|
|
602
|
+
name: "dynamic-facade-imports",
|
|
603
|
+
setup(build) {
|
|
604
|
+
build.onResolve({ filter: /^__LOADER__$/ }, () => {
|
|
605
|
+
const loaderPath = path.resolve(
|
|
605
606
|
getBasePath(),
|
|
606
|
-
"templates/middleware/
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
607
|
+
"templates/middleware/loader-sw.ts"
|
|
608
|
+
);
|
|
609
|
+
return { path: loaderPath };
|
|
610
|
+
});
|
|
611
|
+
const middlewareFilter = /^__MIDDLEWARE_(\d+)__$/;
|
|
612
|
+
build.onResolve({ filter: middlewareFilter }, (args) => {
|
|
613
|
+
const match = middlewareFilter.exec(args.path);
|
|
614
|
+
assert(match !== null);
|
|
615
|
+
const middlewareIndex = parseInt(match[1]);
|
|
616
|
+
return {
|
|
617
|
+
path: path.resolve(
|
|
618
|
+
getBasePath(),
|
|
619
|
+
middleware[middlewareIndex].path
|
|
620
|
+
),
|
|
621
|
+
};
|
|
622
|
+
});
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
outfile: targetPathInsertion,
|
|
627
|
+
});
|
|
628
|
+
return {
|
|
629
|
+
...entry,
|
|
630
|
+
inject: [targetPathInsertion],
|
|
631
|
+
};
|
|
632
|
+
}
|
|
618
633
|
}
|
|
619
634
|
|
|
620
635
|
/**
|
package/src/config/validation.ts
CHANGED
|
@@ -1814,7 +1814,7 @@ const validateD1Binding: ValidatorFn = (diagnostics, field, value) => {
|
|
|
1814
1814
|
}
|
|
1815
1815
|
if (isValid && !process.env.NO_D1_WARNING) {
|
|
1816
1816
|
diagnostics.warnings.push(
|
|
1817
|
-
`D1 Bindings are currently in
|
|
1817
|
+
`D1 Bindings are currently in alpha to allow the API to evolve before general availability.\nPlease report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose\nNote: set NO_D1_WARNING=true to hide this message`
|
|
1818
1818
|
);
|
|
1819
1819
|
}
|
|
1820
1820
|
return isValid;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Box, render, Text } from "ink";
|
|
4
|
+
import Table from "ink-table";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { withConfig } from "../../config";
|
|
7
|
+
import { confirm } from "../../dialogs";
|
|
8
|
+
import { logger } from "../../logger";
|
|
9
|
+
import { requireAuth } from "../../user";
|
|
10
|
+
import { createBackup } from "../backups";
|
|
11
|
+
import { executeSql } from "../execute";
|
|
12
|
+
import { Database } from "../options";
|
|
13
|
+
import { d1BetaWarning, getDatabaseInfoFromConfig } from "../utils";
|
|
14
|
+
import {
|
|
15
|
+
getMigrationsPath,
|
|
16
|
+
getUnappliedMigrations,
|
|
17
|
+
initMigrationsTable,
|
|
18
|
+
} from "./helpers";
|
|
19
|
+
import type { ParseError } from "../../parse";
|
|
20
|
+
import type { BaseSqlExecuteArgs } from "../execute";
|
|
21
|
+
import type { Argv } from "yargs";
|
|
22
|
+
|
|
23
|
+
export function ApplyOptions(yargs: Argv): Argv<BaseSqlExecuteArgs> {
|
|
24
|
+
return Database(yargs);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ApplyHandler = withConfig<BaseSqlExecuteArgs>(
|
|
28
|
+
async ({ config, database, local, persistTo }): Promise<void> => {
|
|
29
|
+
const accountId = await requireAuth({});
|
|
30
|
+
logger.log(d1BetaWarning);
|
|
31
|
+
|
|
32
|
+
const databaseInfo = await getDatabaseInfoFromConfig(config, database);
|
|
33
|
+
if (!databaseInfo) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Can't find a DB with name/binding '${database}' in local config. Check info in wrangler.toml...`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!config.configPath) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const migrationsPath = await getMigrationsPath(
|
|
44
|
+
path.dirname(config.configPath),
|
|
45
|
+
databaseInfo.migrationsFolderPath,
|
|
46
|
+
false
|
|
47
|
+
);
|
|
48
|
+
await initMigrationsTable(
|
|
49
|
+
databaseInfo.migrationsTableName,
|
|
50
|
+
local,
|
|
51
|
+
config,
|
|
52
|
+
database,
|
|
53
|
+
persistTo
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const unappliedMigrations = (
|
|
57
|
+
await getUnappliedMigrations(
|
|
58
|
+
databaseInfo.migrationsTableName,
|
|
59
|
+
migrationsPath,
|
|
60
|
+
local,
|
|
61
|
+
config,
|
|
62
|
+
database,
|
|
63
|
+
persistTo
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
.map((migration) => {
|
|
67
|
+
return {
|
|
68
|
+
Name: migration,
|
|
69
|
+
Status: "🕒️",
|
|
70
|
+
};
|
|
71
|
+
})
|
|
72
|
+
.sort((a, b) => {
|
|
73
|
+
const migrationNumberA = parseInt(a.Name.split("_")[0]);
|
|
74
|
+
const migrationNumberB = parseInt(b.Name.split("_")[0]);
|
|
75
|
+
if (migrationNumberA < migrationNumberB) {
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
if (migrationNumberA > migrationNumberB) {
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// numbers must be equal
|
|
83
|
+
return 0;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (unappliedMigrations.length === 0) {
|
|
87
|
+
render(<Text>✅ No migrations to apply!</Text>);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const isInteractive = process.stdout.isTTY;
|
|
92
|
+
if (isInteractive) {
|
|
93
|
+
const ok = await confirm(
|
|
94
|
+
`About to apply ${unappliedMigrations.length} migration(s)\n` +
|
|
95
|
+
"Your database may not be available to serve requests during the migration, continue?",
|
|
96
|
+
<Box flexDirection="column">
|
|
97
|
+
<Text>Migrations to be applied:</Text>
|
|
98
|
+
<Table data={unappliedMigrations} columns={["Name"]}></Table>
|
|
99
|
+
</Box>
|
|
100
|
+
);
|
|
101
|
+
if (!ok) return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
render(<Text>🕒 Creating backup...</Text>);
|
|
105
|
+
await createBackup(accountId, databaseInfo.uuid);
|
|
106
|
+
|
|
107
|
+
for (const migration of unappliedMigrations) {
|
|
108
|
+
let query = fs.readFileSync(
|
|
109
|
+
`${migrationsPath}/${migration.Name}`,
|
|
110
|
+
"utf8"
|
|
111
|
+
);
|
|
112
|
+
query += `
|
|
113
|
+
INSERT INTO ${databaseInfo.migrationsTableName} (name)
|
|
114
|
+
values ('${migration.Name}');
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
let success = true;
|
|
118
|
+
let errorNotes: Array<string> = [];
|
|
119
|
+
try {
|
|
120
|
+
const response = await executeSql(
|
|
121
|
+
local,
|
|
122
|
+
config,
|
|
123
|
+
database,
|
|
124
|
+
undefined,
|
|
125
|
+
persistTo,
|
|
126
|
+
undefined,
|
|
127
|
+
query
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (response === null) {
|
|
131
|
+
// TODO: return error
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const result of response) {
|
|
136
|
+
// When executing more than 1 statement, response turns into an array of QueryResult
|
|
137
|
+
if (Array.isArray(result)) {
|
|
138
|
+
for (const subResult of result) {
|
|
139
|
+
if (!subResult.success) {
|
|
140
|
+
success = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
if (!result.success) {
|
|
145
|
+
success = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (e) {
|
|
150
|
+
const err = e as ParseError;
|
|
151
|
+
|
|
152
|
+
success = false;
|
|
153
|
+
errorNotes = err.notes.map((msg) => msg.text);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
migration.Status = success ? "✅" : "❌";
|
|
157
|
+
|
|
158
|
+
render(
|
|
159
|
+
<Box flexDirection="column">
|
|
160
|
+
<Table
|
|
161
|
+
data={unappliedMigrations}
|
|
162
|
+
columns={["Name", "Status"]}
|
|
163
|
+
></Table>
|
|
164
|
+
{errorNotes.length > 0 && (
|
|
165
|
+
<Box flexDirection="column">
|
|
166
|
+
<Text> </Text>
|
|
167
|
+
<Text>
|
|
168
|
+
❌ Migration {migration.Name} failed with following Errors
|
|
169
|
+
</Text>
|
|
170
|
+
<Table
|
|
171
|
+
data={errorNotes.map((err) => {
|
|
172
|
+
return { Error: err };
|
|
173
|
+
})}
|
|
174
|
+
></Table>
|
|
175
|
+
</Box>
|
|
176
|
+
)}
|
|
177
|
+
</Box>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (errorNotes.length > 0) return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
);
|