puls-dev 0.2.7 → 0.2.9

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 (117) hide show
  1. package/dist/core/checker.js +71 -0
  2. package/dist/core/config.d.ts +6 -0
  3. package/dist/core/config.js +11 -1
  4. package/dist/core/context.d.ts +14 -0
  5. package/dist/core/context.js +2 -0
  6. package/dist/core/decorators.d.ts +4 -0
  7. package/dist/core/decorators.js +56 -30
  8. package/dist/core/hooks.d.ts +21 -0
  9. package/dist/core/hooks.js +116 -0
  10. package/dist/core/hooks.test.d.ts +1 -0
  11. package/dist/core/hooks.test.js +194 -0
  12. package/dist/core/multiregion.test.d.ts +1 -0
  13. package/dist/core/multiregion.test.js +87 -0
  14. package/dist/core/output.d.ts +2 -0
  15. package/dist/core/output.js +9 -2
  16. package/dist/core/parallel.test.d.ts +1 -0
  17. package/dist/core/parallel.test.js +215 -0
  18. package/dist/core/parser.d.ts +10 -0
  19. package/dist/core/parser.js +140 -0
  20. package/dist/core/parser.test.d.ts +1 -0
  21. package/dist/core/parser.test.js +117 -0
  22. package/dist/core/production.test.d.ts +1 -0
  23. package/dist/core/production.test.js +189 -0
  24. package/dist/core/provisioner.d.ts +4 -0
  25. package/dist/core/provisioner.js +123 -0
  26. package/dist/core/resource.d.ts +23 -0
  27. package/dist/core/resource.js +54 -0
  28. package/dist/core/retry.d.ts +9 -0
  29. package/dist/core/retry.js +28 -0
  30. package/dist/core/retry.test.d.ts +1 -0
  31. package/dist/core/retry.test.js +66 -0
  32. package/dist/core/secret.d.ts +41 -0
  33. package/dist/core/secret.js +105 -0
  34. package/dist/core/secret.test.d.ts +1 -0
  35. package/dist/core/secret.test.js +166 -0
  36. package/dist/core/stack.d.ts +4 -3
  37. package/dist/core/stack.js +322 -48
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.js +3 -0
  40. package/dist/providers/aws/api.js +97 -17
  41. package/dist/providers/aws/ec2.d.ts +51 -0
  42. package/dist/providers/aws/ec2.js +331 -0
  43. package/dist/providers/aws/ec2.test.d.ts +1 -0
  44. package/dist/providers/aws/ec2.test.js +281 -0
  45. package/dist/providers/aws/index.d.ts +4 -0
  46. package/dist/providers/aws/index.js +4 -0
  47. package/dist/providers/aws/route53.d.ts +1 -0
  48. package/dist/providers/aws/route53.js +15 -2
  49. package/dist/providers/aws/route53.test.js +47 -0
  50. package/dist/providers/aws/template.d.ts +34 -0
  51. package/dist/providers/aws/template.js +252 -0
  52. package/dist/providers/aws/template.test.d.ts +1 -0
  53. package/dist/providers/aws/template.test.js +208 -0
  54. package/dist/providers/do/api.d.ts +3 -1
  55. package/dist/providers/do/api.js +126 -27
  56. package/dist/providers/do/app.d.ts +26 -0
  57. package/dist/providers/do/app.js +124 -0
  58. package/dist/providers/do/app.test.d.ts +1 -0
  59. package/dist/providers/do/app.test.js +268 -0
  60. package/dist/providers/do/database.d.ts +44 -0
  61. package/dist/providers/do/database.js +208 -0
  62. package/dist/providers/do/database.test.d.ts +1 -0
  63. package/dist/providers/do/database.test.js +293 -0
  64. package/dist/providers/do/domain.d.ts +2 -0
  65. package/dist/providers/do/domain.js +30 -0
  66. package/dist/providers/do/domain.test.js +49 -0
  67. package/dist/providers/do/droplet.d.ts +9 -0
  68. package/dist/providers/do/droplet.js +146 -8
  69. package/dist/providers/do/droplet.test.js +228 -1
  70. package/dist/providers/do/firewall.d.ts +2 -1
  71. package/dist/providers/do/firewall.js +23 -9
  72. package/dist/providers/do/firewall.test.js +54 -0
  73. package/dist/providers/do/index.d.ts +11 -0
  74. package/dist/providers/do/index.js +8 -0
  75. package/dist/providers/do/spaces.d.ts +27 -0
  76. package/dist/providers/do/spaces.js +142 -0
  77. package/dist/providers/do/spaces.test.d.ts +1 -0
  78. package/dist/providers/do/spaces.test.js +180 -0
  79. package/dist/providers/do/spaces_api.d.ts +2 -0
  80. package/dist/providers/do/spaces_api.js +20 -0
  81. package/dist/providers/do/vpc.d.ts +30 -0
  82. package/dist/providers/do/vpc.js +128 -0
  83. package/dist/providers/do/vpc.test.d.ts +1 -0
  84. package/dist/providers/do/vpc.test.js +258 -0
  85. package/dist/providers/firebase/api.js +92 -29
  86. package/dist/providers/firebase/list.d.ts +2 -0
  87. package/dist/providers/firebase/list.js +25 -0
  88. package/dist/providers/gcp/api.js +88 -14
  89. package/dist/providers/gcp/clouddns.d.ts +1 -0
  90. package/dist/providers/gcp/clouddns.js +15 -2
  91. package/dist/providers/gcp/clouddns.test.js +45 -0
  92. package/dist/providers/gcp/index.d.ts +5 -1
  93. package/dist/providers/gcp/index.js +5 -1
  94. package/dist/providers/gcp/list.d.ts +2 -0
  95. package/dist/providers/gcp/list.js +55 -0
  96. package/dist/providers/gcp/secrets.js +1 -1
  97. package/dist/providers/gcp/template.d.ts +32 -0
  98. package/dist/providers/gcp/template.js +252 -0
  99. package/dist/providers/gcp/template.test.d.ts +1 -0
  100. package/dist/providers/gcp/template.test.js +227 -0
  101. package/dist/providers/gcp/vm.d.ts +48 -0
  102. package/dist/providers/gcp/vm.js +375 -0
  103. package/dist/providers/gcp/vm.test.d.ts +1 -0
  104. package/dist/providers/gcp/vm.test.js +321 -0
  105. package/dist/providers/proxmox/api.d.ts +1 -0
  106. package/dist/providers/proxmox/api.js +72 -16
  107. package/dist/providers/proxmox/index.d.ts +2 -0
  108. package/dist/providers/proxmox/index.js +2 -0
  109. package/dist/providers/proxmox/template.d.ts +44 -0
  110. package/dist/providers/proxmox/template.js +349 -0
  111. package/dist/providers/proxmox/template.test.d.ts +1 -0
  112. package/dist/providers/proxmox/template.test.js +179 -0
  113. package/dist/providers/proxmox/vm.d.ts +7 -4
  114. package/dist/providers/proxmox/vm.js +57 -102
  115. package/dist/providers/proxmox/vm.test.js +77 -0
  116. package/dist/types/inventory.d.ts +44 -1
  117. package/package.json +3 -1
@@ -0,0 +1,124 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { getDoApi } from "./api.js";
3
+ import { Output } from "../../core/output.js";
4
+ export class AppPlatformBuilder extends BaseBuilder {
5
+ out = {
6
+ id: new Output(),
7
+ liveUrl: new Output(),
8
+ };
9
+ _spec = {};
10
+ constructor(appName) {
11
+ super(appName);
12
+ this._spec.name = appName;
13
+ this.discoveryPromise = this.discoverApp(appName);
14
+ }
15
+ spec(jsonSpec) {
16
+ this._spec = { ...this._spec, ...jsonSpec, name: this.name };
17
+ return this;
18
+ }
19
+ async discoverApp(name) {
20
+ try {
21
+ const api = getDoApi();
22
+ const res = await api.get("/apps");
23
+ const match = (res.apps ?? []).find((a) => a.spec?.name === name);
24
+ if (match) {
25
+ this.out.id.resolve(match.id);
26
+ this.out.liveUrl.resolve(match.live_url);
27
+ }
28
+ return match ?? null;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ async deploy() {
35
+ const dryRun = this.isDryRunActive();
36
+ const existing = await this.discoveryPromise;
37
+ const api = getDoApi();
38
+ console.log(`\nšŸš€ Finalizing DigitalOcean App Platform "${this.name}"...`);
39
+ if (existing) {
40
+ this.out.id.resolve(existing.id);
41
+ this.out.liveUrl.resolve(existing.live_url);
42
+ const hasSpecChange = JSON.stringify(existing.spec) !== JSON.stringify(this._spec);
43
+ if (hasSpecChange) {
44
+ if (dryRun) {
45
+ console.log(` šŸ“ [PLAN] Update App Platform "${this.name}" with new specification`);
46
+ }
47
+ else {
48
+ console.log(` šŸ”„ Updating App Platform "${this.name}" (id=${existing.id})...`);
49
+ const updateRes = await api.put(`/apps/${existing.id}`, {
50
+ spec: this._spec,
51
+ });
52
+ console.log(` āœ… App Platform "${this.name}" updated successfully.`);
53
+ this.out.id.resolve(updateRes.app.id);
54
+ this.out.liveUrl.resolve(updateRes.app.live_url);
55
+ }
56
+ }
57
+ else {
58
+ console.log(` āœ… App Platform "${this.name}" already exists and configuration is up to date.`);
59
+ }
60
+ return {
61
+ name: this.name,
62
+ id: existing.id,
63
+ liveUrl: existing.live_url,
64
+ };
65
+ }
66
+ if (dryRun) {
67
+ console.log(` šŸ“ [PLAN] Create DigitalOcean App Platform "${this.name}"`);
68
+ console.log(` └─ Region: ${this._spec.region ?? "default"}`);
69
+ if (this._spec.services) {
70
+ console.log(` └─ Services: ${this._spec.services.map((s) => s.name).join(", ")}`);
71
+ }
72
+ if (this._spec.static_sites) {
73
+ console.log(` └─ Static Sites: ${this._spec.static_sites.map((s) => s.name).join(", ")}`);
74
+ }
75
+ this.out.id.resolve("PENDING");
76
+ this.out.liveUrl.resolve(`https://${this.name}.ondigitalocean.app`);
77
+ return { name: this.name, id: "PENDING" };
78
+ }
79
+ console.log(`šŸš€ Creating DigitalOcean App Platform "${this.name}"...`);
80
+ const createRes = await api.post("/apps", {
81
+ spec: this._spec,
82
+ });
83
+ const app = createRes.app;
84
+ console.log(`šŸš€ App Platform created with ID: ${app.id}`);
85
+ let finalApp = app;
86
+ // Wait for the app deployment to complete and be active
87
+ await this.waitFor(`App Platform "${this.name}" to finish deploying`, async () => {
88
+ const check = await api.get(`/apps/${app.id}`);
89
+ if (check.app) {
90
+ if (check.app.live_url) {
91
+ finalApp = check.app;
92
+ this.out.id.resolve(check.app.id);
93
+ this.out.liveUrl.resolve(check.app.live_url);
94
+ return true;
95
+ }
96
+ }
97
+ return false;
98
+ }, { intervalMs: 15_000, timeoutMs: 900_000 });
99
+ console.log(`šŸš€ App Platform deployment complete → ${finalApp.live_url}`);
100
+ return {
101
+ name: this.name,
102
+ id: finalApp.id,
103
+ liveUrl: finalApp.live_url,
104
+ };
105
+ }
106
+ async destroy() {
107
+ const dryRun = this.isDryRunActive();
108
+ const existing = await this.discoveryPromise;
109
+ const api = getDoApi();
110
+ console.log(`\nšŸ—‘ļø Destroying DigitalOcean App Platform "${this.name}"...`);
111
+ if (!existing) {
112
+ console.log(` ─ App Platform "${this.name}" not found`);
113
+ return { destroyed: false };
114
+ }
115
+ if (dryRun) {
116
+ console.log(` šŸ“ [PLAN] Delete App Platform "${this.name}" (id=${existing.id})`);
117
+ return { destroyed: this.name };
118
+ }
119
+ console.log(` šŸ”„ Deleting App Platform "${this.name}" (id=${existing.id})...`);
120
+ await api.delete(`/apps/${existing.id}`);
121
+ console.log(` šŸ—‘ļø Removed App Platform "${this.name}"`);
122
+ return { destroyed: this.name };
123
+ }
124
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,268 @@
1
+ import { test, describe, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { AppPlatformBuilder } from "./app.js";
4
+ import { Config } from "../../core/config.js";
5
+ describe("AppPlatformBuilder Unit Tests", () => {
6
+ let originalFetch;
7
+ let fetchCalls = [];
8
+ let mockResponses = {};
9
+ beforeEach(() => {
10
+ Config.set({
11
+ dryRun: false,
12
+ providers: {
13
+ do: { token: "fake-do-token", defaultRegion: "nyc3" },
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
+ const body = init?.body ? JSON.parse(init.body) : undefined;
23
+ const headers = init?.headers;
24
+ fetchCalls.push({ url, method, body, headers });
25
+ const matchKey = Object.keys(mockResponses)
26
+ .filter((key) => {
27
+ const [mMethod, mPath] = key.split(" ");
28
+ return method === mMethod && url.includes(mPath);
29
+ })
30
+ .sort((a, b) => b.split(" ")[1].length - a.split(" ")[1].length)[0];
31
+ if (matchKey) {
32
+ const resp = mockResponses[matchKey];
33
+ return {
34
+ ok: resp.status >= 200 && resp.status < 300,
35
+ status: resp.status,
36
+ json: async () => resp.body,
37
+ text: async () => JSON.stringify(resp.body),
38
+ };
39
+ }
40
+ return {
41
+ ok: false,
42
+ status: 404,
43
+ json: async () => ({ message: "Not found" }),
44
+ text: async () => "Not found",
45
+ };
46
+ };
47
+ });
48
+ afterEach(() => {
49
+ globalThis.fetch = originalFetch;
50
+ });
51
+ test("gracefully handles discovery when App does not exist", async () => {
52
+ mockResponses["GET /apps"] = {
53
+ status: 200,
54
+ body: { apps: [] },
55
+ };
56
+ const builder = new AppPlatformBuilder("my-app");
57
+ const discoveryResult = await builder.discoveryPromise;
58
+ assert.strictEqual(discoveryResult, null);
59
+ assert.strictEqual(fetchCalls.length, 1);
60
+ assert.strictEqual(fetchCalls[0].method, "GET");
61
+ assert.ok(fetchCalls[0].url.includes("/apps"));
62
+ });
63
+ test("discovers App successfully when it exists", async () => {
64
+ mockResponses["GET /apps"] = {
65
+ status: 200,
66
+ body: {
67
+ apps: [
68
+ {
69
+ id: "app-123",
70
+ spec: { name: "my-app" },
71
+ live_url: "https://my-app.ondigitalocean.app",
72
+ },
73
+ ],
74
+ },
75
+ };
76
+ const builder = new AppPlatformBuilder("my-app");
77
+ const discoveryResult = await builder.discoveryPromise;
78
+ assert.ok(discoveryResult);
79
+ assert.strictEqual(discoveryResult.id, "app-123");
80
+ const id = await builder.out.id.get();
81
+ const liveUrl = await builder.out.liveUrl.get();
82
+ assert.strictEqual(id, "app-123");
83
+ assert.strictEqual(liveUrl, "https://my-app.ondigitalocean.app");
84
+ });
85
+ test("performs clean dry-run planning without making write requests", async () => {
86
+ Config.set({
87
+ dryRun: true,
88
+ providers: { do: { token: "fake-token" } },
89
+ });
90
+ mockResponses["GET /apps"] = {
91
+ status: 200,
92
+ body: { apps: [] },
93
+ };
94
+ const builder = new AppPlatformBuilder("my-dry-app")
95
+ .spec({
96
+ region: "nyc",
97
+ services: [
98
+ {
99
+ name: "web",
100
+ instance_size_slug: "apps-s-1vcpu-1gb",
101
+ },
102
+ ],
103
+ });
104
+ const result = await builder.deploy();
105
+ assert.ok(result);
106
+ assert.strictEqual(result.name, "my-dry-app");
107
+ assert.strictEqual(result.id, "PENDING");
108
+ // Discover should run, but no creations/updates
109
+ const writeCalls = fetchCalls.filter((c) => c.method !== "GET");
110
+ assert.strictEqual(writeCalls.length, 0);
111
+ const liveUrl = await builder.out.liveUrl.get();
112
+ assert.strictEqual(liveUrl, "https://my-dry-app.ondigitalocean.app");
113
+ });
114
+ test("deploys new App and awaits status: active with live url", async () => {
115
+ mockResponses["GET /apps"] = {
116
+ status: 200,
117
+ body: { apps: [] },
118
+ };
119
+ mockResponses["POST /apps"] = {
120
+ status: 202,
121
+ body: { app: { id: "new-app-id", live_url: "" } },
122
+ };
123
+ let pollCount = 0;
124
+ mockResponses["GET /apps/new-app-id"] = {
125
+ status: 200,
126
+ get body() {
127
+ pollCount++;
128
+ if (pollCount === 1) {
129
+ return { app: { id: "new-app-id", live_url: "" } };
130
+ }
131
+ return {
132
+ app: {
133
+ id: "new-app-id",
134
+ live_url: "https://new-app.ondigitalocean.app",
135
+ },
136
+ };
137
+ },
138
+ };
139
+ const builder = new AppPlatformBuilder("my-new-app")
140
+ .spec({
141
+ region: "nyc",
142
+ services: [
143
+ {
144
+ name: "api",
145
+ github: { repo: "user/repo", branch: "main" },
146
+ },
147
+ ],
148
+ });
149
+ // Instantly check status
150
+ builder.waitFor = async (label, condition) => {
151
+ let done = false;
152
+ while (!done) {
153
+ done = await condition();
154
+ }
155
+ };
156
+ const result = await builder.deploy();
157
+ assert.ok(result);
158
+ assert.strictEqual(result.id, "new-app-id");
159
+ assert.strictEqual(result.liveUrl, "https://new-app.ondigitalocean.app");
160
+ const liveUrl = await builder.out.liveUrl.get();
161
+ assert.strictEqual(liveUrl, "https://new-app.ondigitalocean.app");
162
+ const postCall = fetchCalls.find((c) => c.method === "POST" && c.url.includes("/apps"));
163
+ assert.ok(postCall);
164
+ assert.deepStrictEqual(postCall.body, {
165
+ spec: {
166
+ name: "my-new-app",
167
+ region: "nyc",
168
+ services: [
169
+ {
170
+ name: "api",
171
+ github: { repo: "user/repo", branch: "main" },
172
+ },
173
+ ],
174
+ },
175
+ });
176
+ });
177
+ test("updates spec on existing app if configuration differs", async () => {
178
+ mockResponses["GET /apps"] = {
179
+ status: 200,
180
+ body: {
181
+ apps: [
182
+ {
183
+ id: "app-existing-id",
184
+ spec: {
185
+ name: "my-existing-app",
186
+ region: "nyc",
187
+ },
188
+ live_url: "https://my-existing-app.ondigitalocean.app",
189
+ },
190
+ ],
191
+ },
192
+ };
193
+ mockResponses["PUT /apps/app-existing-id"] = {
194
+ status: 200,
195
+ body: {
196
+ app: {
197
+ id: "app-existing-id",
198
+ spec: {
199
+ name: "my-existing-app",
200
+ region: "ams",
201
+ },
202
+ live_url: "https://my-existing-app.ondigitalocean.app",
203
+ },
204
+ },
205
+ };
206
+ const builder = new AppPlatformBuilder("my-existing-app")
207
+ .spec({
208
+ region: "ams", // differs from existing "nyc"
209
+ });
210
+ const result = await builder.deploy();
211
+ assert.ok(result);
212
+ const putCall = fetchCalls.find((c) => c.method === "PUT" && c.url.includes("/apps/app-existing-id"));
213
+ assert.ok(putCall);
214
+ assert.deepStrictEqual(putCall.body, {
215
+ spec: {
216
+ name: "my-existing-app",
217
+ region: "ams",
218
+ },
219
+ });
220
+ });
221
+ test("skips deploy on existing app if spec matches exactly", async () => {
222
+ mockResponses["GET /apps"] = {
223
+ status: 200,
224
+ body: {
225
+ apps: [
226
+ {
227
+ id: "app-existing-id",
228
+ spec: {
229
+ name: "my-existing-app",
230
+ region: "nyc",
231
+ },
232
+ live_url: "https://my-existing-app.ondigitalocean.app",
233
+ },
234
+ ],
235
+ },
236
+ };
237
+ const builder = new AppPlatformBuilder("my-existing-app")
238
+ .spec({
239
+ region: "nyc", // matches exactly
240
+ });
241
+ const result = await builder.deploy();
242
+ assert.ok(result);
243
+ // Verify no PUT writes
244
+ const putCall = fetchCalls.find((c) => c.method === "PUT");
245
+ assert.ok(!putCall);
246
+ });
247
+ test("destroys App successfully", async () => {
248
+ mockResponses["GET /apps"] = {
249
+ status: 200,
250
+ body: {
251
+ apps: [
252
+ { id: "app-123", spec: { name: "my-app-del" } },
253
+ ],
254
+ },
255
+ };
256
+ mockResponses["DELETE /apps/app-123"] = {
257
+ status: 204,
258
+ body: {},
259
+ };
260
+ const builder = new AppPlatformBuilder("my-app-del");
261
+ await builder.discoveryPromise;
262
+ const result = await builder.destroy();
263
+ assert.deepStrictEqual(result, { destroyed: "my-app-del" });
264
+ const deleteCall = fetchCalls.find((c) => c.method === "DELETE");
265
+ assert.ok(deleteCall);
266
+ assert.ok(deleteCall.url.endsWith("/apps/app-123"));
267
+ });
268
+ });
@@ -0,0 +1,44 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { Output } from "../../core/output.js";
3
+ export declare class DatabaseBuilder extends BaseBuilder {
4
+ readonly out: {
5
+ host: Output<string>;
6
+ port: Output<number>;
7
+ uri: Output<string>;
8
+ user: Output<string>;
9
+ password: Output<string>;
10
+ id: Output<string>;
11
+ };
12
+ private _engine;
13
+ private _version;
14
+ private _size;
15
+ private _region;
16
+ private _nodes;
17
+ private _vpcUuid?;
18
+ private _firewallRules;
19
+ constructor(name: string);
20
+ engine(type: "pg" | "mysql" | "redis" | "mongodb" | "valkey" | "kafka"): this;
21
+ version(v: string): this;
22
+ size(slug: string): this;
23
+ region(r: string): this;
24
+ nodes(num: number): this;
25
+ vpc(uuid: string): this;
26
+ allowIp(cidr: string): this;
27
+ allowDroplet(dropletId: string): this;
28
+ allowTag(tagName: string): this;
29
+ private discoverCluster;
30
+ deploy(): Promise<{
31
+ name: string;
32
+ id: any;
33
+ status: any;
34
+ } | {
35
+ name: string;
36
+ id: string;
37
+ status?: undefined;
38
+ }>;
39
+ destroy(): Promise<{
40
+ destroyed: boolean;
41
+ } | {
42
+ destroyed: string;
43
+ }>;
44
+ }
@@ -0,0 +1,208 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { getDoApi } from "./api.js";
3
+ import { Output } from "../../core/output.js";
4
+ export class DatabaseBuilder extends BaseBuilder {
5
+ out = {
6
+ host: new Output(),
7
+ port: new Output(),
8
+ uri: new Output(),
9
+ user: new Output(),
10
+ password: new Output(),
11
+ id: new Output(),
12
+ };
13
+ _engine = "pg";
14
+ _version = "15";
15
+ _size = "db-s-1vcpu-1gb";
16
+ _region = "nyc3";
17
+ _nodes = 1;
18
+ _vpcUuid;
19
+ _firewallRules = [];
20
+ constructor(name) {
21
+ super(name);
22
+ this.discoveryPromise = this.discoverCluster(name);
23
+ }
24
+ engine(type) {
25
+ this._engine = type;
26
+ return this;
27
+ }
28
+ version(v) {
29
+ this._version = v;
30
+ return this;
31
+ }
32
+ size(slug) {
33
+ this._size = slug;
34
+ return this;
35
+ }
36
+ region(r) {
37
+ this._region = r;
38
+ this.discoveryPromise = this.discoverCluster(this.name);
39
+ return this;
40
+ }
41
+ nodes(num) {
42
+ this._nodes = num;
43
+ return this;
44
+ }
45
+ vpc(uuid) {
46
+ this._vpcUuid = uuid;
47
+ return this;
48
+ }
49
+ allowIp(cidr) {
50
+ this._firewallRules.push({ type: "ip_addr", value: cidr });
51
+ return this;
52
+ }
53
+ allowDroplet(dropletId) {
54
+ this._firewallRules.push({ type: "droplet", value: dropletId });
55
+ return this;
56
+ }
57
+ allowTag(tagName) {
58
+ this._firewallRules.push({ type: "tag", value: tagName });
59
+ return this;
60
+ }
61
+ async discoverCluster(name) {
62
+ try {
63
+ const api = getDoApi();
64
+ const res = await api.get("/databases");
65
+ const match = (res.databases ?? []).find((db) => db.name === name);
66
+ if (match) {
67
+ // Resolve connection outputs immediately
68
+ const conn = match.private_connection ?? match.connection;
69
+ if (conn) {
70
+ this.out.host.resolve(conn.host);
71
+ this.out.port.resolve(conn.port);
72
+ this.out.uri.resolve(conn.uri);
73
+ this.out.user.resolve(conn.user);
74
+ this.out.password.resolve(conn.password);
75
+ }
76
+ this.out.id.resolve(match.id);
77
+ }
78
+ return match ?? null;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ async deploy() {
85
+ const dryRun = this.isDryRunActive();
86
+ const existing = await this.discoveryPromise;
87
+ const api = getDoApi();
88
+ console.log(`\nšŸ—„ļø Finalizing DigitalOcean Database Cluster "${this.name}"...`);
89
+ if (existing) {
90
+ console.log(` āœ… Database Cluster "${this.name}" already exists (id=${existing.id}, status=${existing.status}).`);
91
+ const conn = existing.private_connection ?? existing.connection;
92
+ if (conn) {
93
+ this.out.host.resolve(conn.host);
94
+ this.out.port.resolve(conn.port);
95
+ this.out.uri.resolve(conn.uri);
96
+ this.out.user.resolve(conn.user);
97
+ this.out.password.resolve(conn.password);
98
+ }
99
+ this.out.id.resolve(existing.id);
100
+ // Handle firewall rules updates
101
+ if (this._firewallRules.length > 0) {
102
+ if (dryRun) {
103
+ console.log(` šŸ“ [PLAN] Update Database Firewall Rules (replace list):`);
104
+ for (const rule of this._firewallRules) {
105
+ console.log(` └─ Rule: ${rule.type}:${rule.value}`);
106
+ }
107
+ }
108
+ else {
109
+ await api.put(`/databases/${existing.id}/firewall`, {
110
+ rules: this._firewallRules,
111
+ });
112
+ console.log(` āœ… Database Firewall Rules updated successfully.`);
113
+ }
114
+ }
115
+ return {
116
+ name: this.name,
117
+ id: existing.id,
118
+ status: existing.status,
119
+ };
120
+ }
121
+ if (dryRun) {
122
+ console.log(` šŸ“ [PLAN] Create DigitalOcean Database Cluster "${this.name}" (${this._region})`);
123
+ console.log(` └─ Engine: ${this._engine} (version: ${this._version})`);
124
+ console.log(` └─ Size: ${this._size} | Nodes: ${this._nodes}`);
125
+ if (this._vpcUuid) {
126
+ console.log(` └─ VPC Network: ${this._vpcUuid}`);
127
+ }
128
+ if (this._firewallRules.length > 0) {
129
+ console.log(` └─ Firewall Rules to apply:`);
130
+ for (const rule of this._firewallRules) {
131
+ console.log(` └─ ${rule.type}:${rule.value}`);
132
+ }
133
+ }
134
+ this.out.host.resolve("127.0.0.1");
135
+ this.out.port.resolve(5432);
136
+ this.out.uri.resolve("postgresql://db:pass@127.0.0.1:5432/db");
137
+ this.out.user.resolve("db");
138
+ this.out.password.resolve("pass");
139
+ this.out.id.resolve("PENDING");
140
+ return { name: this.name, id: "PENDING" };
141
+ }
142
+ // Create the database cluster
143
+ console.log(`šŸš€ Creating DigitalOcean Database Cluster "${this.name}" (takes several minutes)...`);
144
+ const body = {
145
+ name: this.name,
146
+ engine: this._engine,
147
+ version: this._version,
148
+ region: this._region,
149
+ size: this._size,
150
+ num_nodes: this._nodes,
151
+ };
152
+ if (this._vpcUuid) {
153
+ body.private_network_uuid = this._vpcUuid;
154
+ }
155
+ const createRes = await api.post("/databases", body);
156
+ const dbCluster = createRes.database;
157
+ console.log(`šŸš€ Database Cluster created with ID: ${dbCluster.id}`);
158
+ // Wait for the database cluster to become active
159
+ await this.waitFor(`Database Cluster "${this.name}" to become active`, async () => {
160
+ const check = await api.get(`/databases/${dbCluster.id}`);
161
+ if (check.database && check.database.status === "online") {
162
+ const conn = check.database.private_connection ?? check.database.connection;
163
+ if (conn) {
164
+ this.out.host.resolve(conn.host);
165
+ this.out.port.resolve(conn.port);
166
+ this.out.uri.resolve(conn.uri);
167
+ this.out.user.resolve(conn.user);
168
+ this.out.password.resolve(conn.password);
169
+ }
170
+ this.out.id.resolve(check.database.id);
171
+ return true;
172
+ }
173
+ return false;
174
+ }, { intervalMs: 15_000, timeoutMs: 900_000 });
175
+ // Apply database firewall rules (trusted sources) if specified
176
+ if (this._firewallRules.length > 0) {
177
+ console.log(` šŸ” Applying Database Firewall Rules...`);
178
+ await api.put(`/databases/${dbCluster.id}/firewall`, {
179
+ rules: this._firewallRules,
180
+ });
181
+ console.log(` āœ… Database Firewall Rules applied.`);
182
+ }
183
+ console.log(`šŸš€ Database available.`);
184
+ return {
185
+ name: this.name,
186
+ id: dbCluster.id,
187
+ status: "online",
188
+ };
189
+ }
190
+ async destroy() {
191
+ const dryRun = this.isDryRunActive();
192
+ const existing = await this.discoveryPromise;
193
+ const api = getDoApi();
194
+ console.log(`\nšŸ—‘ļø Destroying DigitalOcean Database Cluster "${this.name}"...`);
195
+ if (!existing) {
196
+ console.log(` ─ Database Cluster "${this.name}" not found`);
197
+ return { destroyed: false };
198
+ }
199
+ if (dryRun) {
200
+ console.log(` šŸ“ [PLAN] Delete Database Cluster "${this.name}" (id=${existing.id})`);
201
+ return { destroyed: this.name };
202
+ }
203
+ console.log(` šŸ”„ Deleting Database Cluster "${this.name}" (id=${existing.id})...`);
204
+ await api.delete(`/databases/${existing.id}`);
205
+ console.log(` šŸ—‘ļø Removed Database Cluster "${this.name}"`);
206
+ return { destroyed: this.name };
207
+ }
208
+ }
@@ -0,0 +1 @@
1
+ export {};