puls-dev 0.2.1 → 0.2.2

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 (36) hide show
  1. package/dist/core/config.d.ts +5 -0
  2. package/dist/providers/firebase/appcheck.d.ts +15 -0
  3. package/dist/providers/firebase/appcheck.js +109 -0
  4. package/dist/providers/firebase/appcheck.test.d.ts +1 -0
  5. package/dist/providers/firebase/appcheck.test.js +141 -0
  6. package/dist/providers/firebase/index.d.ts +2 -0
  7. package/dist/providers/firebase/index.js +2 -0
  8. package/dist/providers/gcp/api.d.ts +10 -0
  9. package/dist/providers/gcp/api.js +111 -0
  10. package/dist/providers/gcp/clouddns.d.ts +37 -0
  11. package/dist/providers/gcp/clouddns.js +284 -0
  12. package/dist/providers/gcp/clouddns.test.d.ts +1 -0
  13. package/dist/providers/gcp/clouddns.test.js +259 -0
  14. package/dist/providers/gcp/cloudrun.d.ts +31 -0
  15. package/dist/providers/gcp/cloudrun.js +240 -0
  16. package/dist/providers/gcp/cloudrun.test.d.ts +1 -0
  17. package/dist/providers/gcp/cloudrun.test.js +281 -0
  18. package/dist/providers/gcp/cloudsql.d.ts +37 -0
  19. package/dist/providers/gcp/cloudsql.js +262 -0
  20. package/dist/providers/gcp/cloudsql.test.d.ts +1 -0
  21. package/dist/providers/gcp/cloudsql.test.js +295 -0
  22. package/dist/providers/gcp/iam.d.ts +38 -0
  23. package/dist/providers/gcp/iam.js +309 -0
  24. package/dist/providers/gcp/iam.test.d.ts +1 -0
  25. package/dist/providers/gcp/iam.test.js +305 -0
  26. package/dist/providers/gcp/index.d.ts +19 -0
  27. package/dist/providers/gcp/index.js +19 -0
  28. package/dist/providers/gcp/pubsub.d.ts +31 -0
  29. package/dist/providers/gcp/pubsub.js +227 -0
  30. package/dist/providers/gcp/pubsub.test.d.ts +1 -0
  31. package/dist/providers/gcp/pubsub.test.js +244 -0
  32. package/dist/providers/gcp/secrets.d.ts +21 -0
  33. package/dist/providers/gcp/secrets.js +187 -0
  34. package/dist/providers/gcp/secrets.test.d.ts +1 -0
  35. package/dist/providers/gcp/secrets.test.js +264 -0
  36. package/package.json +5 -1
@@ -0,0 +1,262 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { gcpFetch, getProjectId, getRegion } from "./api.js";
3
+ const SQL_BASE = "https://sqladmin.googleapis.com";
4
+ const DB_PORT = {
5
+ postgres: 5432,
6
+ postgresql: 5432,
7
+ mysql: 3306,
8
+ };
9
+ function formatDatabaseVersion(engine, version) {
10
+ const eng = engine.toLowerCase();
11
+ if (eng === "postgres" || eng === "postgresql") {
12
+ return `POSTGRES_${version.split(".")[0]}`;
13
+ }
14
+ if (eng === "mysql") {
15
+ return `MYSQL_${version.replace(/\./g, "_")}`;
16
+ }
17
+ return `${engine.toUpperCase()}_${version}`;
18
+ }
19
+ export class GCPCloudSQLBuilder extends BaseBuilder {
20
+ _engine = "postgres";
21
+ _engineVersion = "16";
22
+ _tier = "db-f1-micro";
23
+ _storage = 10;
24
+ _username;
25
+ _password;
26
+ _dbName;
27
+ _publicAccess = false;
28
+ _region;
29
+ resolvedEndpoint = null;
30
+ resolvedPort = null;
31
+ resolvedConnectionName = null;
32
+ constructor(instanceId) {
33
+ super(instanceId);
34
+ this.discoveryPromise = this.discoverInstance();
35
+ }
36
+ engine(e) {
37
+ this._engine = e.engine;
38
+ this._engineVersion = e.version;
39
+ return this;
40
+ }
41
+ size(tier) {
42
+ this._tier = tier;
43
+ return this;
44
+ }
45
+ storage(gb) {
46
+ this._storage = gb;
47
+ return this;
48
+ }
49
+ credentials(username, password) {
50
+ this._username = username;
51
+ this._password = password;
52
+ return this;
53
+ }
54
+ database(name) {
55
+ this._dbName = name;
56
+ return this;
57
+ }
58
+ publicAccess(enabled = true) {
59
+ this._publicAccess = enabled;
60
+ return this;
61
+ }
62
+ region(reg) {
63
+ this._region = reg;
64
+ this.discoveryPromise = this.discoverInstance();
65
+ return this;
66
+ }
67
+ async discoverInstance() {
68
+ try {
69
+ const project = getProjectId();
70
+ const instanceId = this.name;
71
+ const res = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}`);
72
+ if (res.state === "DELETED")
73
+ return null;
74
+ const primaryIp = res.ipAddresses?.find((ip) => ip.type === "PRIMARY")?.ipAddress;
75
+ this.resolvedEndpoint = primaryIp ?? null;
76
+ this.resolvedPort = DB_PORT[this._engine] ?? 5432;
77
+ this.resolvedConnectionName = res.connectionName ?? null;
78
+ return res;
79
+ }
80
+ catch (e) {
81
+ if (e.message?.includes("404") ||
82
+ e.message?.includes("403") ||
83
+ e.message?.includes("credentials not configured")) {
84
+ return null;
85
+ }
86
+ throw e;
87
+ }
88
+ }
89
+ async waitForOperation(opName, label) {
90
+ const project = getProjectId();
91
+ await this.waitFor(label, async () => {
92
+ const op = await gcpFetch(SQL_BASE, `/v1/projects/${project}/operations/${opName}`);
93
+ if (op.status === "DONE") {
94
+ if (op.error) {
95
+ throw new Error(`Cloud SQL Operation failed: ${JSON.stringify(op.error)}`);
96
+ }
97
+ return true;
98
+ }
99
+ return false;
100
+ }, { intervalMs: 10_000, timeoutMs: 900_000 });
101
+ }
102
+ async deploy() {
103
+ const dryRun = this.isDryRunActive();
104
+ const project = getProjectId();
105
+ const location = this._region ?? getRegion();
106
+ const instanceId = this.name;
107
+ const port = DB_PORT[this._engine] ?? 5432;
108
+ console.log(`\n⚡ Finalizing GCP Cloud SQL Instance "${instanceId}"...`);
109
+ if (!this._username || !this._password) {
110
+ throw new Error(`[GCP.CloudSQL:${instanceId}] .credentials(username, password) is required`);
111
+ }
112
+ const existing = await this.discoveryPromise;
113
+ const targetDbVersion = formatDatabaseVersion(this._engine, this._engineVersion);
114
+ const targetAuthorizedNetworks = this._publicAccess
115
+ ? [{ value: "0.0.0.0/0", name: "internet" }]
116
+ : [];
117
+ if (dryRun) {
118
+ console.log(` 📝 [PLAN] ${existing ? "Update" : "Create"} Cloud SQL instance "${instanceId}" in ${location}`);
119
+ console.log(` └─ Engine: ${this._engine} (${targetDbVersion})`);
120
+ console.log(` └─ Tier: ${this._tier} | Disk: ${this._storage}GB`);
121
+ console.log(` └─ Public Access: ${this._publicAccess ? "enabled" : "disabled"}`);
122
+ if (this._dbName) {
123
+ console.log(` └─ Database to create: "${this._dbName}"`);
124
+ }
125
+ if (this._username && this._username !== "postgres" && this._username !== "root") {
126
+ console.log(` └─ Custom user to create: "${this._username}"`);
127
+ }
128
+ this.resolvedEndpoint = "127.0.0.1";
129
+ this.resolvedPort = port;
130
+ this.resolvedConnectionName = `${project}:${location}:${instanceId}`;
131
+ return {
132
+ name: instanceId,
133
+ endpoint: this.resolvedEndpoint,
134
+ port: this.resolvedPort,
135
+ connectionName: this.resolvedConnectionName,
136
+ };
137
+ }
138
+ let needsUpdate = !existing;
139
+ if (existing) {
140
+ const existingSettings = existing.settings ?? {};
141
+ const existingNetworks = existingSettings.ipConfiguration?.authorizedNetworks ?? [];
142
+ const hasTierChange = existingSettings.tier !== this._tier;
143
+ const hasStorageChange = Number(existingSettings.dataDiskSizeGb ?? 0) < this._storage;
144
+ const hasNetworkChange = JSON.stringify(existingNetworks) !== JSON.stringify(targetAuthorizedNetworks);
145
+ needsUpdate = hasTierChange || hasStorageChange || hasNetworkChange;
146
+ }
147
+ const settings = {
148
+ tier: this._tier,
149
+ dataDiskSizeGb: String(this._storage),
150
+ ipConfiguration: {
151
+ ipv4Enabled: true,
152
+ authorizedNetworks: targetAuthorizedNetworks,
153
+ },
154
+ };
155
+ let currentInstance;
156
+ if (!existing) {
157
+ console.log(`🚀 Creating GCP Cloud SQL instance "${instanceId}" (takes several minutes)...`);
158
+ const op = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances`, {
159
+ method: "POST",
160
+ body: JSON.stringify({
161
+ name: instanceId,
162
+ databaseVersion: targetDbVersion,
163
+ region: location,
164
+ rootPassword: this._password,
165
+ settings,
166
+ }),
167
+ });
168
+ await this.waitForOperation(op.name, `Instance creation "${instanceId}"`);
169
+ currentInstance = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}`);
170
+ }
171
+ else if (needsUpdate) {
172
+ console.log(`🔄 Updating GCP Cloud SQL instance "${instanceId}"...`);
173
+ const op = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}`, {
174
+ method: "PATCH",
175
+ body: JSON.stringify({ settings }),
176
+ });
177
+ await this.waitForOperation(op.name, `Instance update "${instanceId}"`);
178
+ currentInstance = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}`);
179
+ }
180
+ else {
181
+ console.log(`✅ GCP Cloud SQL instance "${instanceId}" is up to date.`);
182
+ currentInstance = existing;
183
+ }
184
+ const primaryIp = currentInstance.ipAddresses?.find((ip) => ip.type === "PRIMARY")?.ipAddress;
185
+ this.resolvedEndpoint = primaryIp ?? null;
186
+ this.resolvedPort = port;
187
+ this.resolvedConnectionName = currentInstance.connectionName ?? null;
188
+ if (this._dbName) {
189
+ console.log(` 🗄️ Ensuring database "${this._dbName}" exists...`);
190
+ try {
191
+ await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}/databases`, {
192
+ method: "POST",
193
+ body: JSON.stringify({
194
+ name: this._dbName,
195
+ instance: instanceId,
196
+ project,
197
+ }),
198
+ });
199
+ console.log(` ✅ Database "${this._dbName}" created.`);
200
+ }
201
+ catch (e) {
202
+ if (!e.message?.includes("409")) {
203
+ throw e;
204
+ }
205
+ console.log(` ✅ Database "${this._dbName}" already exists.`);
206
+ }
207
+ }
208
+ const defaultAdmin = this._engine === "postgres" ? "postgres" : "root";
209
+ if (this._username && this._username !== defaultAdmin) {
210
+ console.log(` 👤 Ensuring custom user "${this._username}" exists...`);
211
+ try {
212
+ await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}/users`, {
213
+ method: "POST",
214
+ body: JSON.stringify({
215
+ name: this._username,
216
+ password: this._password,
217
+ instance: instanceId,
218
+ project,
219
+ }),
220
+ });
221
+ console.log(` ✅ Custom user "${this._username}" created.`);
222
+ }
223
+ catch (e) {
224
+ if (!e.message?.includes("409")) {
225
+ throw e;
226
+ }
227
+ console.log(` ✅ Custom user "${this._username}" already exists.`);
228
+ }
229
+ }
230
+ await this.deploySidecars();
231
+ console.log(`🚀 Database available → ${this.resolvedEndpoint}:${this.resolvedPort}`);
232
+ return {
233
+ name: instanceId,
234
+ endpoint: this.resolvedEndpoint,
235
+ port: this.resolvedPort,
236
+ connectionName: this.resolvedConnectionName,
237
+ };
238
+ }
239
+ async destroy() {
240
+ const dryRun = this.isDryRunActive();
241
+ const project = getProjectId();
242
+ const instanceId = this.name;
243
+ console.log(`\n🗑️ Destroying GCP Cloud SQL Instance "${instanceId}"...`);
244
+ const existing = await this.discoverInstance();
245
+ if (!existing) {
246
+ console.log(` ✅ Instance "${instanceId}" does not exist - nothing to do.`);
247
+ return { destroyed: instanceId };
248
+ }
249
+ if (dryRun) {
250
+ console.log(` 📝 [PLAN] Delete Cloud SQL instance "${instanceId}"`);
251
+ return { destroyed: instanceId };
252
+ }
253
+ console.log(` 🔄 Deleting Cloud SQL instance "${instanceId}"...`);
254
+ const op = await gcpFetch(SQL_BASE, `/v1/projects/${project}/instances/${instanceId}`, {
255
+ method: "DELETE",
256
+ });
257
+ await this.waitForOperation(op.name, `Instance deletion "${instanceId}"`);
258
+ console.log(` ✅ Instance "${instanceId}" deleted.`);
259
+ await this.destroySidecars();
260
+ return { destroyed: instanceId };
261
+ }
262
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,295 @@
1
+ import { test, describe, beforeEach, afterEach, mock } from "node:test";
2
+ import assert from "node:assert";
3
+ import { GoogleAuth } from "google-auth-library";
4
+ import { GCPCloudSQLBuilder } from "./cloudsql.js";
5
+ import { Config } from "../../core/config.js";
6
+ describe("GCPCloudSQLBuilder Unit Tests", () => {
7
+ let originalFetch;
8
+ let fetchCalls = [];
9
+ let mockResponses = {};
10
+ beforeEach(() => {
11
+ Config.set({
12
+ dryRun: false,
13
+ providers: {
14
+ gcp: {
15
+ projectId: "my-gcp-project",
16
+ serviceAccountPath: "/fake/sa.json",
17
+ region: "us-central1",
18
+ },
19
+ },
20
+ });
21
+ originalFetch = globalThis.fetch;
22
+ fetchCalls = [];
23
+ mockResponses = {};
24
+ globalThis.fetch = async (input, init) => {
25
+ const url = String(input);
26
+ const method = init?.method ?? "GET";
27
+ let body;
28
+ if (init?.body) {
29
+ if (typeof init.body === "string") {
30
+ try {
31
+ body = JSON.parse(init.body);
32
+ }
33
+ catch {
34
+ body = init.body;
35
+ }
36
+ }
37
+ else {
38
+ body = "[Binary/Buffer Body]";
39
+ }
40
+ }
41
+ const headers = init?.headers;
42
+ fetchCalls.push({ url, method, body, headers });
43
+ const matchKey = Object.keys(mockResponses)
44
+ .filter((key) => {
45
+ const [mMethod, mPath] = key.split(" ");
46
+ return method === mMethod && url.includes(mPath);
47
+ })
48
+ .sort((a, b) => b.split(" ")[1].length - a.split(" ")[1].length)[0];
49
+ if (matchKey) {
50
+ const resp = mockResponses[matchKey];
51
+ return {
52
+ ok: resp.status >= 200 && resp.status < 300,
53
+ status: resp.status,
54
+ json: async () => resp.body,
55
+ text: async () => JSON.stringify(resp.body),
56
+ };
57
+ }
58
+ return {
59
+ ok: false,
60
+ status: 404,
61
+ json: async () => ({ message: `Endpoint not mocked: ${method} ${url}` }),
62
+ text: async () => `Endpoint not mocked: ${method} ${url}`,
63
+ };
64
+ };
65
+ mock.method(GoogleAuth.prototype, "getClient", async () => {
66
+ return {
67
+ getAccessToken: async () => ({ token: "fake-gcp-token" }),
68
+ };
69
+ });
70
+ });
71
+ afterEach(() => {
72
+ globalThis.fetch = originalFetch;
73
+ mock.restoreAll();
74
+ });
75
+ test("fluent builder api sets properties correctly", () => {
76
+ const builder = new GCPCloudSQLBuilder("my-db")
77
+ .engine({ engine: "postgres", version: "15" })
78
+ .size("db-custom-2-7680")
79
+ .storage(50)
80
+ .credentials("db_user", "db_pass")
81
+ .database("my_app_db")
82
+ .publicAccess(true)
83
+ .region("us-east4");
84
+ assert.strictEqual(builder._engine, "postgres");
85
+ assert.strictEqual(builder._engineVersion, "15");
86
+ assert.strictEqual(builder._tier, "db-custom-2-7680");
87
+ assert.strictEqual(builder._storage, 50);
88
+ assert.strictEqual(builder._username, "db_user");
89
+ assert.strictEqual(builder._password, "db_pass");
90
+ assert.strictEqual(builder._dbName, "my_app_db");
91
+ assert.strictEqual(builder._publicAccess, true);
92
+ assert.strictEqual(builder._region, "us-east4");
93
+ });
94
+ test("runs in dry-run mode safely and logs plans", async () => {
95
+ Config.set({
96
+ dryRun: true,
97
+ providers: {
98
+ gcp: {
99
+ projectId: "my-gcp-project",
100
+ serviceAccountPath: "/fake/sa.json",
101
+ region: "us-central1",
102
+ },
103
+ },
104
+ });
105
+ const builder = new GCPCloudSQLBuilder("dry-run-db")
106
+ .credentials("root", "secretpwd")
107
+ .engine({ engine: "mysql", version: "8.0" })
108
+ .size("db-f1-micro")
109
+ .storage(15);
110
+ const result = await builder.deploy();
111
+ assert.strictEqual(result.name, "dry-run-db");
112
+ assert.strictEqual(result.endpoint, "127.0.0.1");
113
+ assert.strictEqual(result.port, 3306);
114
+ assert.strictEqual(result.connectionName, "my-gcp-project:us-central1:dry-run-db");
115
+ // No write calls should be sent in dry-run mode
116
+ const writeCalls = fetchCalls.filter((c) => c.method !== "GET");
117
+ assert.strictEqual(writeCalls.length, 0);
118
+ });
119
+ test("creates a new instance, database, and custom user when missing", async () => {
120
+ // 1. Stateful mock for GET /instances/new-db: returns 404 on discovery, 200 after creation
121
+ mockResponses["GET /instances/new-db"] = {
122
+ get status() {
123
+ const getCalls = fetchCalls.filter((c) => c.method === "GET" && c.url.includes("/instances/new-db"));
124
+ return getCalls.length <= 1 ? 404 : 200;
125
+ },
126
+ get body() {
127
+ const getCalls = fetchCalls.filter((c) => c.method === "GET" && c.url.includes("/instances/new-db"));
128
+ if (getCalls.length <= 1)
129
+ return { message: "Not found" };
130
+ return {
131
+ name: "new-db",
132
+ connectionName: "my-gcp-project:us-central1:new-db",
133
+ ipAddresses: [{ type: "PRIMARY", ipAddress: "35.200.10.20" }],
134
+ settings: {
135
+ tier: "db-f1-micro",
136
+ dataDiskSizeGb: "20",
137
+ },
138
+ };
139
+ },
140
+ };
141
+ // 2. Mock POST (create instance)
142
+ mockResponses["POST /instances"] = {
143
+ status: 200,
144
+ body: { name: "op-create-123", status: "PENDING" },
145
+ };
146
+ // 3. Mock GET (poll operation status until DONE)
147
+ mockResponses["GET /operations/op-create-123"] = {
148
+ status: 200,
149
+ body: { status: "DONE" },
150
+ };
151
+ // 5. Mock POST (create database)
152
+ mockResponses["POST /instances/new-db/databases"] = {
153
+ status: 201,
154
+ body: {},
155
+ };
156
+ // 6. Mock POST (create custom user)
157
+ mockResponses["POST /instances/new-db/users"] = {
158
+ status: 201,
159
+ body: {},
160
+ };
161
+ const builder = new GCPCloudSQLBuilder("new-db")
162
+ .engine({ engine: "postgres", version: "16" })
163
+ .size("db-f1-micro")
164
+ .storage(20)
165
+ .credentials("custom_admin", "securepwd")
166
+ .database("custom_db")
167
+ .publicAccess(false);
168
+ const result = await builder.deploy();
169
+ assert.strictEqual(result.name, "new-db");
170
+ assert.strictEqual(result.endpoint, "35.200.10.20");
171
+ assert.strictEqual(result.port, 5432);
172
+ assert.strictEqual(result.connectionName, "my-gcp-project:us-central1:new-db");
173
+ // Verify correct calls were made
174
+ const postInstances = fetchCalls.find((c) => c.method === "POST" && c.url.endsWith("/instances"));
175
+ assert.ok(postInstances);
176
+ assert.strictEqual(postInstances.body.name, "new-db");
177
+ assert.strictEqual(postInstances.body.databaseVersion, "POSTGRES_16");
178
+ assert.strictEqual(postInstances.body.rootPassword, "securepwd");
179
+ assert.strictEqual(postInstances.body.settings.dataDiskSizeGb, "20");
180
+ const postDB = fetchCalls.find((c) => c.method === "POST" && c.url.includes("/databases"));
181
+ assert.ok(postDB);
182
+ assert.strictEqual(postDB.body.name, "custom_db");
183
+ const postUser = fetchCalls.find((c) => c.method === "POST" && c.url.includes("/users"));
184
+ assert.ok(postUser);
185
+ assert.strictEqual(postUser.body.name, "custom_admin");
186
+ assert.strictEqual(postUser.body.password, "securepwd");
187
+ });
188
+ test("patches instance when configuration storage or tier differs", async () => {
189
+ // 1. Mock GET (discovery) returns existing instance with 10GB and different tier
190
+ mockResponses["GET /instances/patch-db"] = {
191
+ status: 200,
192
+ body: {
193
+ name: "patch-db",
194
+ connectionName: "my-gcp-project:us-central1:patch-db",
195
+ ipAddresses: [{ type: "PRIMARY", ipAddress: "35.200.10.20" }],
196
+ settings: {
197
+ tier: "db-f1-micro",
198
+ dataDiskSizeGb: "10",
199
+ },
200
+ },
201
+ };
202
+ // 2. Mock PATCH (update settings)
203
+ mockResponses["PATCH /instances/patch-db"] = {
204
+ status: 200,
205
+ body: { name: "op-patch-123", status: "PENDING" },
206
+ };
207
+ // 3. Mock GET (poll operation status until DONE)
208
+ mockResponses["GET /operations/op-patch-123"] = {
209
+ status: 200,
210
+ body: { status: "DONE" },
211
+ };
212
+ const builder = new GCPCloudSQLBuilder("patch-db")
213
+ .engine({ engine: "postgres", version: "16" })
214
+ .size("db-custom-2-7680") // changed tier!
215
+ .storage(30) // increased storage!
216
+ .credentials("postgres", "securepwd");
217
+ const result = await builder.deploy();
218
+ assert.strictEqual(result.name, "patch-db");
219
+ // Verify PATCH was called
220
+ const patchCalls = fetchCalls.filter((c) => c.method === "PATCH");
221
+ assert.strictEqual(patchCalls.length, 1);
222
+ assert.strictEqual(patchCalls[0].body.settings.tier, "db-custom-2-7680");
223
+ assert.strictEqual(patchCalls[0].body.settings.dataDiskSizeGb, "30");
224
+ });
225
+ test("skips patch if instance configuration is identical", async () => {
226
+ // 1. Mock GET (discovery) returns identical settings
227
+ mockResponses["GET /instances/ident-db"] = {
228
+ status: 200,
229
+ body: {
230
+ name: "ident-db",
231
+ connectionName: "my-gcp-project:us-central1:ident-db",
232
+ ipAddresses: [{ type: "PRIMARY", ipAddress: "35.200.10.20" }],
233
+ settings: {
234
+ tier: "db-f1-micro",
235
+ dataDiskSizeGb: "10",
236
+ ipConfiguration: {
237
+ authorizedNetworks: [],
238
+ },
239
+ },
240
+ },
241
+ };
242
+ const builder = new GCPCloudSQLBuilder("ident-db")
243
+ .engine({ engine: "postgres", version: "16" })
244
+ .size("db-f1-micro")
245
+ .storage(10)
246
+ .credentials("postgres", "securepwd")
247
+ .publicAccess(false);
248
+ const result = await builder.deploy();
249
+ assert.strictEqual(result.name, "ident-db");
250
+ // Assert NO write calls for instances (POST or PATCH) occurred
251
+ const writeCalls = fetchCalls.filter((c) => c.method === "PATCH" ||
252
+ (c.method === "POST" && c.url.endsWith("/instances")));
253
+ assert.strictEqual(writeCalls.length, 0);
254
+ });
255
+ test("destroys an existing instance successfully", async () => {
256
+ // 1. Mock GET (discovery on destroy) returns existing instance
257
+ mockResponses["GET /instances/to-delete-db"] = {
258
+ status: 200,
259
+ body: {
260
+ name: "to-delete-db",
261
+ connectionName: "my-gcp-project:us-central1:to-delete-db",
262
+ },
263
+ };
264
+ // 2. Mock DELETE
265
+ mockResponses["DELETE /instances/to-delete-db"] = {
266
+ status: 200,
267
+ body: { name: "op-delete-123", status: "PENDING" },
268
+ };
269
+ // 3. Mock GET (poll operation status until DONE)
270
+ mockResponses["GET /operations/op-delete-123"] = {
271
+ status: 200,
272
+ body: { status: "DONE" },
273
+ };
274
+ const builder = new GCPCloudSQLBuilder("to-delete-db");
275
+ const result = await builder.destroy();
276
+ assert.deepStrictEqual(result, { destroyed: "to-delete-db" });
277
+ // Verify DELETE was called
278
+ const deleteCalls = fetchCalls.filter((c) => c.method === "DELETE");
279
+ assert.strictEqual(deleteCalls.length, 1);
280
+ assert.strictEqual(deleteCalls[0].url.includes("/instances/to-delete-db"), true);
281
+ });
282
+ test("destroy does nothing when instance does not exist", async () => {
283
+ // Mock GET returned 404
284
+ mockResponses["GET /instances/not-exist-db"] = {
285
+ status: 404,
286
+ body: { message: "Not found" },
287
+ };
288
+ const builder = new GCPCloudSQLBuilder("not-exist-db");
289
+ const result = await builder.destroy();
290
+ assert.deepStrictEqual(result, { destroyed: "not-exist-db" });
291
+ // Verify no DELETE was called
292
+ const deleteCalls = fetchCalls.filter((c) => c.method === "DELETE");
293
+ assert.strictEqual(deleteCalls.length, 0);
294
+ });
295
+ });
@@ -0,0 +1,38 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { Output } from "../../core/output.js";
3
+ export declare class GCPServiceAccountBuilder extends BaseBuilder {
4
+ readonly out: {
5
+ email: Output<string>;
6
+ name: Output<string>;
7
+ };
8
+ private _displayName?;
9
+ private _description?;
10
+ constructor(accountId: string);
11
+ get email(): string;
12
+ displayName(name: string): this;
13
+ description(desc: string): this;
14
+ private discoverServiceAccount;
15
+ deploy(): Promise<{
16
+ email: string;
17
+ name: string;
18
+ }>;
19
+ destroy(): Promise<{
20
+ destroyed: string;
21
+ }>;
22
+ }
23
+ export declare class GCPIAMBindingBuilder extends BaseBuilder {
24
+ private _role;
25
+ private _members;
26
+ constructor(name: string);
27
+ role(name: string): this;
28
+ member(m: string | GCPServiceAccountBuilder | Output<string>): this;
29
+ members(...m: (string | GCPServiceAccountBuilder | Output<string>)[]): this;
30
+ private resolveMembers;
31
+ deploy(): Promise<{
32
+ role: string;
33
+ bound: string[];
34
+ }>;
35
+ destroy(): Promise<{
36
+ destroyed: string;
37
+ }>;
38
+ }