puls-dev 0.3.5 ā 0.3.7
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/README.md +165 -54
- package/dist/bin/install-shell.js +5 -6
- package/dist/bin/puls.js +10 -3
- package/dist/core/config.d.ts +4 -0
- package/dist/core/decorators.d.ts +4 -0
- package/dist/core/decorators.js +2 -0
- package/dist/core/parallel.test.js +4 -3
- package/dist/core/resource.d.ts +2 -1
- package/dist/core/resource.js +4 -2
- package/dist/core/stack.d.ts +4 -0
- package/dist/core/stack.js +8 -8
- package/dist/providers/aws/acm.test.d.ts +1 -0
- package/dist/providers/aws/acm.test.js +167 -0
- package/dist/providers/aws/cloudfront.test.d.ts +1 -0
- package/dist/providers/aws/cloudfront.test.js +170 -0
- package/dist/providers/aws/fargate.test.d.ts +1 -0
- package/dist/providers/aws/fargate.test.js +244 -0
- package/dist/providers/aws/rds.test.d.ts +1 -0
- package/dist/providers/aws/rds.test.js +219 -0
- package/dist/providers/aws/sqs.test.d.ts +1 -0
- package/dist/providers/aws/sqs.test.js +181 -0
- package/dist/providers/cloudflare/api.d.ts +15 -0
- package/dist/providers/cloudflare/api.js +199 -0
- package/dist/providers/cloudflare/index.d.ts +14 -0
- package/dist/providers/cloudflare/index.js +19 -0
- package/dist/providers/cloudflare/kv.d.ts +20 -0
- package/dist/providers/cloudflare/kv.js +69 -0
- package/dist/providers/cloudflare/kv.test.d.ts +1 -0
- package/dist/providers/cloudflare/kv.test.js +134 -0
- package/dist/providers/cloudflare/r2.d.ts +14 -0
- package/dist/providers/cloudflare/r2.js +57 -0
- package/dist/providers/cloudflare/r2.test.d.ts +1 -0
- package/dist/providers/cloudflare/r2.test.js +132 -0
- package/dist/providers/cloudflare/worker.d.ts +28 -0
- package/dist/providers/cloudflare/worker.js +172 -0
- package/dist/providers/cloudflare/worker.test.d.ts +1 -0
- package/dist/providers/cloudflare/worker.test.js +220 -0
- package/dist/providers/cloudflare/zone.d.ts +42 -0
- package/dist/providers/cloudflare/zone.js +280 -0
- package/dist/providers/cloudflare/zone.test.d.ts +1 -0
- package/dist/providers/cloudflare/zone.test.js +284 -0
- package/dist/providers/firebase/auth.test.d.ts +1 -0
- package/dist/providers/firebase/auth.test.js +145 -0
- package/dist/providers/firebase/hosting.test.js +7 -6
- package/dist/providers/firebase/storage.test.d.ts +1 -0
- package/dist/providers/firebase/storage.test.js +148 -0
- package/package.json +6 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { Output } from "../../core/output.js";
|
|
3
|
+
import { getCloudflareApi, getCloudflareAccountId } from "./api.js";
|
|
4
|
+
export class KVBuilder extends BaseBuilder {
|
|
5
|
+
title;
|
|
6
|
+
out = {
|
|
7
|
+
id: new Output(),
|
|
8
|
+
};
|
|
9
|
+
resolvedId = null;
|
|
10
|
+
constructor(title) {
|
|
11
|
+
super(title);
|
|
12
|
+
this.title = title;
|
|
13
|
+
this.discoveryPromise = this.discoverNamespace(title);
|
|
14
|
+
}
|
|
15
|
+
async discoverNamespace(title) {
|
|
16
|
+
try {
|
|
17
|
+
const api = getCloudflareApi();
|
|
18
|
+
const accountId = getCloudflareAccountId();
|
|
19
|
+
const res = await api.get(`/accounts/${accountId}/workers/namespaces?per_page=100`);
|
|
20
|
+
return (res.result ?? []).find((n) => n.title === title) ?? null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async deploy() {
|
|
27
|
+
const dryRun = this.isDryRunActive();
|
|
28
|
+
const existing = await this.discoveryPromise;
|
|
29
|
+
const api = getCloudflareApi();
|
|
30
|
+
const accountId = getCloudflareAccountId();
|
|
31
|
+
console.log(`\nš¦ Finalizing Cloudflare KV Namespace "${this.title}"...`);
|
|
32
|
+
if (existing) {
|
|
33
|
+
this.resolvedId = existing.id;
|
|
34
|
+
this.out.id.resolve(existing.id);
|
|
35
|
+
console.log(` ā
KV Namespace "${this.title}" already exists (id=${existing.id})`);
|
|
36
|
+
return { title: this.title, id: this.resolvedId };
|
|
37
|
+
}
|
|
38
|
+
if (dryRun) {
|
|
39
|
+
console.log(` š [PLAN] Create KV Namespace "${this.title}"`);
|
|
40
|
+
this.out.id.resolve("PENDING");
|
|
41
|
+
return { title: this.title, id: "PENDING" };
|
|
42
|
+
}
|
|
43
|
+
const res = await api.post(`/accounts/${accountId}/workers/namespaces`, {
|
|
44
|
+
title: this.title,
|
|
45
|
+
});
|
|
46
|
+
this.resolvedId = res.result.id;
|
|
47
|
+
this.out.id.resolve(this.resolvedId);
|
|
48
|
+
console.log(`š Created KV Namespace "${this.title}" (id=${this.resolvedId})`);
|
|
49
|
+
return { title: this.title, id: this.resolvedId };
|
|
50
|
+
}
|
|
51
|
+
async destroy() {
|
|
52
|
+
const dryRun = this.isDryRunActive();
|
|
53
|
+
const existing = await this.discoveryPromise;
|
|
54
|
+
const api = getCloudflareApi();
|
|
55
|
+
const accountId = getCloudflareAccountId();
|
|
56
|
+
console.log(`\nšļø Destroying Cloudflare KV Namespace "${this.title}"...`);
|
|
57
|
+
if (!existing) {
|
|
58
|
+
console.log(` ā KV Namespace "${this.title}" not found`);
|
|
59
|
+
return { destroyed: false };
|
|
60
|
+
}
|
|
61
|
+
if (dryRun) {
|
|
62
|
+
console.log(` š [PLAN] Delete KV Namespace "${this.title}" (id=${existing.id})`);
|
|
63
|
+
return { destroyed: this.title };
|
|
64
|
+
}
|
|
65
|
+
await api.delete(`/accounts/${accountId}/workers/namespaces/${existing.id}`);
|
|
66
|
+
console.log(` šļø Removed KV Namespace "${this.title}" (id=${existing.id})`);
|
|
67
|
+
return { destroyed: this.title };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { KVBuilder } from "./kv.js";
|
|
4
|
+
import { Config } from "../../core/config.js";
|
|
5
|
+
describe("KVBuilder Unit Tests", () => {
|
|
6
|
+
let originalFetch;
|
|
7
|
+
let fetchCalls = [];
|
|
8
|
+
let mockResponses = {};
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
Config.set({
|
|
11
|
+
dryRun: false,
|
|
12
|
+
providers: {
|
|
13
|
+
cloudflare: { token: "fake-cf-token", accountId: "fake-cf-account" }
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
originalFetch = globalThis.fetch;
|
|
17
|
+
fetchCalls = [];
|
|
18
|
+
mockResponses = {};
|
|
19
|
+
globalThis.fetch = async (input, init) => {
|
|
20
|
+
const url = String(input);
|
|
21
|
+
const method = init?.method ?? "GET";
|
|
22
|
+
let body;
|
|
23
|
+
if (init?.body) {
|
|
24
|
+
if (typeof init.body === "string") {
|
|
25
|
+
try {
|
|
26
|
+
body = JSON.parse(init.body);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
body = init.body;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
body = init.body;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const headers = init?.headers;
|
|
37
|
+
fetchCalls.push({ url, method, body, headers });
|
|
38
|
+
const matchKey = Object.keys(mockResponses).find(key => {
|
|
39
|
+
const [mMethod, mPath] = key.split(" ");
|
|
40
|
+
return method === mMethod && url.endsWith(mPath);
|
|
41
|
+
});
|
|
42
|
+
if (matchKey) {
|
|
43
|
+
const resp = mockResponses[matchKey];
|
|
44
|
+
return {
|
|
45
|
+
ok: resp.status >= 200 && resp.status < 300,
|
|
46
|
+
status: resp.status,
|
|
47
|
+
json: async () => resp.body,
|
|
48
|
+
text: async () => JSON.stringify(resp.body),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
status: 404,
|
|
54
|
+
json: async () => ({ errors: [{ message: "Not found" }] }),
|
|
55
|
+
text: async () => "Not found",
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
globalThis.fetch = originalFetch;
|
|
61
|
+
});
|
|
62
|
+
test("discovers namespace successfully if it already exists", async () => {
|
|
63
|
+
mockResponses["GET /accounts/fake-cf-account/workers/namespaces?per_page=100"] = {
|
|
64
|
+
status: 200,
|
|
65
|
+
body: {
|
|
66
|
+
result: [
|
|
67
|
+
{ id: "kv-123", title: "my-namespace" }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const builder = new KVBuilder("my-namespace");
|
|
72
|
+
const result = await builder.deploy();
|
|
73
|
+
assert.strictEqual(result.id, "kv-123");
|
|
74
|
+
assert.strictEqual(builder.resolvedId, "kv-123");
|
|
75
|
+
// Deploying again shouldn't send post
|
|
76
|
+
const deployAgainResult = await builder.deploy();
|
|
77
|
+
assert.strictEqual(deployAgainResult.id, "kv-123");
|
|
78
|
+
const posts = fetchCalls.filter(c => c.method === "POST");
|
|
79
|
+
assert.strictEqual(posts.length, 0);
|
|
80
|
+
});
|
|
81
|
+
test("creates namespace if it does not exist", async () => {
|
|
82
|
+
mockResponses["GET /accounts/fake-cf-account/workers/namespaces?per_page=100"] = {
|
|
83
|
+
status: 200,
|
|
84
|
+
body: { result: [] }
|
|
85
|
+
};
|
|
86
|
+
mockResponses["POST /accounts/fake-cf-account/workers/namespaces"] = {
|
|
87
|
+
status: 200,
|
|
88
|
+
body: { result: { id: "kv-created-456" } }
|
|
89
|
+
};
|
|
90
|
+
const builder = new KVBuilder("my-namespace");
|
|
91
|
+
const result = await builder.deploy();
|
|
92
|
+
assert.strictEqual(result.id, "kv-created-456");
|
|
93
|
+
assert.strictEqual(builder.resolvedId, "kv-created-456");
|
|
94
|
+
const postCall = fetchCalls.find(c => c.method === "POST");
|
|
95
|
+
assert.ok(postCall);
|
|
96
|
+
assert.deepStrictEqual(postCall.body, { title: "my-namespace" });
|
|
97
|
+
});
|
|
98
|
+
test("does not call fetch during dryRun deploy", async () => {
|
|
99
|
+
Config.set({
|
|
100
|
+
dryRun: true,
|
|
101
|
+
providers: {
|
|
102
|
+
cloudflare: { token: "fake-cf-token", accountId: "fake-cf-account" }
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
mockResponses["GET /accounts/fake-cf-account/workers/namespaces?per_page=100"] = {
|
|
106
|
+
status: 200,
|
|
107
|
+
body: { result: [] }
|
|
108
|
+
};
|
|
109
|
+
const builder = new KVBuilder("my-namespace");
|
|
110
|
+
const result = await builder.deploy();
|
|
111
|
+
assert.strictEqual(result.id, "PENDING");
|
|
112
|
+
const posts = fetchCalls.filter(c => c.method === "POST");
|
|
113
|
+
assert.strictEqual(posts.length, 0);
|
|
114
|
+
});
|
|
115
|
+
test("destroys namespace successfully if exists", async () => {
|
|
116
|
+
mockResponses["GET /accounts/fake-cf-account/workers/namespaces?per_page=100"] = {
|
|
117
|
+
status: 200,
|
|
118
|
+
body: {
|
|
119
|
+
result: [
|
|
120
|
+
{ id: "kv-123", title: "my-namespace" }
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
mockResponses["DELETE /accounts/fake-cf-account/workers/namespaces/kv-123"] = {
|
|
125
|
+
status: 200,
|
|
126
|
+
body: { success: true }
|
|
127
|
+
};
|
|
128
|
+
const builder = new KVBuilder("my-namespace");
|
|
129
|
+
const result = await builder.destroy();
|
|
130
|
+
assert.deepStrictEqual(result, { destroyed: "my-namespace" });
|
|
131
|
+
const deleteCall = fetchCalls.find(c => c.method === "DELETE");
|
|
132
|
+
assert.ok(deleteCall);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
export declare class R2Builder extends BaseBuilder {
|
|
3
|
+
bucketName: string;
|
|
4
|
+
constructor(bucketName: string);
|
|
5
|
+
private discoverBucket;
|
|
6
|
+
deploy(): Promise<{
|
|
7
|
+
bucket: string;
|
|
8
|
+
}>;
|
|
9
|
+
destroy(): Promise<{
|
|
10
|
+
destroyed: boolean;
|
|
11
|
+
} | {
|
|
12
|
+
destroyed: string;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { getCloudflareApi, getCloudflareAccountId } from "./api.js";
|
|
3
|
+
export class R2Builder extends BaseBuilder {
|
|
4
|
+
bucketName;
|
|
5
|
+
constructor(bucketName) {
|
|
6
|
+
super(bucketName);
|
|
7
|
+
this.bucketName = bucketName;
|
|
8
|
+
this.discoveryPromise = this.discoverBucket(bucketName);
|
|
9
|
+
}
|
|
10
|
+
async discoverBucket(name) {
|
|
11
|
+
try {
|
|
12
|
+
const api = getCloudflareApi();
|
|
13
|
+
const accountId = getCloudflareAccountId();
|
|
14
|
+
const res = await api.get(`/accounts/${accountId}/r2/buckets`);
|
|
15
|
+
return (res.result?.buckets ?? []).find((b) => b.name === name) ?? null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async deploy() {
|
|
22
|
+
const dryRun = this.isDryRunActive();
|
|
23
|
+
const existing = await this.discoveryPromise;
|
|
24
|
+
const api = getCloudflareApi();
|
|
25
|
+
const accountId = getCloudflareAccountId();
|
|
26
|
+
console.log(`\nšŖ£ Finalizing Cloudflare R2 Bucket "${this.bucketName}"...`);
|
|
27
|
+
if (existing) {
|
|
28
|
+
console.log(` ā
R2 Bucket "${this.bucketName}" already exists`);
|
|
29
|
+
return { bucket: this.bucketName };
|
|
30
|
+
}
|
|
31
|
+
if (dryRun) {
|
|
32
|
+
console.log(` š [PLAN] Create R2 Bucket "${this.bucketName}"`);
|
|
33
|
+
return { bucket: this.bucketName };
|
|
34
|
+
}
|
|
35
|
+
await api.put(`/accounts/${accountId}/r2/buckets/${this.bucketName}`, {});
|
|
36
|
+
console.log(`š Created R2 Bucket "${this.bucketName}"`);
|
|
37
|
+
return { bucket: this.bucketName };
|
|
38
|
+
}
|
|
39
|
+
async destroy() {
|
|
40
|
+
const dryRun = this.isDryRunActive();
|
|
41
|
+
const existing = await this.discoveryPromise;
|
|
42
|
+
const api = getCloudflareApi();
|
|
43
|
+
const accountId = getCloudflareAccountId();
|
|
44
|
+
console.log(`\nšļø Destroying Cloudflare R2 Bucket "${this.bucketName}"...`);
|
|
45
|
+
if (!existing) {
|
|
46
|
+
console.log(` ā R2 Bucket "${this.bucketName}" not found`);
|
|
47
|
+
return { destroyed: false };
|
|
48
|
+
}
|
|
49
|
+
if (dryRun) {
|
|
50
|
+
console.log(` š [PLAN] Delete R2 Bucket "${this.bucketName}"`);
|
|
51
|
+
return { destroyed: this.bucketName };
|
|
52
|
+
}
|
|
53
|
+
await api.delete(`/accounts/${accountId}/r2/buckets/${this.bucketName}`);
|
|
54
|
+
console.log(` šļø Removed R2 Bucket "${this.bucketName}"`);
|
|
55
|
+
return { destroyed: this.bucketName };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { R2Builder } from "./r2.js";
|
|
4
|
+
import { Config } from "../../core/config.js";
|
|
5
|
+
describe("R2Builder Unit Tests", () => {
|
|
6
|
+
let originalFetch;
|
|
7
|
+
let fetchCalls = [];
|
|
8
|
+
let mockResponses = {};
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
Config.set({
|
|
11
|
+
dryRun: false,
|
|
12
|
+
providers: {
|
|
13
|
+
cloudflare: { token: "fake-cf-token", accountId: "fake-cf-account" }
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
originalFetch = globalThis.fetch;
|
|
17
|
+
fetchCalls = [];
|
|
18
|
+
mockResponses = {};
|
|
19
|
+
globalThis.fetch = async (input, init) => {
|
|
20
|
+
const url = String(input);
|
|
21
|
+
const method = init?.method ?? "GET";
|
|
22
|
+
let body;
|
|
23
|
+
if (init?.body) {
|
|
24
|
+
if (typeof init.body === "string") {
|
|
25
|
+
try {
|
|
26
|
+
body = JSON.parse(init.body);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
body = init.body;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
body = init.body;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const headers = init?.headers;
|
|
37
|
+
fetchCalls.push({ url, method, body, headers });
|
|
38
|
+
const matchKey = Object.keys(mockResponses).find(key => {
|
|
39
|
+
const [mMethod, mPath] = key.split(" ");
|
|
40
|
+
return method === mMethod && url.endsWith(mPath);
|
|
41
|
+
});
|
|
42
|
+
if (matchKey) {
|
|
43
|
+
const resp = mockResponses[matchKey];
|
|
44
|
+
return {
|
|
45
|
+
ok: resp.status >= 200 && resp.status < 300,
|
|
46
|
+
status: resp.status,
|
|
47
|
+
json: async () => resp.body,
|
|
48
|
+
text: async () => JSON.stringify(resp.body),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
status: 404,
|
|
54
|
+
json: async () => ({ errors: [{ message: "Not found" }] }),
|
|
55
|
+
text: async () => "Not found",
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
globalThis.fetch = originalFetch;
|
|
61
|
+
});
|
|
62
|
+
test("discovers bucket successfully if it already exists", async () => {
|
|
63
|
+
mockResponses["GET /accounts/fake-cf-account/r2/buckets"] = {
|
|
64
|
+
status: 200,
|
|
65
|
+
body: {
|
|
66
|
+
result: {
|
|
67
|
+
buckets: [
|
|
68
|
+
{ name: "my-bucket" }
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const builder = new R2Builder("my-bucket");
|
|
74
|
+
const result = await builder.deploy();
|
|
75
|
+
assert.deepStrictEqual(result, { bucket: "my-bucket" });
|
|
76
|
+
const puts = fetchCalls.filter(c => c.method === "PUT");
|
|
77
|
+
assert.strictEqual(puts.length, 0);
|
|
78
|
+
});
|
|
79
|
+
test("creates bucket if it does not exist", async () => {
|
|
80
|
+
mockResponses["GET /accounts/fake-cf-account/r2/buckets"] = {
|
|
81
|
+
status: 200,
|
|
82
|
+
body: { result: { buckets: [] } }
|
|
83
|
+
};
|
|
84
|
+
mockResponses["PUT /accounts/fake-cf-account/r2/buckets/my-bucket"] = {
|
|
85
|
+
status: 200,
|
|
86
|
+
body: {}
|
|
87
|
+
};
|
|
88
|
+
const builder = new R2Builder("my-bucket");
|
|
89
|
+
const result = await builder.deploy();
|
|
90
|
+
assert.deepStrictEqual(result, { bucket: "my-bucket" });
|
|
91
|
+
const putCall = fetchCalls.find(c => c.method === "PUT");
|
|
92
|
+
assert.ok(putCall);
|
|
93
|
+
});
|
|
94
|
+
test("does not call fetch during dryRun deploy", async () => {
|
|
95
|
+
Config.set({
|
|
96
|
+
dryRun: true,
|
|
97
|
+
providers: {
|
|
98
|
+
cloudflare: { token: "fake-cf-token", accountId: "fake-cf-account" }
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
mockResponses["GET /accounts/fake-cf-account/r2/buckets"] = {
|
|
102
|
+
status: 200,
|
|
103
|
+
body: { result: { buckets: [] } }
|
|
104
|
+
};
|
|
105
|
+
const builder = new R2Builder("my-bucket");
|
|
106
|
+
const result = await builder.deploy();
|
|
107
|
+
assert.deepStrictEqual(result, { bucket: "my-bucket" });
|
|
108
|
+
const puts = fetchCalls.filter(c => c.method === "PUT");
|
|
109
|
+
assert.strictEqual(puts.length, 0);
|
|
110
|
+
});
|
|
111
|
+
test("destroys bucket successfully if exists", async () => {
|
|
112
|
+
mockResponses["GET /accounts/fake-cf-account/r2/buckets"] = {
|
|
113
|
+
status: 200,
|
|
114
|
+
body: {
|
|
115
|
+
result: {
|
|
116
|
+
buckets: [
|
|
117
|
+
{ name: "my-bucket" }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
mockResponses["DELETE /accounts/fake-cf-account/r2/buckets/my-bucket"] = {
|
|
123
|
+
status: 200,
|
|
124
|
+
body: {}
|
|
125
|
+
};
|
|
126
|
+
const builder = new R2Builder("my-bucket");
|
|
127
|
+
const result = await builder.destroy();
|
|
128
|
+
assert.deepStrictEqual(result, { destroyed: "my-bucket" });
|
|
129
|
+
const deleteCall = fetchCalls.find(c => c.method === "DELETE");
|
|
130
|
+
assert.ok(deleteCall);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { Output } from "../../core/output.js";
|
|
3
|
+
import { KVBuilder } from "./kv.js";
|
|
4
|
+
import { R2Builder } from "./r2.js";
|
|
5
|
+
export declare class WorkerBuilder extends BaseBuilder {
|
|
6
|
+
scriptName: string;
|
|
7
|
+
private _scriptPath?;
|
|
8
|
+
private _routes;
|
|
9
|
+
private _kvs;
|
|
10
|
+
private _r2s;
|
|
11
|
+
private _envs;
|
|
12
|
+
constructor(scriptName: string);
|
|
13
|
+
script(filePath: string): this;
|
|
14
|
+
route(pattern: string): this;
|
|
15
|
+
kv(bindingName: string, kvNamespace: KVBuilder): this;
|
|
16
|
+
r2(bindingName: string, r2Bucket: R2Builder): this;
|
|
17
|
+
env(bindingName: string, value: string | Output<string>): this;
|
|
18
|
+
deploy(): Promise<{
|
|
19
|
+
scriptName: string;
|
|
20
|
+
routes?: undefined;
|
|
21
|
+
} | {
|
|
22
|
+
scriptName: string;
|
|
23
|
+
routes: string[];
|
|
24
|
+
}>;
|
|
25
|
+
destroy(): Promise<{
|
|
26
|
+
destroyed: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
3
|
+
import { Output } from "../../core/output.js";
|
|
4
|
+
import { getCloudflareApi, getCloudflareAccountId } from "./api.js";
|
|
5
|
+
async function getZoneIdForPattern(pattern, api) {
|
|
6
|
+
let host = pattern.split("/")[0];
|
|
7
|
+
const parts = host.split(".");
|
|
8
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
9
|
+
const possibleZone = parts.slice(i).join(".");
|
|
10
|
+
try {
|
|
11
|
+
const res = await api.get(`/zones?name=${possibleZone}`);
|
|
12
|
+
if (res.result && res.result.length > 0) {
|
|
13
|
+
return res.result[0].id;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Ignore and try next parent domain
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Could not find Cloudflare DNS Zone for route pattern "${pattern}"`);
|
|
21
|
+
}
|
|
22
|
+
export class WorkerBuilder extends BaseBuilder {
|
|
23
|
+
scriptName;
|
|
24
|
+
_scriptPath;
|
|
25
|
+
_routes = [];
|
|
26
|
+
_kvs = new Map();
|
|
27
|
+
_r2s = new Map();
|
|
28
|
+
_envs = new Map();
|
|
29
|
+
constructor(scriptName) {
|
|
30
|
+
super(scriptName);
|
|
31
|
+
this.scriptName = scriptName;
|
|
32
|
+
}
|
|
33
|
+
script(filePath) {
|
|
34
|
+
this._scriptPath = filePath;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
route(pattern) {
|
|
38
|
+
this._routes.push(pattern);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
kv(bindingName, kvNamespace) {
|
|
42
|
+
this._kvs.set(bindingName, kvNamespace);
|
|
43
|
+
this.dependsOn(kvNamespace);
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
r2(bindingName, r2Bucket) {
|
|
47
|
+
this._r2s.set(bindingName, r2Bucket);
|
|
48
|
+
this.dependsOn(r2Bucket);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
env(bindingName, value) {
|
|
52
|
+
this._envs.set(bindingName, value);
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
async deploy() {
|
|
56
|
+
if (!this._scriptPath) {
|
|
57
|
+
throw new Error(`Worker script path is not configured for "${this.name}". Call .script("filePath")`);
|
|
58
|
+
}
|
|
59
|
+
const dryRun = this.isDryRunActive();
|
|
60
|
+
const api = getCloudflareApi();
|
|
61
|
+
const accountId = getCloudflareAccountId();
|
|
62
|
+
console.log(`\nā” Finalizing Cloudflare Worker "${this.scriptName}"...`);
|
|
63
|
+
const metadata = {
|
|
64
|
+
main_module: "index.js",
|
|
65
|
+
bindings: [],
|
|
66
|
+
};
|
|
67
|
+
// Resolve KV Bindings
|
|
68
|
+
for (const [binding, kv] of this._kvs.entries()) {
|
|
69
|
+
const kvId = dryRun ? "mock-kv-id" : await kv.out.id.get();
|
|
70
|
+
metadata.bindings.push({
|
|
71
|
+
type: "kv_namespace",
|
|
72
|
+
name: binding,
|
|
73
|
+
namespace_id: kvId,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Resolve R2 Bindings
|
|
77
|
+
for (const [binding, r2] of this._r2s.entries()) {
|
|
78
|
+
metadata.bindings.push({
|
|
79
|
+
type: "r2_bucket",
|
|
80
|
+
name: binding,
|
|
81
|
+
bucket_name: r2.bucketName,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Resolve Env Bindings
|
|
85
|
+
for (const [binding, val] of this._envs.entries()) {
|
|
86
|
+
const resolvedVal = val instanceof Output ? await val.get() : val;
|
|
87
|
+
metadata.bindings.push({
|
|
88
|
+
type: "plain_text",
|
|
89
|
+
name: binding,
|
|
90
|
+
text: resolvedVal,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (dryRun) {
|
|
94
|
+
console.log(` š [PLAN] Upload Worker script "${this.scriptName}"`);
|
|
95
|
+
for (const r of this._routes) {
|
|
96
|
+
console.log(` āā Route: ${r}`);
|
|
97
|
+
}
|
|
98
|
+
return { scriptName: this.scriptName };
|
|
99
|
+
}
|
|
100
|
+
// Load and build FormData
|
|
101
|
+
const scriptContent = readFileSync(this._scriptPath, "utf8");
|
|
102
|
+
const form = new FormData();
|
|
103
|
+
form.append("metadata", JSON.stringify(metadata));
|
|
104
|
+
form.append("script", new Blob([scriptContent], { type: "application/javascript+module" }), "index.js");
|
|
105
|
+
await api.put(`/accounts/${accountId}/workers/scripts/${this.scriptName}`, form);
|
|
106
|
+
console.log(`š Uploaded Worker script "${this.scriptName}"`);
|
|
107
|
+
// Reconcile Routes
|
|
108
|
+
for (const pattern of this._routes) {
|
|
109
|
+
const zoneId = await getZoneIdForPattern(pattern, api);
|
|
110
|
+
const routesRes = await api.get(`/zones/${zoneId}/workers/routes`);
|
|
111
|
+
const existingRoute = (routesRes.result ?? []).find((r) => r.pattern === pattern);
|
|
112
|
+
if (existingRoute) {
|
|
113
|
+
if (existingRoute.script !== this.scriptName) {
|
|
114
|
+
await api.put(`/zones/${zoneId}/workers/routes/${existingRoute.id}`, {
|
|
115
|
+
pattern,
|
|
116
|
+
script: this.scriptName,
|
|
117
|
+
});
|
|
118
|
+
console.log(` š Updated route ${pattern} ā ${this.scriptName}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.log(` ā
Route ${pattern} is up to date`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
await api.post(`/zones/${zoneId}/workers/routes`, {
|
|
126
|
+
pattern,
|
|
127
|
+
script: this.scriptName,
|
|
128
|
+
});
|
|
129
|
+
console.log(` š Created route ${pattern} ā ${this.scriptName}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { scriptName: this.scriptName, routes: this._routes };
|
|
133
|
+
}
|
|
134
|
+
async destroy() {
|
|
135
|
+
const dryRun = this.isDryRunActive();
|
|
136
|
+
const api = getCloudflareApi();
|
|
137
|
+
const accountId = getCloudflareAccountId();
|
|
138
|
+
console.log(`\nšļø Destroying Cloudflare Worker "${this.scriptName}"...`);
|
|
139
|
+
if (dryRun) {
|
|
140
|
+
console.log(` š [PLAN] Delete Worker script "${this.scriptName}"`);
|
|
141
|
+
return { destroyed: this.scriptName };
|
|
142
|
+
}
|
|
143
|
+
// Clean up routes pointing to this script
|
|
144
|
+
if (this._routes.length > 0) {
|
|
145
|
+
for (const pattern of this._routes) {
|
|
146
|
+
try {
|
|
147
|
+
const zoneId = await getZoneIdForPattern(pattern, api);
|
|
148
|
+
const routesRes = await api.get(`/zones/${zoneId}/workers/routes`);
|
|
149
|
+
const match = (routesRes.result ?? []).find((r) => r.pattern === pattern && r.script === this.scriptName);
|
|
150
|
+
if (match) {
|
|
151
|
+
await api.delete(`/zones/${zoneId}/workers/routes/${match.id}`);
|
|
152
|
+
console.log(` šļø Removed route ${pattern}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Ignore if zone or route is already gone
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await api.delete(`/accounts/${accountId}/workers/scripts/${this.scriptName}`);
|
|
162
|
+
console.log(` šļø Removed Worker script "${this.scriptName}"`);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
// Ignore if already deleted
|
|
166
|
+
if (!err.message.includes("404"))
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
await this.destroySidecars();
|
|
170
|
+
return { destroyed: this.scriptName };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|