Skip to content

Commit 2c996aa

Browse files
elithrarnetclioliythreepointone
authored
Pub/Sub: add wrangler pubsub commands (#1314)
* Support for wrangler2 pubsub ns * Added tests * Changes for removing staging and enabling Prod API support * More support for pubsub command * Updates for prod * pubsub: (wip) fix types, docs, and validation. * pubsub: fix deletePubSubBrokers URL * pubsub: fix on-publish-url flag * pubsub: fix format strings * pubsub: move issue,revoke,unrevoke under brokers * pubsub: fix update output formatting * pubsub: initial list tests * pubsub: tests, fix delete * pubsub: add public-keys, tests for ns * pubsub: fix date parsing iterator; add beta warning * pubsub: fix broker update + add test * pubsub: support --expiration for token issue * pubsub: separate commands * pubsub: use URLSearchParams; fix tests * update snapshots, remove examples folder * fix merge on index.tsx * reorder some files Co-authored-by: netcli <[email protected]> Co-authored-by: Oli Yu <[email protected]> Co-authored-by: Sunil Pai <[email protected]>
1 parent 5cc0772 commit 2c996aa

File tree

6 files changed

+1241
-0
lines changed

6 files changed

+1241
-0
lines changed

packages/wrangler/src/__tests__/index.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ describe("wrangler", () => {
4141
wrangler kv:bulk 💪 Interact with multiple Workers KV key-value pairs at once
4242
wrangler pages ⚡️ Configure Cloudflare Pages
4343
wrangler r2 📦 Interact with an R2 store
44+
wrangler pubsub 📮 Interact and manage Pub/Sub Brokers
4445
wrangler login 🔓 Login to Cloudflare
4546
wrangler logout 🚪 Logout from Cloudflare
4647
wrangler whoami 🕵️ Retrieve your user info and test your auth config
@@ -78,6 +79,7 @@ describe("wrangler", () => {
7879
wrangler kv:bulk 💪 Interact with multiple Workers KV key-value pairs at once
7980
wrangler pages ⚡️ Configure Cloudflare Pages
8081
wrangler r2 📦 Interact with an R2 store
82+
wrangler pubsub 📮 Interact and manage Pub/Sub Brokers
8183
wrangler login 🔓 Login to Cloudflare
8284
wrangler logout 🚪 Logout from Cloudflare
8385
wrangler whoami 🕵️ Retrieve your user info and test your auth config
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
2+
import { setMockResponse, unsetAllMocks } from "./helpers/mock-cfetch";
3+
import { mockConsoleMethods } from "./helpers/mock-console";
4+
import { runInTempDir } from "./helpers/run-in-tmp";
5+
import { runWrangler } from "./helpers/run-wrangler";
6+
import type {
7+
PubSubNamespace,
8+
PubSubBroker,
9+
PubSubBrokerUpdate,
10+
PubSubBrokerOnPublish,
11+
} from "../pubsub";
12+
13+
describe("wrangler", () => {
14+
mockAccountId();
15+
mockApiToken();
16+
runInTempDir();
17+
const std = mockConsoleMethods();
18+
19+
afterEach(() => {
20+
unsetAllMocks();
21+
});
22+
23+
describe("pubsub", () => {
24+
describe("namespaces", () => {
25+
describe("create", () => {
26+
function mockCreateRequest(expectedNamespaceName: string) {
27+
const requests = { count: 0 };
28+
setMockResponse(
29+
"/accounts/:accountId/pubsub/namespaces",
30+
"POST",
31+
([_url, accountId], { body }) => {
32+
expect(accountId).toEqual("some-account-id");
33+
const namespaceName = JSON.parse(body as string).name;
34+
expect(namespaceName).toEqual(expectedNamespaceName);
35+
requests.count += 1;
36+
}
37+
);
38+
return requests;
39+
}
40+
41+
it("should create a namespace", async () => {
42+
const requests = mockCreateRequest("my-namespace");
43+
await runWrangler("pubsub namespaces create my-namespace");
44+
// TODO: check returned object
45+
expect(requests.count).toEqual(1);
46+
});
47+
});
48+
49+
describe("list", () => {
50+
function mockListRequest(namespaces: PubSubNamespace[]) {
51+
const requests = { count: 0 };
52+
setMockResponse(
53+
"/accounts/:accountId/pubsub/namespaces",
54+
([_url, accountId], init) => {
55+
requests.count++;
56+
expect(accountId).toEqual("some-account-id");
57+
expect(init).toEqual({});
58+
return { namespaces };
59+
}
60+
);
61+
return requests;
62+
}
63+
64+
it("should list namespaces", async () => {
65+
const expectedNamespaces: PubSubNamespace[] = [
66+
{ name: "namespace-1", created_on: "01-01-2001" },
67+
{ name: "namespace-2", created_on: "01-01-2001" },
68+
];
69+
const requests = mockListRequest(expectedNamespaces);
70+
await runWrangler("pubsub namespaces list");
71+
72+
expect(std.err).toMatchInlineSnapshot(`""`);
73+
// TODO(elithrar): check returned object
74+
expect(requests.count).toEqual(1);
75+
});
76+
});
77+
});
78+
79+
describe("brokers", () => {
80+
describe("create", () => {
81+
function mockCreateRequest(expectedBrokerName: string) {
82+
const requests = { count: 0 };
83+
setMockResponse(
84+
"/accounts/:accountId/pubsub/namespaces/:namespaceName/brokers",
85+
"POST",
86+
([_url, accountId, namespaceName], { body }) => {
87+
expect(accountId).toEqual("some-account-id");
88+
expect(namespaceName).toEqual("some-namespace");
89+
const brokerName = JSON.parse(body as string).name;
90+
expect(brokerName).toEqual(expectedBrokerName);
91+
requests.count += 1;
92+
}
93+
);
94+
return requests;
95+
}
96+
97+
it("should create a broker", async () => {
98+
const requests = mockCreateRequest("my-broker");
99+
await runWrangler(
100+
"pubsub brokers create my-broker --namespace=some-namespace"
101+
);
102+
103+
// TODO: check returned object
104+
expect(requests.count).toEqual(1);
105+
});
106+
107+
it("fail to create broker when no namespace is set", async () => {
108+
await expect(
109+
runWrangler("pubsub brokers create my-broker")
110+
).rejects.toThrowErrorMatchingInlineSnapshot(
111+
`"Missing required argument: namespace"`
112+
);
113+
});
114+
});
115+
116+
describe("update", () => {
117+
function mockUpdateRequest(
118+
expectedBrokerName: string,
119+
expectedExpiration: number,
120+
expectedDescription: string,
121+
expectedOnPublishConfig: PubSubBrokerOnPublish
122+
) {
123+
const requests = { count: 0 };
124+
setMockResponse(
125+
"/accounts/:accountId/pubsub/namespaces/:namespaceName/brokers/:brokerName",
126+
"PATCH",
127+
([_url, accountId, namespaceName, brokerName], { body }) => {
128+
expect(accountId).toEqual("some-account-id");
129+
expect(namespaceName).toEqual("some-namespace");
130+
expect(brokerName).toEqual(expectedBrokerName);
131+
132+
const patchBody: PubSubBrokerUpdate = JSON.parse(body as string);
133+
expect(patchBody.expiration).toEqual(expectedExpiration);
134+
expect(patchBody.description).toEqual(expectedDescription);
135+
expect(patchBody.on_publish).toEqual(expectedOnPublishConfig);
136+
137+
requests.count += 1;
138+
}
139+
);
140+
return requests;
141+
}
142+
143+
it("should update a broker's properties", async () => {
144+
const expectedOnPublish: PubSubBrokerOnPublish = {
145+
url: "https://foo.bar.example.com",
146+
};
147+
const requests = mockUpdateRequest(
148+
"my-broker",
149+
86400,
150+
"hello",
151+
expectedOnPublish
152+
);
153+
await runWrangler(
154+
"pubsub brokers update my-broker --namespace=some-namespace --expiration=24h --description='hello' --on-publish-url='https://foo.bar.example.com'"
155+
);
156+
157+
expect(std.err).toMatchInlineSnapshot(`""`);
158+
// TODO(elithrar): check returned object
159+
expect(requests.count).toEqual(1);
160+
});
161+
});
162+
163+
describe("list", () => {
164+
function mockListRequest(brokers: PubSubBroker[]) {
165+
const requests = { count: 0 };
166+
setMockResponse(
167+
"/accounts/:accountId/pubsub/namespaces/:namespaceName/brokers",
168+
([_url, accountId, namespaceName], init) => {
169+
requests.count++;
170+
expect(accountId).toEqual("some-account-id");
171+
expect(namespaceName).toEqual("some-namespace");
172+
expect(init).toEqual({});
173+
return { brokers };
174+
}
175+
);
176+
return requests;
177+
}
178+
179+
it("should list brokers", async () => {
180+
const expectedBrokers: PubSubBroker[] = [
181+
{ name: "broker-1", created_on: "01-01-2001" },
182+
{ name: "broker-2", created_on: "01-01-2001" },
183+
];
184+
const requests = mockListRequest(expectedBrokers);
185+
await runWrangler("pubsub brokers list --namespace=some-namespace");
186+
187+
expect(std.err).toMatchInlineSnapshot(`""`);
188+
// TODO(elithrar): check returned object
189+
expect(requests.count).toEqual(1);
190+
});
191+
});
192+
193+
describe("describe", () => {
194+
function mockGetRequest(broker: PubSubBroker) {
195+
const requests = { count: 0 };
196+
setMockResponse(
197+
"/accounts/:accountId/pubsub/namespaces/:namespaceName/brokers/:brokerName",
198+
([_url, accountId, namespaceName, brokerName]) => {
199+
requests.count++;
200+
expect(accountId).toEqual("some-account-id");
201+
expect(namespaceName).toEqual("some-namespace");
202+
expect(brokerName).toEqual(broker.name);
203+
return { result: broker };
204+
}
205+
);
206+
return requests;
207+
}
208+
209+
it("should describe a single broker", async () => {
210+
const requests = mockGetRequest({ id: "1234", name: "my-broker" });
211+
await runWrangler(
212+
"pubsub brokers describe my-broker --namespace=some-namespace"
213+
);
214+
215+
expect(std.err).toMatchInlineSnapshot(`""`);
216+
// TODO(elithrar): check returned object
217+
expect(requests.count).toEqual(1);
218+
});
219+
});
220+
221+
describe("issue", () => {
222+
function mockIssueRequest(expectedBrokerName: string) {
223+
const requests = { count: 0 };
224+
setMockResponse(
225+
"/accounts/:accountId/pubsub/namespaces/:namespaceName/brokers/:brokerName/credentials",
226+
([_url, accountId, namespaceName, brokerName]) => {
227+
expect(accountId).toEqual("some-account-id");
228+
expect(namespaceName).toEqual("some-namespace");
229+
expect(brokerName).toEqual(expectedBrokerName);
230+
requests.count += 1;
231+
}
232+
);
233+
return requests;
234+
}
235+
236+
it("should issue a token for the broker", async () => {
237+
const requests = mockIssueRequest("my-broker");
238+
await runWrangler(
239+
"pubsub brokers issue my-broker --namespace=some-namespace"
240+
);
241+
242+
expect(std.err).toMatchInlineSnapshot(`""`);
243+
// TODO(elithrar): check returned object
244+
expect(requests.count).toEqual(1);
245+
});
246+
});
247+
248+
describe("public-keys", () => {
249+
function mockIssueRequest(expectedBrokerName: string) {
250+
const requests = { count: 0 };
251+
setMockResponse(
252+
"/accounts/:accountId/pubsub/namespaces/:namespaceName/brokers/:brokerName/publickeys",
253+
([_url, accountId, namespaceName, brokerName]) => {
254+
expect(accountId).toEqual("some-account-id");
255+
expect(namespaceName).toEqual("some-namespace");
256+
expect(brokerName).toEqual(expectedBrokerName);
257+
requests.count += 1;
258+
}
259+
);
260+
return requests;
261+
}
262+
263+
it("should return the public keys for a broker", async () => {
264+
const requests = mockIssueRequest("my-broker");
265+
await runWrangler(
266+
"pubsub brokers public-keys my-broker --namespace=some-namespace"
267+
);
268+
269+
expect(std.err).toMatchInlineSnapshot(`""`);
270+
// TODO(elithrar): check returned object
271+
expect(requests.count).toEqual(1);
272+
});
273+
});
274+
});
275+
});
276+
});

packages/wrangler/src/index.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
} from "./parse";
4444
import { previewHandler, previewOptions } from "./preview";
4545
import publish from "./publish";
46+
import { pubSubCommands } from "./pubsub/pubsub-commands";
4647
import { createR2Bucket, deleteR2Bucket, listR2Buckets } from "./r2";
4748
import { getAssetPaths, getSiteAssetPaths } from "./sites";
4849
import {
@@ -1693,6 +1694,14 @@ function createCLIParser(argv: string[]) {
16931694
});
16941695
});
16951696

1697+
wrangler.command(
1698+
"pubsub",
1699+
"📮 Interact and manage Pub/Sub Brokers",
1700+
(pubsubYargs) => {
1701+
return pubSubCommands(pubsubYargs, subHelp);
1702+
}
1703+
);
1704+
16961705
/**
16971706
* User Group: login, logout, and whoami
16981707
* TODO: group commands into User group similar to .group() for flags in yargs

packages/wrangler/src/parse.ts

+64
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,67 @@ export function searchLocation(file: File, query: unknown): Location {
220220
}
221221
return { lineText, line, column, length, ...file };
222222
}
223+
224+
const units = {
225+
nanoseconds: 0.000000001,
226+
nanosecond: 0.000000001,
227+
microseconds: 0.000001,
228+
microsecond: 0.000001,
229+
milliseconds: 0.001,
230+
millisecond: 0.001,
231+
seconds: 1,
232+
second: 1,
233+
minutes: 60,
234+
minute: 60,
235+
hours: 3600,
236+
hour: 3600,
237+
days: 86400,
238+
day: 86400,
239+
weeks: 604800,
240+
week: 604800,
241+
month: 18144000,
242+
year: 220752000,
243+
244+
nsecs: 0.000000001,
245+
nsec: 0.000000001,
246+
usecs: 0.000001,
247+
usec: 0.000001,
248+
msecs: 0.001,
249+
msec: 0.001,
250+
secs: 1,
251+
sec: 1,
252+
mins: 60,
253+
min: 60,
254+
255+
ns: 0.000000001,
256+
us: 0.000001,
257+
ms: 0.001,
258+
mo: 18144000,
259+
yr: 220752000,
260+
261+
s: 1,
262+
m: 60,
263+
h: 3600,
264+
d: 86400,
265+
w: 604800,
266+
y: 220752000,
267+
};
268+
269+
/**
270+
* Parse a human-readable time duration in seconds (including fractional)
271+
*
272+
* Invalid values will return NaN
273+
*/
274+
export function parseHumanDuration(s: string): number {
275+
const unitsMap = new Map(Object.entries(units));
276+
s = s.trim().toLowerCase();
277+
let base = 1;
278+
for (const [name, _] of unitsMap) {
279+
if (s.endsWith(name)) {
280+
s = s.substring(0, s.length - name.length);
281+
base = unitsMap.get(name) || 1;
282+
break;
283+
}
284+
}
285+
return Number(s) * base;
286+
}

0 commit comments

Comments
 (0)