puls-dev 0.2.1 → 0.2.3

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 (37) hide show
  1. package/README.md +13 -5
  2. package/dist/core/config.d.ts +5 -0
  3. package/dist/providers/firebase/appcheck.d.ts +15 -0
  4. package/dist/providers/firebase/appcheck.js +109 -0
  5. package/dist/providers/firebase/appcheck.test.d.ts +1 -0
  6. package/dist/providers/firebase/appcheck.test.js +141 -0
  7. package/dist/providers/firebase/index.d.ts +2 -0
  8. package/dist/providers/firebase/index.js +2 -0
  9. package/dist/providers/gcp/api.d.ts +10 -0
  10. package/dist/providers/gcp/api.js +111 -0
  11. package/dist/providers/gcp/clouddns.d.ts +37 -0
  12. package/dist/providers/gcp/clouddns.js +284 -0
  13. package/dist/providers/gcp/clouddns.test.d.ts +1 -0
  14. package/dist/providers/gcp/clouddns.test.js +259 -0
  15. package/dist/providers/gcp/cloudrun.d.ts +31 -0
  16. package/dist/providers/gcp/cloudrun.js +240 -0
  17. package/dist/providers/gcp/cloudrun.test.d.ts +1 -0
  18. package/dist/providers/gcp/cloudrun.test.js +281 -0
  19. package/dist/providers/gcp/cloudsql.d.ts +37 -0
  20. package/dist/providers/gcp/cloudsql.js +262 -0
  21. package/dist/providers/gcp/cloudsql.test.d.ts +1 -0
  22. package/dist/providers/gcp/cloudsql.test.js +295 -0
  23. package/dist/providers/gcp/iam.d.ts +38 -0
  24. package/dist/providers/gcp/iam.js +309 -0
  25. package/dist/providers/gcp/iam.test.d.ts +1 -0
  26. package/dist/providers/gcp/iam.test.js +305 -0
  27. package/dist/providers/gcp/index.d.ts +19 -0
  28. package/dist/providers/gcp/index.js +19 -0
  29. package/dist/providers/gcp/pubsub.d.ts +31 -0
  30. package/dist/providers/gcp/pubsub.js +227 -0
  31. package/dist/providers/gcp/pubsub.test.d.ts +1 -0
  32. package/dist/providers/gcp/pubsub.test.js +244 -0
  33. package/dist/providers/gcp/secrets.d.ts +21 -0
  34. package/dist/providers/gcp/secrets.js +187 -0
  35. package/dist/providers/gcp/secrets.test.d.ts +1 -0
  36. package/dist/providers/gcp/secrets.test.js +264 -0
  37. package/package.json +5 -1
@@ -0,0 +1,31 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ export declare class GCPPubSubTopicBuilder extends BaseBuilder {
3
+ resolvedTopicName: string | null;
4
+ constructor(topicId: string);
5
+ private discoverTopic;
6
+ deploy(): Promise<{
7
+ name: string;
8
+ topicName: string | null;
9
+ }>;
10
+ destroy(): Promise<{
11
+ destroyed: string;
12
+ }>;
13
+ }
14
+ export declare class GCPPubSubSubscriptionBuilder extends BaseBuilder {
15
+ private _topic?;
16
+ private _pushEndpoint?;
17
+ private _ackDeadlineSeconds;
18
+ resolvedSubscriptionName: string | null;
19
+ constructor(subscriptionId: string);
20
+ topic(t: string | GCPPubSubTopicBuilder): this;
21
+ pushEndpoint(url: string): this;
22
+ ackDeadline(seconds: number): this;
23
+ private discoverSubscription;
24
+ deploy(): Promise<{
25
+ name: string;
26
+ subscriptionName: string | null;
27
+ }>;
28
+ destroy(): Promise<{
29
+ destroyed: string;
30
+ }>;
31
+ }
@@ -0,0 +1,227 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { gcpFetch, getProjectId } from "./api.js";
3
+ const PUBSUB_BASE = "https://pubsub.googleapis.com";
4
+ export class GCPPubSubTopicBuilder extends BaseBuilder {
5
+ resolvedTopicName = null;
6
+ constructor(topicId) {
7
+ super(topicId);
8
+ this.discoveryPromise = this.discoverTopic();
9
+ }
10
+ async discoverTopic() {
11
+ try {
12
+ const project = getProjectId();
13
+ const res = await gcpFetch(PUBSUB_BASE, `/v1/projects/${project}/topics/${this.name}`);
14
+ this.resolvedTopicName = res.name ?? null;
15
+ return res;
16
+ }
17
+ catch (e) {
18
+ if (e.message?.includes("404") ||
19
+ e.message?.includes("403") ||
20
+ e.message?.includes("credentials not configured")) {
21
+ return null;
22
+ }
23
+ throw e;
24
+ }
25
+ }
26
+ async deploy() {
27
+ const dryRun = this.isDryRunActive();
28
+ const project = getProjectId();
29
+ const topicId = this.name;
30
+ const existing = await this.discoveryPromise;
31
+ console.log(`\nšŸ“¢ Finalizing GCP Pub/Sub Topic "${topicId}"...`);
32
+ if (dryRun) {
33
+ console.log(` šŸ“ [PLAN] ${existing ? "Update" : "Create"} Pub/Sub topic "${topicId}"`);
34
+ this.resolvedTopicName = `projects/${project}/topics/${topicId}`;
35
+ return {
36
+ name: topicId,
37
+ topicName: this.resolvedTopicName,
38
+ };
39
+ }
40
+ if (!existing) {
41
+ console.log(`šŸš€ Creating GCP Pub/Sub topic "${topicId}"...`);
42
+ const topic = await gcpFetch(PUBSUB_BASE, `/v1/projects/${project}/topics/${topicId}`, {
43
+ method: "PUT",
44
+ body: JSON.stringify({}),
45
+ });
46
+ this.resolvedTopicName = topic.name ?? null;
47
+ console.log(`šŸš€ Created Pub/Sub topic "${topicId}"`);
48
+ }
49
+ else {
50
+ this.resolvedTopicName = existing.name ?? null;
51
+ console.log(` āœ… GCP Pub/Sub topic "${topicId}" already exists.`);
52
+ }
53
+ await this.deploySidecars();
54
+ return {
55
+ name: topicId,
56
+ topicName: this.resolvedTopicName,
57
+ };
58
+ }
59
+ async destroy() {
60
+ const dryRun = this.isDryRunActive();
61
+ const project = getProjectId();
62
+ const topicId = this.name;
63
+ console.log(`\nšŸ—‘ļø Destroying GCP Pub/Sub Topic "${topicId}"...`);
64
+ const existing = await this.discoverTopic();
65
+ if (!existing) {
66
+ console.log(` āœ… Topic "${topicId}" does not exist - nothing to do.`);
67
+ return { destroyed: topicId };
68
+ }
69
+ if (dryRun) {
70
+ console.log(` šŸ“ [PLAN] Delete Pub/Sub topic "${topicId}"`);
71
+ return { destroyed: topicId };
72
+ }
73
+ console.log(` šŸ”„ Deleting Pub/Sub topic "${topicId}"...`);
74
+ await gcpFetch(PUBSUB_BASE, `/v1/projects/${project}/topics/${topicId}`, {
75
+ method: "DELETE",
76
+ });
77
+ console.log(` āœ… Pub/Sub topic "${topicId}" deleted.`);
78
+ await this.destroySidecars();
79
+ return { destroyed: topicId };
80
+ }
81
+ }
82
+ export class GCPPubSubSubscriptionBuilder extends BaseBuilder {
83
+ _topic;
84
+ _pushEndpoint;
85
+ _ackDeadlineSeconds = 10;
86
+ resolvedSubscriptionName = null;
87
+ constructor(subscriptionId) {
88
+ super(subscriptionId);
89
+ this.discoveryPromise = this.discoverSubscription();
90
+ }
91
+ topic(t) {
92
+ this._topic = t;
93
+ return this;
94
+ }
95
+ pushEndpoint(url) {
96
+ this._pushEndpoint = url;
97
+ return this;
98
+ }
99
+ ackDeadline(seconds) {
100
+ this._ackDeadlineSeconds = seconds;
101
+ return this;
102
+ }
103
+ async discoverSubscription() {
104
+ try {
105
+ const project = getProjectId();
106
+ const res = await gcpFetch(PUBSUB_BASE, `/v1/projects/${project}/subscriptions/${this.name}`);
107
+ this.resolvedSubscriptionName = res.name ?? null;
108
+ return res;
109
+ }
110
+ catch (e) {
111
+ if (e.message?.includes("404") ||
112
+ e.message?.includes("403") ||
113
+ e.message?.includes("credentials not configured")) {
114
+ return null;
115
+ }
116
+ throw e;
117
+ }
118
+ }
119
+ async deploy() {
120
+ const dryRun = this.isDryRunActive();
121
+ const project = getProjectId();
122
+ const subscriptionId = this.name;
123
+ console.log(`\nšŸ“„ Finalizing GCP Pub/Sub Subscription "${subscriptionId}"...`);
124
+ if (!this._topic) {
125
+ throw new Error(`[GCP.PubSub.Subscription:${subscriptionId}] .topic(...) is required`);
126
+ }
127
+ const existing = await this.discoveryPromise;
128
+ // Resolve full topic name
129
+ let targetTopicName;
130
+ if (this._topic instanceof GCPPubSubTopicBuilder) {
131
+ targetTopicName = this._topic.resolvedTopicName ?? `projects/${project}/topics/${this._topic.name}`;
132
+ }
133
+ else {
134
+ targetTopicName = this._topic.includes("/")
135
+ ? this._topic
136
+ : `projects/${project}/topics/${this._topic}`;
137
+ }
138
+ const pushConfig = this._pushEndpoint
139
+ ? { pushEndpoint: this._pushEndpoint }
140
+ : {};
141
+ if (dryRun) {
142
+ console.log(` šŸ“ [PLAN] ${existing ? "Update" : "Create"} Pub/Sub subscription "${subscriptionId}"`);
143
+ console.log(` └─ Topic: ${targetTopicName}`);
144
+ if (this._pushEndpoint) {
145
+ console.log(` └─ Push Endpoint: ${this._pushEndpoint}`);
146
+ }
147
+ else {
148
+ console.log(` └─ Pull Subscription`);
149
+ }
150
+ this.resolvedSubscriptionName = `projects/${project}/subscriptions/${subscriptionId}`;
151
+ return {
152
+ name: subscriptionId,
153
+ subscriptionName: this.resolvedSubscriptionName,
154
+ };
155
+ }
156
+ // Determine if update is needed
157
+ let needsUpdate = !existing;
158
+ if (existing) {
159
+ const existingEndpoint = existing.pushConfig?.pushEndpoint ?? "";
160
+ const targetEndpoint = this._pushEndpoint ?? "";
161
+ const hasEndpointChange = existingEndpoint !== targetEndpoint;
162
+ const hasTopicChange = existing.topic !== targetTopicName;
163
+ const hasAckChange = (existing.ackDeadlineSeconds ?? 10) !== this._ackDeadlineSeconds;
164
+ needsUpdate = hasEndpointChange || hasTopicChange || hasAckChange;
165
+ }
166
+ const subscriptionBody = {
167
+ topic: targetTopicName,
168
+ pushConfig,
169
+ ackDeadlineSeconds: this._ackDeadlineSeconds,
170
+ };
171
+ if (!existing) {
172
+ console.log(`šŸš€ Creating GCP Pub/Sub subscription "${subscriptionId}"...`);
173
+ const sub = await gcpFetch(PUBSUB_BASE, `/v1/projects/${project}/subscriptions/${subscriptionId}`, {
174
+ method: "PUT",
175
+ body: JSON.stringify(subscriptionBody),
176
+ });
177
+ this.resolvedSubscriptionName = sub.name ?? null;
178
+ console.log(`šŸš€ Created Pub/Sub subscription "${subscriptionId}"`);
179
+ }
180
+ else if (needsUpdate) {
181
+ console.log(`šŸ”„ Updating GCP Pub/Sub subscription "${subscriptionId}"...`);
182
+ const sub = await gcpFetch(PUBSUB_BASE, `/v1/projects/${project}/subscriptions/${subscriptionId}`, {
183
+ method: "PATCH",
184
+ body: JSON.stringify({
185
+ subscription: {
186
+ name: `projects/${project}/subscriptions/${subscriptionId}`,
187
+ ...subscriptionBody,
188
+ },
189
+ updateMask: "topic,pushConfig,ackDeadlineSeconds",
190
+ }),
191
+ });
192
+ this.resolvedSubscriptionName = sub.name ?? null;
193
+ console.log(`šŸ”„ Updated Pub/Sub subscription "${subscriptionId}"`);
194
+ }
195
+ else {
196
+ this.resolvedSubscriptionName = existing.name ?? null;
197
+ console.log(` āœ… GCP Pub/Sub subscription "${subscriptionId}" is up to date.`);
198
+ }
199
+ await this.deploySidecars();
200
+ return {
201
+ name: subscriptionId,
202
+ subscriptionName: this.resolvedSubscriptionName,
203
+ };
204
+ }
205
+ async destroy() {
206
+ const dryRun = this.isDryRunActive();
207
+ const project = getProjectId();
208
+ const subscriptionId = this.name;
209
+ console.log(`\nšŸ—‘ļø Destroying GCP Pub/Sub Subscription "${subscriptionId}"...`);
210
+ const existing = await this.discoverSubscription();
211
+ if (!existing) {
212
+ console.log(` āœ… Subscription "${subscriptionId}" does not exist - nothing to do.`);
213
+ return { destroyed: subscriptionId };
214
+ }
215
+ if (dryRun) {
216
+ console.log(` šŸ“ [PLAN] Delete Pub/Sub subscription "${subscriptionId}"`);
217
+ return { destroyed: subscriptionId };
218
+ }
219
+ console.log(` šŸ”„ Deleting Pub/Sub subscription "${subscriptionId}"...`);
220
+ await gcpFetch(PUBSUB_BASE, `/v1/projects/${project}/subscriptions/${subscriptionId}`, {
221
+ method: "DELETE",
222
+ });
223
+ console.log(` āœ… Pub/Sub subscription "${subscriptionId}" deleted.`);
224
+ await this.destroySidecars();
225
+ return { destroyed: subscriptionId };
226
+ }
227
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,244 @@
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 { GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder } from "./pubsub.js";
5
+ import { Config } from "../../core/config.js";
6
+ describe("GCPPubSub 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
+ },
18
+ },
19
+ });
20
+ originalFetch = globalThis.fetch;
21
+ fetchCalls = [];
22
+ mockResponses = {};
23
+ globalThis.fetch = async (input, init) => {
24
+ const url = String(input);
25
+ const method = init?.method ?? "GET";
26
+ let body;
27
+ if (init?.body) {
28
+ if (typeof init.body === "string") {
29
+ try {
30
+ body = JSON.parse(init.body);
31
+ }
32
+ catch {
33
+ body = init.body;
34
+ }
35
+ }
36
+ else {
37
+ body = "[Binary/Buffer Body]";
38
+ }
39
+ }
40
+ const headers = init?.headers;
41
+ fetchCalls.push({ url, method, body, headers });
42
+ const matchKey = Object.keys(mockResponses)
43
+ .filter((key) => {
44
+ const [mMethod, mPath] = key.split(" ");
45
+ return method === mMethod && url.includes(mPath);
46
+ })
47
+ .sort((a, b) => b.split(" ")[1].length - a.split(" ")[1].length)[0];
48
+ if (matchKey) {
49
+ const resp = mockResponses[matchKey];
50
+ return {
51
+ ok: resp.status >= 200 && resp.status < 300,
52
+ status: resp.status,
53
+ json: async () => resp.body,
54
+ text: async () => JSON.stringify(resp.body),
55
+ };
56
+ }
57
+ return {
58
+ ok: false,
59
+ status: 404,
60
+ json: async () => ({ message: `Endpoint not mocked: ${method} ${url}` }),
61
+ text: async () => `Endpoint not mocked: ${method} ${url}`,
62
+ };
63
+ };
64
+ mock.method(GoogleAuth.prototype, "getClient", async () => {
65
+ return {
66
+ getAccessToken: async () => ({ token: "fake-gcp-token" }),
67
+ };
68
+ });
69
+ });
70
+ afterEach(() => {
71
+ globalThis.fetch = originalFetch;
72
+ mock.restoreAll();
73
+ });
74
+ describe("GCPPubSubTopicBuilder", () => {
75
+ test("runs in dry-run mode safely and logs plans", async () => {
76
+ Config.set({
77
+ dryRun: true,
78
+ });
79
+ mockResponses["GET /topics/dryrun-topic"] = {
80
+ status: 404,
81
+ body: {},
82
+ };
83
+ const builder = new GCPPubSubTopicBuilder("dryrun-topic");
84
+ const result = await builder.deploy();
85
+ assert.strictEqual(result.name, "dryrun-topic");
86
+ assert.strictEqual(result.topicName, "projects/my-gcp-project/topics/dryrun-topic");
87
+ const writeCalls = fetchCalls.filter((c) => c.method !== "GET");
88
+ assert.strictEqual(writeCalls.length, 0);
89
+ });
90
+ test("creates a new topic when missing", async () => {
91
+ mockResponses["GET /topics/new-topic"] = {
92
+ status: 404,
93
+ body: {},
94
+ };
95
+ mockResponses["PUT /topics/new-topic"] = {
96
+ status: 200,
97
+ body: { name: "projects/my-gcp-project/topics/new-topic" },
98
+ };
99
+ const builder = new GCPPubSubTopicBuilder("new-topic");
100
+ const result = await builder.deploy();
101
+ assert.strictEqual(result.name, "new-topic");
102
+ assert.strictEqual(result.topicName, "projects/my-gcp-project/topics/new-topic");
103
+ const putCall = fetchCalls.find((c) => c.method === "PUT" && c.url.includes("/topics/new-topic"));
104
+ assert.ok(putCall);
105
+ });
106
+ test("skips creation if topic already exists", async () => {
107
+ mockResponses["GET /topics/exist-topic"] = {
108
+ status: 200,
109
+ body: { name: "projects/my-gcp-project/topics/exist-topic" },
110
+ };
111
+ const builder = new GCPPubSubTopicBuilder("exist-topic");
112
+ const result = await builder.deploy();
113
+ assert.strictEqual(result.name, "exist-topic");
114
+ assert.strictEqual(result.topicName, "projects/my-gcp-project/topics/exist-topic");
115
+ const writeCalls = fetchCalls.filter((c) => c.method !== "GET");
116
+ assert.strictEqual(writeCalls.length, 0);
117
+ });
118
+ test("destroys an existing topic successfully", async () => {
119
+ mockResponses["GET /topics/delete-topic"] = {
120
+ status: 200,
121
+ body: { name: "projects/my-gcp-project/topics/delete-topic" },
122
+ };
123
+ mockResponses["DELETE /topics/delete-topic"] = {
124
+ status: 200,
125
+ body: {},
126
+ };
127
+ const builder = new GCPPubSubTopicBuilder("delete-topic");
128
+ const result = await builder.destroy();
129
+ assert.deepStrictEqual(result, { destroyed: "delete-topic" });
130
+ const deleteCall = fetchCalls.find((c) => c.method === "DELETE" && c.url.includes("/topics/delete-topic"));
131
+ assert.ok(deleteCall);
132
+ });
133
+ });
134
+ describe("GCPPubSubSubscriptionBuilder", () => {
135
+ test("fluent builder api sets properties and binds topic references", () => {
136
+ const topic = new GCPPubSubTopicBuilder("my-t");
137
+ topic.resolvedTopicName = "projects/my-gcp-project/topics/my-t";
138
+ const builder = new GCPPubSubSubscriptionBuilder("my-sub")
139
+ .topic(topic)
140
+ .pushEndpoint("https://my-endpoint.com/push")
141
+ .ackDeadline(30);
142
+ assert.strictEqual(builder._topic, topic);
143
+ assert.strictEqual(builder._pushEndpoint, "https://my-endpoint.com/push");
144
+ assert.strictEqual(builder._ackDeadlineSeconds, 30);
145
+ });
146
+ test("creates pull subscription when missing", async () => {
147
+ mockResponses["GET /subscriptions/new-pull-sub"] = {
148
+ status: 404,
149
+ body: {},
150
+ };
151
+ mockResponses["PUT /subscriptions/new-pull-sub"] = {
152
+ status: 200,
153
+ body: { name: "projects/my-gcp-project/subscriptions/new-pull-sub" },
154
+ };
155
+ const builder = new GCPPubSubSubscriptionBuilder("new-pull-sub")
156
+ .topic("my-t")
157
+ .ackDeadline(20);
158
+ const result = await builder.deploy();
159
+ assert.strictEqual(result.name, "new-pull-sub");
160
+ assert.strictEqual(result.subscriptionName, "projects/my-gcp-project/subscriptions/new-pull-sub");
161
+ const putCall = fetchCalls.find((c) => c.method === "PUT" && c.url.includes("/subscriptions/new-pull-sub"));
162
+ assert.ok(putCall);
163
+ assert.strictEqual(putCall.body.topic, "projects/my-gcp-project/topics/my-t");
164
+ assert.deepStrictEqual(putCall.body.pushConfig, {});
165
+ assert.strictEqual(putCall.body.ackDeadlineSeconds, 20);
166
+ });
167
+ test("creates push subscription when missing", async () => {
168
+ mockResponses["GET /subscriptions/new-push-sub"] = {
169
+ status: 404,
170
+ body: {},
171
+ };
172
+ mockResponses["PUT /subscriptions/new-push-sub"] = {
173
+ status: 200,
174
+ body: { name: "projects/my-gcp-project/subscriptions/new-push-sub" },
175
+ };
176
+ const builder = new GCPPubSubSubscriptionBuilder("new-push-sub")
177
+ .topic("my-t")
178
+ .pushEndpoint("https://my-app.run.app/push");
179
+ const result = await builder.deploy();
180
+ assert.strictEqual(result.name, "new-push-sub");
181
+ const putCall = fetchCalls.find((c) => c.method === "PUT" && c.url.includes("/subscriptions/new-push-sub"));
182
+ assert.ok(putCall);
183
+ assert.strictEqual(putCall.body.pushConfig.pushEndpoint, "https://my-app.run.app/push");
184
+ });
185
+ test("patches subscription if endpoints differ", async () => {
186
+ mockResponses["GET /subscriptions/existing-sub"] = {
187
+ status: 200,
188
+ body: {
189
+ name: "projects/my-gcp-project/subscriptions/existing-sub",
190
+ topic: "projects/my-gcp-project/topics/my-t",
191
+ pushConfig: { pushEndpoint: "https://old-endpoint.com" },
192
+ ackDeadlineSeconds: 10,
193
+ },
194
+ };
195
+ mockResponses["PATCH /subscriptions/existing-sub"] = {
196
+ status: 200,
197
+ body: { name: "projects/my-gcp-project/subscriptions/existing-sub" },
198
+ };
199
+ const builder = new GCPPubSubSubscriptionBuilder("existing-sub")
200
+ .topic("my-t")
201
+ .pushEndpoint("https://new-endpoint.com"); // endpoint changed!
202
+ const result = await builder.deploy();
203
+ assert.strictEqual(result.name, "existing-sub");
204
+ const patchCall = fetchCalls.find((c) => c.method === "PATCH" && c.url.includes("/subscriptions/existing-sub"));
205
+ assert.ok(patchCall);
206
+ assert.strictEqual(patchCall.body.subscription.pushConfig.pushEndpoint, "https://new-endpoint.com");
207
+ assert.strictEqual(patchCall.body.updateMask, "topic,pushConfig,ackDeadlineSeconds");
208
+ });
209
+ test("skips patching if identical", async () => {
210
+ mockResponses["GET /subscriptions/ident-sub"] = {
211
+ status: 200,
212
+ body: {
213
+ name: "projects/my-gcp-project/subscriptions/ident-sub",
214
+ topic: "projects/my-gcp-project/topics/my-t",
215
+ pushConfig: { pushEndpoint: "https://endpoint.com" },
216
+ ackDeadlineSeconds: 20,
217
+ },
218
+ };
219
+ const builder = new GCPPubSubSubscriptionBuilder("ident-sub")
220
+ .topic("my-t")
221
+ .pushEndpoint("https://endpoint.com")
222
+ .ackDeadline(20);
223
+ const result = await builder.deploy();
224
+ assert.strictEqual(result.name, "ident-sub");
225
+ const writeCalls = fetchCalls.filter((c) => c.method !== "GET");
226
+ assert.strictEqual(writeCalls.length, 0);
227
+ });
228
+ test("destroys an existing subscription successfully", async () => {
229
+ mockResponses["GET /subscriptions/delete-sub"] = {
230
+ status: 200,
231
+ body: { name: "projects/my-gcp-project/subscriptions/delete-sub" },
232
+ };
233
+ mockResponses["DELETE /subscriptions/delete-sub"] = {
234
+ status: 200,
235
+ body: {},
236
+ };
237
+ const builder = new GCPPubSubSubscriptionBuilder("delete-sub");
238
+ const result = await builder.destroy();
239
+ assert.deepStrictEqual(result, { destroyed: "delete-sub" });
240
+ const deleteCall = fetchCalls.find((c) => c.method === "DELETE" && c.url.includes("/subscriptions/delete-sub"));
241
+ assert.ok(deleteCall);
242
+ });
243
+ });
244
+ });
@@ -0,0 +1,21 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ export declare class GCPSecretBuilder extends BaseBuilder {
3
+ private _value?;
4
+ resolvedValue: string | null;
5
+ resolvedArn: string | null;
6
+ constructor(secretId: string);
7
+ private fetchSecret;
8
+ awaitValue(): Promise<string | null>;
9
+ plainText(v: string): this;
10
+ keyValue(obj: object): this;
11
+ deploy(): Promise<{
12
+ name: string;
13
+ arn: string | null;
14
+ value: string | null;
15
+ }>;
16
+ destroy(): Promise<{
17
+ destroyed: string;
18
+ }>;
19
+ private discoverSecretMetadata;
20
+ }
21
+ export declare function resolveGCPEnvVars(env: Record<string, string | GCPSecretBuilder>, isDryRun?: boolean): Promise<Record<string, string>>;