wrangler 2.1.14 → 2.2.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.
Files changed (75) hide show
  1. package/miniflare-dist/index.mjs +3 -1
  2. package/package.json +2 -1
  3. package/src/__tests__/access.test.ts +25 -0
  4. package/src/__tests__/api-dev.test.ts +1 -1
  5. package/src/__tests__/api-devregistry.test.js +2 -2
  6. package/src/__tests__/configuration.test.ts +119 -2
  7. package/src/__tests__/d1.test.ts +2 -0
  8. package/src/__tests__/deployments.test.ts +22 -22
  9. package/src/__tests__/dev.test.tsx +167 -15
  10. package/src/__tests__/helpers/msw/handlers/access.ts +13 -0
  11. package/src/__tests__/helpers/msw/handlers/deployments.ts +22 -43
  12. package/src/__tests__/helpers/msw/handlers/zones.ts +22 -0
  13. package/src/__tests__/helpers/msw/index.ts +4 -0
  14. package/src/__tests__/index.test.ts +42 -33
  15. package/src/__tests__/init.test.ts +88 -4
  16. package/src/__tests__/jest.setup.ts +11 -0
  17. package/src/__tests__/kv.test.ts +400 -400
  18. package/src/__tests__/pages.test.ts +140 -28
  19. package/src/__tests__/publish.test.ts +1161 -647
  20. package/src/__tests__/pubsub.test.ts +3 -0
  21. package/src/__tests__/queues.test.ts +371 -0
  22. package/src/__tests__/r2.test.ts +57 -52
  23. package/src/__tests__/worker-namespace.test.ts +15 -10
  24. package/src/bundle-reporter.tsx +41 -2
  25. package/src/bundle.ts +59 -30
  26. package/src/cli.ts +0 -1
  27. package/src/config/environment.ts +50 -0
  28. package/src/config/index.ts +41 -0
  29. package/src/config/validation.ts +173 -0
  30. package/src/create-worker-preview.ts +10 -3
  31. package/src/create-worker-upload-form.ts +12 -0
  32. package/src/d1/backups.tsx +11 -5
  33. package/src/d1/execute.tsx +52 -47
  34. package/src/d1/index.ts +2 -1
  35. package/src/delete.ts +7 -10
  36. package/src/deployments.ts +73 -0
  37. package/src/deprecated/index.ts +9 -24
  38. package/src/dev/dev-vars.ts +11 -8
  39. package/src/dev/dev.tsx +12 -0
  40. package/src/dev/local.tsx +26 -0
  41. package/src/dev/remote.tsx +2 -0
  42. package/src/dev/start-server.ts +7 -0
  43. package/src/dev/use-esbuild.ts +12 -5
  44. package/src/dev.tsx +12 -9
  45. package/src/dispatch-namespace.ts +4 -3
  46. package/src/index.tsx +61 -45
  47. package/src/init.ts +4 -4
  48. package/src/inspect.ts +21 -1
  49. package/src/is-interactive.ts +4 -0
  50. package/src/kv/index.ts +5 -54
  51. package/src/logger.ts +12 -0
  52. package/src/pages/constants.ts +2 -0
  53. package/src/pages/upload.tsx +42 -15
  54. package/src/proxy.ts +38 -6
  55. package/src/publish/index.ts +11 -8
  56. package/src/publish/publish.ts +151 -30
  57. package/src/pubsub/pubsub-commands.tsx +3 -2
  58. package/src/queues/cli/commands/consumer/add.ts +71 -0
  59. package/src/queues/cli/commands/consumer/index.ts +22 -0
  60. package/src/queues/cli/commands/consumer/remove.ts +38 -0
  61. package/src/queues/cli/commands/create.ts +25 -0
  62. package/src/queues/cli/commands/delete.ts +26 -0
  63. package/src/queues/cli/commands/index.ts +33 -0
  64. package/src/queues/cli/commands/list.ts +25 -0
  65. package/src/queues/client.ts +135 -0
  66. package/src/secret/index.ts +14 -39
  67. package/src/tail/index.ts +5 -8
  68. package/src/user/access.ts +69 -0
  69. package/src/worker.ts +7 -0
  70. package/src/yargs-types.ts +15 -2
  71. package/src/zones.ts +31 -5
  72. package/templates/pages-template-plugin.ts +4 -0
  73. package/templates/pages-template-worker.ts +21 -4
  74. package/wrangler-dist/cli.d.ts +42 -0
  75. package/wrangler-dist/cli.js +4559 -3228
package/src/proxy.ts CHANGED
@@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from "react";
8
8
  import serveStatic from "serve-static";
9
9
  import { getHttpsOptions } from "./https-options";
10
10
  import { logger } from "./logger";
11
+ import { getAccessToken } from "./user/access";
11
12
  import type { CfPreviewToken } from "./create-worker-preview";
12
13
  import type { HttpTerminator } from "http-terminator";
13
14
  import type {
@@ -42,6 +43,27 @@ function addCfPreviewTokenHeader(
42
43
  headers["cf-workers-preview-token"] = previewTokenValue;
43
44
  }
44
45
 
46
+ export async function addCfAccessToken(
47
+ headers: IncomingHttpHeaders,
48
+ domain: string,
49
+ accessTokenRef: { current: string | undefined | null }
50
+ ) {
51
+ if (accessTokenRef.current === null) {
52
+ return;
53
+ }
54
+ if (typeof accessTokenRef.current === "string") {
55
+ headers[
56
+ "cookie"
57
+ ] = `${headers["cookie"]};CF_Authorization=${accessTokenRef.current}`;
58
+ return;
59
+ }
60
+ const token = await getAccessToken(domain);
61
+ accessTokenRef.current = token;
62
+ if (token)
63
+ headers[
64
+ "cookie"
65
+ ] = `${headers["cookie"]};CF_Authorization=${accessTokenRef.current}`;
66
+ }
45
67
  /**
46
68
  * Rewrite references in request headers
47
69
  * from the preview host to the local host.
@@ -120,6 +142,7 @@ export async function startPreviewServer({
120
142
  // We have a token. Let's proxy requests to the preview end point.
121
143
  const streamBufferRef = { current: [] };
122
144
  const requestResponseBufferRef = { current: [] };
145
+ const accessTokenRef = { current: undefined };
123
146
  const cleanupListeners = configureProxyServer({
124
147
  proxy,
125
148
  previewToken,
@@ -129,6 +152,7 @@ export async function startPreviewServer({
129
152
  assetDirectory,
130
153
  localProtocol,
131
154
  port,
155
+ accessTokenRef,
132
156
  });
133
157
 
134
158
  await waitForPortToBeAvailable(port, {
@@ -215,6 +239,7 @@ export function usePreviewServer({
215
239
  const requestResponseBufferRef = useRef<
216
240
  { request: IncomingMessage; response: ServerResponse }[]
217
241
  >([]);
242
+ const accessTokenRef = useRef<string | undefined | null>(undefined);
218
243
 
219
244
  /**
220
245
  * The session doesn't last forever, and will eventually drop
@@ -237,6 +262,7 @@ export function usePreviewServer({
237
262
  assetDirectory,
238
263
  localProtocol,
239
264
  port,
265
+ accessTokenRef,
240
266
  });
241
267
  return () => {
242
268
  cleanupListeners?.forEach((cleanup) => cleanup());
@@ -307,6 +333,7 @@ function configureProxyServer({
307
333
  port,
308
334
  localProtocol,
309
335
  assetDirectory,
336
+ accessTokenRef,
310
337
  }: {
311
338
  proxy: PreviewProxy | undefined;
312
339
  previewToken: CfPreviewToken | undefined;
@@ -324,6 +351,7 @@ function configureProxyServer({
324
351
  port: number;
325
352
  localProtocol: "https" | "http";
326
353
  assetDirectory: string | undefined;
354
+ accessTokenRef: { current: string | null | undefined };
327
355
  }) {
328
356
  if (proxy === undefined) {
329
357
  return;
@@ -376,7 +404,8 @@ function configureProxyServer({
376
404
  previewToken,
377
405
  remote,
378
406
  port,
379
- localProtocol
407
+ localProtocol,
408
+ accessTokenRef
380
409
  );
381
410
  proxy.server.on("stream", handleStream);
382
411
  cleanupListeners.push(() => proxy.server.off("stream", handleStream));
@@ -389,7 +418,7 @@ function configureProxyServer({
389
418
  streamBufferRef.current = [];
390
419
 
391
420
  /** HTTP/1 -> HTTP/2 */
392
- const handleRequest: RequestListener = (
421
+ const handleRequest: RequestListener = async (
393
422
  message: IncomingMessage,
394
423
  response: ServerResponse
395
424
  ) => {
@@ -397,6 +426,7 @@ function configureProxyServer({
397
426
  if (httpVersionMajor >= 2) {
398
427
  return; // Already handled by the "stream" event.
399
428
  }
429
+ await addCfAccessToken(headers, previewToken.host, accessTokenRef);
400
430
  addCfPreviewTokenHeader(headers, previewToken.value);
401
431
  headers[":method"] = method;
402
432
  headers[":path"] = url;
@@ -419,7 +449,6 @@ function configureProxyServer({
419
449
 
420
450
  request.on("response", (responseHeaders) => {
421
451
  const status = responseHeaders[":status"] ?? 500;
422
-
423
452
  // log all requests to terminal
424
453
  logger.log(new Date().toLocaleTimeString(), method, url, status);
425
454
 
@@ -461,12 +490,13 @@ function configureProxyServer({
461
490
  requestResponseBufferRef.current = [];
462
491
 
463
492
  /** HTTP/1 -> WebSocket (over HTTP/1) */
464
- const handleUpgrade = (
493
+ const handleUpgrade = async (
465
494
  originalMessage: IncomingMessage,
466
495
  originalSocket: Duplex,
467
496
  originalHead: Buffer
468
497
  ) => {
469
498
  const { headers, method, url } = originalMessage;
499
+ await addCfAccessToken(headers, previewToken.host, accessTokenRef);
470
500
  addCfPreviewTokenHeader(headers, previewToken.value);
471
501
  headers["host"] = previewToken.host;
472
502
 
@@ -562,12 +592,14 @@ function createStreamHandler(
562
592
  previewToken: CfPreviewToken,
563
593
  remote: ClientHttp2Session,
564
594
  localPort: number,
565
- localProtocol: "https" | "http"
595
+ localProtocol: "https" | "http",
596
+ accessTokenRef: { current: string | undefined | null }
566
597
  ) {
567
- return function handleStream(
598
+ return async function handleStream(
568
599
  stream: ServerHttp2Stream,
569
600
  headers: IncomingHttpHeaders
570
601
  ) {
602
+ await addCfAccessToken(headers, previewToken.host, accessTokenRef);
571
603
  addCfPreviewTokenHeader(headers, previewToken.value);
572
604
  headers[":authority"] = previewToken.host;
573
605
  const request = stream.pipe(remote.request(headers));
@@ -14,18 +14,15 @@ import { requireAuth } from "../user";
14
14
  import { collectKeyValues } from "../utils/collectKeyValues";
15
15
  import publish from "./publish";
16
16
  import type { ConfigPath } from "../index";
17
- import type { YargsOptionsToInterface } from "../yargs-types";
17
+ import type {
18
+ CommonYargsOptions,
19
+ YargsOptionsToInterface,
20
+ } from "../yargs-types";
18
21
  import type { Argv, ArgumentsCamelCase } from "yargs";
19
22
 
20
- export function publishOptions(yargs: Argv) {
23
+ export function publishOptions(yargs: Argv<CommonYargsOptions>) {
21
24
  return (
22
25
  yargs
23
- .option("env", {
24
- type: "string",
25
- requiresArg: true,
26
- describe: "Perform on a specific environment",
27
- alias: "e",
28
- })
29
26
  .positional("script", {
30
27
  describe: "The path to an entry point for your worker",
31
28
  type: "string",
@@ -174,6 +171,11 @@ export function publishOptions(yargs: Argv) {
174
171
  describe: "Use legacy environments",
175
172
  hidden: true,
176
173
  })
174
+ .option("logpush", {
175
+ type: "boolean",
176
+ describe:
177
+ "Send Trace Events from this worker to Workers Logpush.\nThis will not configure a corresponding Logpush job automatically.",
178
+ })
177
179
  );
178
180
  }
179
181
 
@@ -263,5 +265,6 @@ export async function publishHandler(args: ArgumentsCamelCase<PublishArgs>) {
263
265
  dryRun: args.dryRun,
264
266
  noBundle: !(args.bundle ?? !config.no_bundle),
265
267
  keepVars: args.keepVars,
268
+ logpush: args.logpush,
266
269
  });
267
270
  }
@@ -5,7 +5,10 @@ import { URLSearchParams } from "node:url";
5
5
  import chalk from "chalk";
6
6
  import tmp from "tmp-promise";
7
7
  import { bundleWorker } from "../bundle";
8
- import { printBundleSize } from "../bundle-reporter";
8
+ import {
9
+ printBundleSize,
10
+ printOffendingDependencies,
11
+ } from "../bundle-reporter";
9
12
  import { fetchListResult, fetchResult } from "../cfetch";
10
13
  import { printBindings } from "../config";
11
14
  import { createWorkerUploadForm } from "../create-worker-upload-form";
@@ -14,10 +17,12 @@ import { getMigrationsToUpload } from "../durable";
14
17
  import { logger } from "../logger";
15
18
  import { getMetricsUsageHeaders } from "../metrics";
16
19
  import { ParseError } from "../parse";
20
+ import { getQueue, putConsumer } from "../queues/client";
17
21
  import { getWorkersDevSubdomain } from "../routes";
18
22
  import { syncAssets } from "../sites";
19
23
  import { identifyD1BindingsAsBeta } from "../worker";
20
24
  import { getZoneForRoute } from "../zones";
25
+ import type { FetchError } from "../cfetch";
21
26
  import type { Config } from "../config";
22
27
  import type {
23
28
  Route,
@@ -25,7 +30,9 @@ import type {
25
30
  ZoneNameRoute,
26
31
  CustomDomainRoute,
27
32
  } from "../config/environment";
33
+ import type { DeploymentListRes } from "../deployments";
28
34
  import type { Entry } from "../entry";
35
+ import type { PutConsumerBody } from "../queues/client";
29
36
  import type { AssetPaths } from "../sites";
30
37
  import type { CfWorkerInit } from "../worker";
31
38
 
@@ -54,6 +61,7 @@ type Props = {
54
61
  dryRun: boolean | undefined;
55
62
  noBundle: boolean | undefined;
56
63
  keepVars: boolean | undefined;
64
+ logpush: boolean | undefined;
57
65
  };
58
66
 
59
67
  type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute;
@@ -83,6 +91,33 @@ function sleep(ms: number) {
83
91
  return new Promise((resolve) => setTimeout(resolve, ms));
84
92
  }
85
93
 
94
+ const scriptStartupErrorRegex = /startup/i;
95
+
96
+ function errIsScriptSizeOrStartupErr(err: unknown) {
97
+ if (!err) return false;
98
+
99
+ // 10027 = workers.api.error.script_too_large
100
+ if ((err as { code: number }).code === 10027) {
101
+ return true;
102
+ }
103
+
104
+ // 10021 = validation error
105
+ // no explicit error code for more granular errors than "invalid script"
106
+ // but the error will contain a string error message directly from the
107
+ // validator.
108
+ // the error always SHOULD look like "Script startup exceeded CPU limit."
109
+ // (or the less likely "Script startup exceeded memory limits.")
110
+ if (
111
+ (err as { code: number }).code === 10021 &&
112
+ err instanceof ParseError &&
113
+ scriptStartupErrorRegex.test(err.notes[0]?.text)
114
+ ) {
115
+ return true;
116
+ }
117
+
118
+ return false;
119
+ }
120
+
86
121
  function renderRoute(route: Route): string {
87
122
  let result = "";
88
123
  if (typeof route === "string") {
@@ -350,6 +385,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
350
385
  : `/accounts/${accountId}/workers/scripts/${scriptName}`;
351
386
 
352
387
  let available_on_subdomain: boolean | undefined = undefined; // we'll set this later
388
+ let scriptTag: string | null = null;
353
389
 
354
390
  const { format } = props.entry;
355
391
 
@@ -407,12 +443,14 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
407
443
 
408
444
  const {
409
445
  modules,
446
+ dependencies,
410
447
  resolvedEntryPointPath,
411
448
  bundleType,
412
449
  }: Awaited<ReturnType<typeof bundleWorker>> = props.noBundle
413
450
  ? // we can skip the whole bundling step and mock a bundle here
414
451
  {
415
452
  modules: [],
453
+ dependencies: {},
416
454
  resolvedEntryPointPath: props.entry.file,
417
455
  bundleType: props.entry.format === "modules" ? "esm" : "commonjs",
418
456
  stop: undefined,
@@ -496,6 +534,9 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
496
534
  },
497
535
  data_blobs: config.data_blobs,
498
536
  durable_objects: config.durable_objects,
537
+ queues: config.queues.producers?.map((producer) => {
538
+ return { binding: producer.binding, queue_name: producer.queue };
539
+ }),
499
540
  r2_buckets: config.r2_buckets,
500
541
  d1_databases: identifyD1BindingsAsBeta(config.d1_databases),
501
542
  services: config.services,
@@ -527,6 +568,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
527
568
  props.compatibilityFlags ?? config.compatibility_flags,
528
569
  usage_model: config.usage_model,
529
570
  keepVars,
571
+ logpush: props.logpush !== undefined ? props.logpush : config.logpush,
530
572
  };
531
573
 
532
574
  // As this is not deterministic for testing, we detect if in a jest environment and run asynchronously
@@ -557,39 +599,50 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
557
599
 
558
600
  printBindings({ ...withoutStaticAssets, vars: maskedVars });
559
601
 
602
+ await ensureQueuesExist(config);
603
+
560
604
  if (!props.dryRun) {
561
605
  // Upload the script so it has time to propagate.
562
606
  // We can also now tell whether available_on_subdomain is set
563
- const result = await fetchResult<{
564
- available_on_subdomain: boolean;
565
- id: string | null;
566
- etag: string | null;
567
- pipeline_hash: string | null;
568
- }>(
569
- workerUrl,
570
- {
571
- method: "PUT",
572
- body: createWorkerUploadForm(worker),
573
- headers: await getMetricsUsageHeaders(config.send_metrics),
574
- },
575
- new URLSearchParams({
576
- include_subdomain_availability: "true",
577
- // pass excludeScript so the whole body of the
578
- // script doesn't get included in the response
579
- excludeScript: "true",
580
- })
581
- );
582
-
583
- available_on_subdomain = result.available_on_subdomain;
607
+ try {
608
+ const result = await fetchResult<{
609
+ available_on_subdomain: boolean;
610
+ id: string | null;
611
+ etag: string | null;
612
+ pipeline_hash: string | null;
613
+ tag: string | null;
614
+ }>(
615
+ workerUrl,
616
+ {
617
+ method: "PUT",
618
+ body: createWorkerUploadForm(worker),
619
+ headers: await getMetricsUsageHeaders(config.send_metrics),
620
+ },
621
+ new URLSearchParams({
622
+ include_subdomain_availability: "true",
623
+ // pass excludeScript so the whole body of the
624
+ // script doesn't get included in the response
625
+ excludeScript: "true",
626
+ })
627
+ );
584
628
 
585
- if (config.first_party_worker) {
586
- // Print some useful information returned after publishing
587
- // Not all fields will be populated for every worker
588
- // These fields are likely to be scraped by tools, so do not rename
589
- if (result.id) logger.log("Worker ID: ", result.id);
590
- if (result.etag) logger.log("Worker ETag: ", result.etag);
591
- if (result.pipeline_hash)
592
- logger.log("Worker PipelineHash: ", result.pipeline_hash);
629
+ available_on_subdomain = result.available_on_subdomain;
630
+ scriptTag = result.tag;
631
+
632
+ if (config.first_party_worker) {
633
+ // Print some useful information returned after publishing
634
+ // Not all fields will be populated for every worker
635
+ // These fields are likely to be scraped by tools, so do not rename
636
+ if (result.id) logger.log("Worker ID: ", result.id);
637
+ if (result.etag) logger.log("Worker ETag: ", result.etag);
638
+ if (result.pipeline_hash)
639
+ logger.log("Worker PipelineHash: ", result.pipeline_hash);
640
+ }
641
+ } catch (err) {
642
+ if (errIsScriptSizeOrStartupErr(err)) {
643
+ printOffendingDependencies(dependencies);
644
+ }
645
+ throw err;
593
646
  }
594
647
  }
595
648
  } finally {
@@ -746,6 +799,10 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
746
799
  );
747
800
  }
748
801
 
802
+ if (config.queues.consumers && config.queues.consumers.length) {
803
+ deployments.push(...updateQueueConsumers(config));
804
+ }
805
+
749
806
  const targets = await Promise.all(deployments);
750
807
  const deployMs = Date.now() - start - uploadMs;
751
808
 
@@ -761,6 +818,19 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
761
818
  } else {
762
819
  logger.log("No publish targets for", workerName, formatTime(deployMs));
763
820
  }
821
+
822
+ try {
823
+ const deploymentsList = await fetchResult<DeploymentListRes>(
824
+ `/accounts/${accountId}/workers/versions/by-script/${scriptTag}`
825
+ );
826
+
827
+ logger.log("Current Deployment ID:", deploymentsList.latest.id);
828
+ } catch (e) {
829
+ if ((e as { code: number }).code === 10023) {
830
+ // TODO: remove this try/catch once versions is completely rolled out
831
+ }
832
+ throw e;
833
+ }
764
834
  }
765
835
 
766
836
  function formatTime(duration: number) {
@@ -911,3 +981,54 @@ async function publishRoutesFallback(
911
981
  function isAuthenticationError(e: unknown): e is ParseError {
912
982
  return e instanceof ParseError && (e as { code?: number }).code === 10000;
913
983
  }
984
+
985
+ async function ensureQueuesExist(config: Config) {
986
+ const producers = (config.queues.producers || []).map(
987
+ (producer) => producer.queue
988
+ );
989
+ const consumers = (config.queues.consumers || []).map(
990
+ (consumer) => consumer.queue
991
+ );
992
+
993
+ const queueNames = producers.concat(consumers);
994
+ for (const queue of queueNames) {
995
+ try {
996
+ await getQueue(config, queue);
997
+ } catch (err) {
998
+ const queueErr = err as FetchError;
999
+ if (queueErr.code === 100123) {
1000
+ // queue_not_found
1001
+ throw new Error(
1002
+ `Queue "${queue}" does not exist. To create it, run: wrangler queues create ${queue}`
1003
+ );
1004
+ }
1005
+ throw err;
1006
+ }
1007
+ }
1008
+ }
1009
+
1010
+ function updateQueueConsumers(config: Config): Promise<string[]>[] {
1011
+ const consumers = config.queues.consumers || [];
1012
+ return consumers.map((consumer) => {
1013
+ const body: PutConsumerBody = {
1014
+ dead_letter_queue: consumer.dead_letter_queue,
1015
+ settings: {
1016
+ batch_size: consumer.max_batch_size,
1017
+ max_retries: consumer.max_retries,
1018
+ max_wait_time_ms: consumer.max_batch_timeout
1019
+ ? 1000 * consumer.max_batch_timeout
1020
+ : undefined,
1021
+ },
1022
+ };
1023
+
1024
+ if (config.name === undefined) {
1025
+ // TODO: how can we reliably get the current script name?
1026
+ throw new Error("Script name is required to update queue consumers");
1027
+ }
1028
+ const scriptName = config.name;
1029
+ const envName = undefined; // TODO: script environment for wrangler publish?
1030
+ return putConsumer(config, consumer.queue, scriptName, envName, body).then(
1031
+ () => [`Consumer for ${consumer.queue}`]
1032
+ );
1033
+ });
1034
+ }
@@ -7,11 +7,12 @@ import * as metrics from "../metrics";
7
7
  import { parseHumanDuration } from "../parse";
8
8
  import { requireAuth } from "../user";
9
9
  import * as pubsub from ".";
10
+ import type { CommonYargsOptions } from "../yargs-types";
10
11
  import type { Argv, CommandModule } from "yargs";
11
12
 
12
13
  export function pubSubCommands(
13
- pubsubYargs: Argv,
14
- subHelp: CommandModule
14
+ pubsubYargs: Argv<CommonYargsOptions>,
15
+ subHelp: CommandModule<CommonYargsOptions, CommonYargsOptions>
15
16
  ): Argv {
16
17
  return pubsubYargs
17
18
  .command(subHelp)
@@ -0,0 +1,71 @@
1
+ import { type Argv } from "yargs";
2
+ import { readConfig } from "../../../../config";
3
+ import { logger } from "../../../../logger";
4
+ import { postConsumer } from "../../../client";
5
+ import type { CommonYargsOptions } from "../../../../yargs-types";
6
+ import type { PostConsumerBody } from "../../../client";
7
+
8
+ type Args = CommonYargsOptions & {
9
+ config?: string;
10
+ ["queue-name"]: string;
11
+ ["script-name"]: string;
12
+ ["batch-size"]?: number;
13
+ ["batch-timeout"]?: number;
14
+ ["message-retries"]?: number;
15
+ ["dead-letter-queue"]?: string;
16
+ };
17
+
18
+ export function options(yargs: Argv<CommonYargsOptions>): Argv<Args> {
19
+ return yargs
20
+ .positional("queue-name", {
21
+ type: "string",
22
+ demandOption: true,
23
+ description: "Name of the queue to configure",
24
+ })
25
+ .positional("script-name", {
26
+ type: "string",
27
+ demandOption: true,
28
+ description: "Name of the consumer script",
29
+ })
30
+ .options({
31
+ "batch-size": {
32
+ type: "number",
33
+ describe: "Maximum number of messages per batch",
34
+ },
35
+ "batch-timeout": {
36
+ type: "number",
37
+ describe:
38
+ "Maximum number of seconds to wait to fill a batch with messages",
39
+ },
40
+ "message-retries": {
41
+ type: "number",
42
+ describe: "Maximum number of retries for each message",
43
+ },
44
+ "dead-letter-queue": {
45
+ type: "string",
46
+ describe: "Queue to send messages that failed to be consumed",
47
+ },
48
+ });
49
+ }
50
+
51
+ export async function handler(args: Args) {
52
+ const config = readConfig(args.config, args);
53
+
54
+ const body: PostConsumerBody = {
55
+ script_name: args["script-name"],
56
+ // TODO(soon) is this still the correct usage of the environment?
57
+ environment_name: args.env || "", // API expects empty string as default
58
+ settings: {
59
+ batch_size: args["batch-size"],
60
+ max_retries: args["message-retries"],
61
+ max_wait_time_ms: args["batch-timeout"] // API expects milliseconds
62
+ ? 1000 * args["batch-timeout"]
63
+ : undefined,
64
+ },
65
+ dead_letter_queue: args["dead-letter-queue"],
66
+ };
67
+
68
+ logger.log(`Adding consumer to queue ${args["queue-name"]}.`);
69
+ await postConsumer(config, args["queue-name"], body);
70
+ logger.log(`Added consumer to queue ${args["queue-name"]}.`);
71
+ }
@@ -0,0 +1,22 @@
1
+ import { type BuilderCallback } from "yargs";
2
+ import { type CommonYargsOptions } from "../../../../yargs-types";
3
+ import { options as addOptions, handler as addHandler } from "./add";
4
+ import { options as removeOptions, handler as removeHandler } from "./remove";
5
+
6
+ export const consumers: BuilderCallback<CommonYargsOptions, unknown> = (
7
+ yargs
8
+ ) => {
9
+ yargs.command(
10
+ "add <queue-name> <script-name>",
11
+ "Add a Queue Consumer",
12
+ addOptions,
13
+ addHandler
14
+ );
15
+
16
+ yargs.command(
17
+ "remove <queue-name> <script-name>",
18
+ "Remove a Queue Consumer",
19
+ removeOptions,
20
+ removeHandler
21
+ );
22
+ };
@@ -0,0 +1,38 @@
1
+ import { type Argv } from "yargs";
2
+ import { readConfig } from "../../../../config";
3
+ import { logger } from "../../../../logger";
4
+ import { deleteConsumer } from "../../../client";
5
+ import type { CommonYargsOptions } from "../../../../yargs-types";
6
+
7
+ type Args = CommonYargsOptions & {
8
+ config?: string;
9
+ ["queue-name"]: string;
10
+ ["script-name"]: string;
11
+ };
12
+
13
+ export function options(yargs: Argv<CommonYargsOptions>): Argv<Args> {
14
+ return yargs
15
+ .positional("queue-name", {
16
+ type: "string",
17
+ demandOption: true,
18
+ description: "Name of the queue to configure",
19
+ })
20
+ .positional("script-name", {
21
+ type: "string",
22
+ demandOption: true,
23
+ description: "Name of the consumer script",
24
+ });
25
+ }
26
+
27
+ export async function handler(args: Args) {
28
+ const config = readConfig(args.config, args);
29
+
30
+ logger.log(`Removing consumer from queue ${args["queue-name"]}.`);
31
+ await deleteConsumer(
32
+ config,
33
+ args["queue-name"],
34
+ args["script-name"],
35
+ args.env
36
+ );
37
+ logger.log(`Removed consumer from queue ${args["queue-name"]}.`);
38
+ }
@@ -0,0 +1,25 @@
1
+ import { type Argv } from "yargs";
2
+ import { readConfig } from "../../../config";
3
+ import { logger } from "../../../logger";
4
+ import { createQueue } from "../../client";
5
+
6
+ interface Args {
7
+ config?: string;
8
+ name: string;
9
+ }
10
+
11
+ export function options(yargs: Argv): Argv<Args> {
12
+ return yargs.positional("name", {
13
+ type: "string",
14
+ demandOption: true,
15
+ description: "The name of the queue",
16
+ });
17
+ }
18
+
19
+ export async function handler(args: Args) {
20
+ const config = readConfig(args.config, args);
21
+
22
+ logger.log(`Creating queue ${args.name}.`);
23
+ await createQueue(config, { queue_name: args.name });
24
+ logger.log(`Created queue ${args.name}.`);
25
+ }
@@ -0,0 +1,26 @@
1
+ import { type Argv } from "yargs";
2
+ import { readConfig } from "../../../config";
3
+ import { logger } from "../../../logger";
4
+ import { deleteQueue } from "../../client";
5
+
6
+ interface Args {
7
+ config?: string;
8
+ name: string;
9
+ }
10
+
11
+ export function options(yargs: Argv): Argv<Args> {
12
+ // TODO(soon) --force option
13
+ return yargs.positional("name", {
14
+ type: "string",
15
+ demandOption: true,
16
+ description: "The name of the queue",
17
+ });
18
+ }
19
+
20
+ export async function handler(args: Args) {
21
+ const config = readConfig(args.config, args);
22
+
23
+ logger.log(`Deleting queue ${args.name}.`);
24
+ await deleteQueue(config, args.name);
25
+ logger.log(`Deleted queue ${args.name}.`);
26
+ }