create-svc 0.1.9 → 0.1.10
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 +130 -11
- package/package.json +9 -4
- package/src/cli.test.ts +29 -8
- package/src/cli.ts +103 -70
- package/src/naming.test.ts +4 -2
- package/src/naming.ts +9 -1
- package/src/neon.ts +10 -8
- package/src/post-scaffold.ts +7 -28
- package/src/profiles.ts +28 -0
- package/src/scaffold.test.ts +126 -15
- package/src/scaffold.ts +94 -23
- package/src/vault.test.ts +33 -9
- package/src/vault.ts +4 -3
- package/templates/shared/README.md +135 -24
- package/templates/shared/docker-compose.yml +19 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +15 -42
- package/templates/shared/scripts/cloudrun/cleanup.ts +17 -31
- package/templates/shared/scripts/cloudrun/config.ts +14 -19
- package/templates/shared/scripts/cloudrun/deploy.ts +19 -10
- package/templates/shared/scripts/cloudrun/integrations.ts +111 -0
- package/templates/shared/scripts/cloudrun/lib.ts +88 -112
- package/templates/shared/scripts/cloudrun/neon.ts +82 -13
- package/templates/shared/service.yaml +44 -1
- package/templates/variants/bun-connectrpc/Dockerfile +1 -0
- package/templates/variants/bun-connectrpc/Makefile +4 -1
- package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +1078 -0
- package/templates/variants/bun-connectrpc/migrations/0000_init.sql +63 -0
- package/templates/variants/bun-connectrpc/package.json +17 -0
- package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +228 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +31 -1
- package/templates/variants/bun-connectrpc/scripts/migrate.ts +46 -0
- package/templates/variants/bun-connectrpc/src/chat/service.ts +384 -0
- package/templates/variants/bun-connectrpc/src/chat/types.ts +142 -0
- package/templates/variants/bun-connectrpc/src/db/client.ts +15 -0
- package/templates/variants/bun-connectrpc/src/db/repository.ts +479 -0
- package/templates/variants/bun-connectrpc/src/db/schema.ts +75 -0
- package/templates/variants/bun-connectrpc/src/index.ts +294 -22
- package/templates/variants/bun-connectrpc/src/storage.ts +72 -0
- package/templates/variants/bun-connectrpc/src/webhooks.ts +35 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +14 -13
- package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +182 -0
- package/templates/variants/bun-connectrpc/tsconfig.json +2 -1
- package/templates/variants/bun-hono/Makefile +4 -1
- package/templates/variants/bun-hono/migrations/0000_init.sql +63 -0
- package/templates/variants/bun-hono/package.json +13 -0
- package/templates/variants/bun-hono/scripts/migrate.ts +46 -0
- package/templates/variants/bun-hono/src/chat/service.ts +384 -0
- package/templates/variants/bun-hono/src/chat/types.ts +142 -0
- package/templates/variants/bun-hono/src/db/client.ts +15 -0
- package/templates/variants/bun-hono/src/db/repository.ts +479 -0
- package/templates/variants/bun-hono/src/db/schema.ts +75 -0
- package/templates/variants/bun-hono/src/index.ts +254 -8
- package/templates/variants/bun-hono/src/storage.ts +72 -0
- package/templates/variants/bun-hono/src/webhooks.ts +35 -0
- package/templates/variants/bun-hono/test/app.test.ts +60 -6
- package/templates/variants/bun-hono/test/list-messages.integration.test.ts +256 -0
- package/templates/variants/bun-hono/tsconfig.json +1 -0
- package/templates/variants/go-chi/Makefile +6 -2
- package/templates/variants/go-chi/buf.gen.yaml +2 -0
- package/templates/variants/go-chi/cmd/migrate/main.go +101 -0
- package/templates/variants/go-chi/cmd/server/main.go +16 -15
- package/templates/variants/go-chi/go.mod +3 -0
- package/templates/variants/go-chi/internal/app/service.go +763 -71
- package/templates/variants/go-chi/internal/config/config.go +22 -7
- package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +298 -0
- package/templates/variants/go-chi/internal/httpapi/routes.go +245 -43
- package/templates/variants/go-chi/migrations/0000_init.sql +63 -0
- package/templates/variants/go-chi/protos/chat/v1/chat.proto +219 -0
- package/templates/variants/go-chi/test/go.test.ts +4 -1
- package/templates/variants/go-connectrpc/Makefile +6 -2
- package/templates/variants/go-connectrpc/buf.gen.yaml +2 -0
- package/templates/variants/go-connectrpc/cmd/migrate/main.go +101 -0
- package/templates/variants/go-connectrpc/cmd/server/main.go +35 -11
- package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +2512 -0
- package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +571 -0
- package/templates/variants/go-connectrpc/go.mod +4 -0
- package/templates/variants/go-connectrpc/internal/app/service.go +763 -71
- package/templates/variants/go-connectrpc/internal/config/config.go +22 -7
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +254 -42
- package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +216 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +41 -56
- package/templates/variants/go-connectrpc/migrations/0000_init.sql +63 -0
- package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +232 -0
- package/templates/shared/.env.example +0 -10
- package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +0 -623
- package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/variants/go-chi/internal/connectapi/handler.go +0 -79
- package/templates/variants/go-chi/protos/dns/v1/dns.proto +0 -58
- package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +0 -623
- package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +0 -58
|
@@ -15,10 +15,9 @@ type DeployArgs = {
|
|
|
15
15
|
|
|
16
16
|
type CleanupArgs = {
|
|
17
17
|
destroyProject: boolean;
|
|
18
|
-
destroyRepo: boolean;
|
|
19
18
|
};
|
|
20
19
|
|
|
21
|
-
type DeploymentTarget = {
|
|
20
|
+
export type DeploymentTarget = {
|
|
22
21
|
environment: "main" | "preview" | "personal";
|
|
23
22
|
serviceName: string;
|
|
24
23
|
branchName: string;
|
|
@@ -33,6 +32,7 @@ type CommandResult = {
|
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
const decoder = new TextDecoder();
|
|
35
|
+
const encoder = new TextEncoder();
|
|
36
36
|
|
|
37
37
|
export class CommandError extends Error {
|
|
38
38
|
command: string;
|
|
@@ -58,11 +58,27 @@ export function requireCommand(name: string) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function requireGcloudAuth() {
|
|
62
|
+
const activeAccount = gcloud(["auth", "list", "--filter=status:ACTIVE", "--format=value(account)"], {
|
|
63
|
+
allowFailure: true,
|
|
64
|
+
}).stdout.trim();
|
|
65
|
+
|
|
66
|
+
if (!activeAccount) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
[
|
|
69
|
+
"gcloud is installed but no active Google Cloud account is available.",
|
|
70
|
+
"Run `gcloud auth login` on this machine before using bootstrap, deploy, or cleanup.",
|
|
71
|
+
"If you also rely on Application Default Credentials for other tooling, run `gcloud auth application-default login` as well.",
|
|
72
|
+
].join(" ")
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
export function run(command: string, args: string[], options: CommandOptions = {}): CommandResult {
|
|
62
78
|
const result = Bun.spawnSync([command, ...args], {
|
|
63
79
|
cwd: process.cwd(),
|
|
64
80
|
env: process.env,
|
|
65
|
-
stdin: options.input,
|
|
81
|
+
stdin: options.input === undefined ? undefined : encoder.encode(options.input),
|
|
66
82
|
stdout: "pipe",
|
|
67
83
|
stderr: "pipe",
|
|
68
84
|
});
|
|
@@ -89,10 +105,6 @@ export function gcloud(args: string[], options: CommandOptions = {}) {
|
|
|
89
105
|
return run("gcloud", normalized, options);
|
|
90
106
|
}
|
|
91
107
|
|
|
92
|
-
export function gh(args: string[], options: CommandOptions = {}) {
|
|
93
|
-
return run("gh", args, options);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
108
|
export async function runStep<T>(label: string, task: () => Promise<T> | T) {
|
|
97
109
|
const indicator = spinner();
|
|
98
110
|
indicator.start(label);
|
|
@@ -228,116 +240,26 @@ export function ensureArtifactRepository() {
|
|
|
228
240
|
]);
|
|
229
241
|
}
|
|
230
242
|
|
|
231
|
-
export function
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function workloadIdentityPoolResource() {
|
|
236
|
-
return `projects/${projectNumber()}/locations/global/workloadIdentityPools/${config.workloadIdentityPoolId}`;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export function workloadIdentityProviderResource() {
|
|
240
|
-
return `${workloadIdentityPoolResource()}/providers/${config.workloadIdentityProviderId}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function ensureWorkloadIdentityPool() {
|
|
244
|
-
if (
|
|
245
|
-
gcloud(["iam", "workload-identity-pools", "describe", config.workloadIdentityPoolId, "--project", config.project.id, "--location", "global"], {
|
|
246
|
-
allowFailure: true,
|
|
247
|
-
}).success
|
|
248
|
-
) {
|
|
243
|
+
export function ensureStorageBucket() {
|
|
244
|
+
if (gcloud(["storage", "buckets", "describe", `gs://${config.storage.attachmentBucket}`, "--project", config.project.id], { allowFailure: true }).success) {
|
|
249
245
|
return;
|
|
250
246
|
}
|
|
251
247
|
|
|
252
248
|
gcloud([
|
|
253
|
-
"
|
|
254
|
-
"
|
|
249
|
+
"storage",
|
|
250
|
+
"buckets",
|
|
255
251
|
"create",
|
|
256
|
-
config.
|
|
252
|
+
`gs://${config.storage.attachmentBucket}`,
|
|
257
253
|
"--project",
|
|
258
254
|
config.project.id,
|
|
259
255
|
"--location",
|
|
260
|
-
|
|
261
|
-
"--
|
|
262
|
-
"GitHub Actions",
|
|
263
|
-
]);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function ensureWorkloadIdentityProvider() {
|
|
267
|
-
if (
|
|
268
|
-
gcloud(
|
|
269
|
-
[
|
|
270
|
-
"iam",
|
|
271
|
-
"workload-identity-pools",
|
|
272
|
-
"providers",
|
|
273
|
-
"describe",
|
|
274
|
-
config.workloadIdentityProviderId,
|
|
275
|
-
"--project",
|
|
276
|
-
config.project.id,
|
|
277
|
-
"--location",
|
|
278
|
-
"global",
|
|
279
|
-
"--workload-identity-pool",
|
|
280
|
-
config.workloadIdentityPoolId,
|
|
281
|
-
],
|
|
282
|
-
{ allowFailure: true }
|
|
283
|
-
).success
|
|
284
|
-
) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
gcloud([
|
|
289
|
-
"iam",
|
|
290
|
-
"workload-identity-pools",
|
|
291
|
-
"providers",
|
|
292
|
-
"create-oidc",
|
|
293
|
-
config.workloadIdentityProviderId,
|
|
294
|
-
"--project",
|
|
295
|
-
config.project.id,
|
|
296
|
-
"--location",
|
|
297
|
-
"global",
|
|
298
|
-
"--workload-identity-pool",
|
|
299
|
-
config.workloadIdentityPoolId,
|
|
300
|
-
"--display-name",
|
|
301
|
-
`${config.serviceName} GitHub`,
|
|
302
|
-
"--issuer-uri",
|
|
303
|
-
"https://token.actions.githubusercontent.com",
|
|
304
|
-
"--attribute-mapping",
|
|
305
|
-
"google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner",
|
|
306
|
-
"--attribute-condition",
|
|
307
|
-
`assertion.repository=='${config.github.repo}'`,
|
|
256
|
+
config.region,
|
|
257
|
+
"--uniform-bucket-level-access",
|
|
308
258
|
]);
|
|
309
259
|
}
|
|
310
260
|
|
|
311
|
-
export function
|
|
312
|
-
gcloud(
|
|
313
|
-
[
|
|
314
|
-
"iam",
|
|
315
|
-
"workload-identity-pools",
|
|
316
|
-
"providers",
|
|
317
|
-
"delete",
|
|
318
|
-
config.workloadIdentityProviderId,
|
|
319
|
-
"--project",
|
|
320
|
-
config.project.id,
|
|
321
|
-
"--location",
|
|
322
|
-
"global",
|
|
323
|
-
"--workload-identity-pool",
|
|
324
|
-
config.workloadIdentityPoolId,
|
|
325
|
-
"--quiet",
|
|
326
|
-
],
|
|
327
|
-
{ allowFailure: true }
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export function setGithubVariable(name: string, value: string) {
|
|
332
|
-
gh(["variable", "set", name, "--repo", config.github.repo, "--body", value]);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export function deleteGithubVariable(name: string) {
|
|
336
|
-
gh(["variable", "delete", name, "--repo", config.github.repo], { allowFailure: true });
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export function deleteGithubRepository() {
|
|
340
|
-
gh(["repo", "delete", config.github.repo, "--yes"]);
|
|
261
|
+
export function projectNumber() {
|
|
262
|
+
return gcloud(["projects", "describe", config.project.id, "--format=value(projectNumber)"]).stdout;
|
|
341
263
|
}
|
|
342
264
|
|
|
343
265
|
export function imageTag() {
|
|
@@ -408,7 +330,6 @@ export function parseDeployArgs(argv: string[]): DeployArgs {
|
|
|
408
330
|
export function parseCleanupArgs(argv: string[]): CleanupArgs {
|
|
409
331
|
const parsed: CleanupArgs = {
|
|
410
332
|
destroyProject: false,
|
|
411
|
-
destroyRepo: false,
|
|
412
333
|
};
|
|
413
334
|
|
|
414
335
|
for (const token of argv) {
|
|
@@ -416,11 +337,6 @@ export function parseCleanupArgs(argv: string[]): CleanupArgs {
|
|
|
416
337
|
parsed.destroyProject = true;
|
|
417
338
|
continue;
|
|
418
339
|
}
|
|
419
|
-
|
|
420
|
-
if (token === "--repo") {
|
|
421
|
-
parsed.destroyRepo = true;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
340
|
}
|
|
425
341
|
|
|
426
342
|
return parsed;
|
|
@@ -458,6 +374,19 @@ export function resolveDeploymentTarget(environment: DeployArgs["environment"],
|
|
|
458
374
|
};
|
|
459
375
|
}
|
|
460
376
|
|
|
377
|
+
export function runtimeSecretNames(target: DeploymentTarget) {
|
|
378
|
+
return {
|
|
379
|
+
CLERK_SECRET_KEY: `${target.serviceName}-clerk-secret-key`,
|
|
380
|
+
CLERK_WEBHOOK_SECRET: `${target.serviceName}-clerk-webhook-secret`,
|
|
381
|
+
STRIPE_SECRET_KEY: `${target.serviceName}-stripe-secret-key`,
|
|
382
|
+
STRIPE_WEBHOOK_SECRET: `${target.serviceName}-stripe-webhook-secret`,
|
|
383
|
+
REVENUECAT_API_KEY: `${target.serviceName}-revenuecat-api-key`,
|
|
384
|
+
REVENUECAT_WEBHOOK_SECRET: `${target.serviceName}-revenuecat-webhook-secret`,
|
|
385
|
+
RESEND_API_KEY: `${target.serviceName}-resend-api-key`,
|
|
386
|
+
POSTHOG_API_KEY: `${target.serviceName}-posthog-api-key`,
|
|
387
|
+
} as const;
|
|
388
|
+
}
|
|
389
|
+
|
|
461
390
|
export async function renderManifest(image: string, target: DeploymentTarget) {
|
|
462
391
|
const template = await Bun.file(new URL("../../service.yaml", import.meta.url)).text();
|
|
463
392
|
const values = {
|
|
@@ -465,8 +394,11 @@ export async function renderManifest(image: string, target: DeploymentTarget) {
|
|
|
465
394
|
RUNTIME_SERVICE_ACCOUNT: config.runtimeServiceAccount,
|
|
466
395
|
IMAGE_URL: image,
|
|
467
396
|
DATABASE_URL_SECRET: target.databaseSecretName,
|
|
397
|
+
...runtimeSecretNames(target),
|
|
468
398
|
SERVICE_RUNTIME: config.runtime,
|
|
469
399
|
SERVICE_FRAMEWORK: config.framework,
|
|
400
|
+
ATTACHMENT_BUCKET: config.storage.attachmentBucket,
|
|
401
|
+
ATTACHMENT_PUBLIC_BASE_URL: config.storage.attachmentPublicBaseUrl,
|
|
470
402
|
};
|
|
471
403
|
|
|
472
404
|
return template.replace(/\$\{([A-Z0-9_]+)\}/g, (_, key: string) => {
|
|
@@ -491,6 +423,50 @@ export function serviceUrl(serviceName: string) {
|
|
|
491
423
|
).stdout;
|
|
492
424
|
}
|
|
493
425
|
|
|
426
|
+
export function serviceDomain(target: DeploymentTarget) {
|
|
427
|
+
if (target.environment === "main") {
|
|
428
|
+
return config.domain.hostname;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return `${target.serviceName}-${config.project.id}-${config.region}.a.run.app`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function serviceOrigin(target: DeploymentTarget) {
|
|
435
|
+
if (target.environment === "main") {
|
|
436
|
+
return `https://${config.domain.hostname}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const url = serviceUrl(target.serviceName);
|
|
440
|
+
return url || `https://${serviceDomain(target)}`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function ensureProductionDomainMapping(serviceName: string) {
|
|
444
|
+
if (gcloud(["beta", "run", "domain-mappings", "describe", "--domain", config.domain.hostname, "--project", config.project.id], { allowFailure: true }).success) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
gcloud([
|
|
449
|
+
"beta",
|
|
450
|
+
"run",
|
|
451
|
+
"domain-mappings",
|
|
452
|
+
"create",
|
|
453
|
+
"--service",
|
|
454
|
+
serviceName,
|
|
455
|
+
"--domain",
|
|
456
|
+
config.domain.hostname,
|
|
457
|
+
"--project",
|
|
458
|
+
config.project.id,
|
|
459
|
+
"--region",
|
|
460
|
+
config.region,
|
|
461
|
+
]);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function deleteProductionDomainMapping() {
|
|
465
|
+
gcloud(["beta", "run", "domain-mappings", "delete", "--domain", config.domain.hostname, "--project", config.project.id, "--quiet"], {
|
|
466
|
+
allowFailure: true,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
494
470
|
export function listCloudRunServices() {
|
|
495
471
|
return gcloud(["run", "services", "list", "--project", config.project.id, "--region", config.region, "--format=value(metadata.name)"]).stdout
|
|
496
472
|
.split("\n")
|
|
@@ -3,11 +3,26 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { config } from "./config";
|
|
5
5
|
|
|
6
|
+
type NeonProject = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
6
11
|
type NeonBranch = {
|
|
7
12
|
id: string;
|
|
8
13
|
name: string;
|
|
9
14
|
};
|
|
10
15
|
|
|
16
|
+
type ResolvedNeonConfig = {
|
|
17
|
+
projectId: string;
|
|
18
|
+
baseBranchId: string;
|
|
19
|
+
baseBranchName: string;
|
|
20
|
+
databaseName: string;
|
|
21
|
+
roleName: string;
|
|
22
|
+
previewBranchPrefix: string;
|
|
23
|
+
personalBranchPrefix: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
11
26
|
async function resolveNeonApiKey() {
|
|
12
27
|
const direct = process.env.NEON_API_KEY?.trim();
|
|
13
28
|
if (direct) {
|
|
@@ -17,8 +32,8 @@ async function resolveNeonApiKey() {
|
|
|
17
32
|
const addr = process.env.VAULT_ADDR?.trim() ?? "";
|
|
18
33
|
const token = await resolveVaultToken();
|
|
19
34
|
const mount = process.env.VAULT_SECRET_MOUNT?.trim() ?? "secret";
|
|
20
|
-
const path = process.env.VAULT_NEON_API_KEY_PATH?.trim() ?? "
|
|
21
|
-
const field = process.env.VAULT_NEON_API_KEY_FIELD?.trim() ?? "
|
|
35
|
+
const path = process.env.VAULT_NEON_API_KEY_PATH?.trim() ?? "prod/providers/neon";
|
|
36
|
+
const field = process.env.VAULT_NEON_API_KEY_FIELD?.trim() ?? "api_key";
|
|
22
37
|
|
|
23
38
|
if (!addr || !token) {
|
|
24
39
|
throw new Error("NEON_API_KEY is required for Neon provisioning, or set VAULT_ADDR with VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token");
|
|
@@ -57,7 +72,7 @@ async function resolveVaultToken() {
|
|
|
57
72
|
return direct;
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
const tokenFile = process.env.VAULT_TOKEN_FILE?.trim() || join(homedir(), ".vault-token");
|
|
75
|
+
const tokenFile = process.env.VAULT_TOKEN_FILE?.trim() || join(process.env.HOME?.trim() || homedir(), ".vault-token");
|
|
61
76
|
|
|
62
77
|
try {
|
|
63
78
|
return (await Bun.file(tokenFile).text()).trim();
|
|
@@ -71,15 +86,68 @@ async function neonClient() {
|
|
|
71
86
|
return createApiClient({ apiKey });
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
export async function listProjects() {
|
|
90
|
+
const payload = await (await neonClient()).listProjects({ limit: 100 });
|
|
91
|
+
const projects = ((payload.data as { projects?: Array<{ id?: string; name?: string }> } | undefined)?.projects ?? []);
|
|
92
|
+
return projects
|
|
93
|
+
.map((project: { id?: string; name?: string }) => ({
|
|
94
|
+
id: project.id ?? "",
|
|
95
|
+
name: project.name ?? project.id ?? "",
|
|
96
|
+
}))
|
|
97
|
+
.filter((project: NeonProject): project is NeonProject => Boolean(project.id))
|
|
98
|
+
.sort((left: NeonProject, right: NeonProject) => left.name.localeCompare(right.name));
|
|
99
|
+
}
|
|
100
|
+
|
|
74
101
|
export async function listBranches(projectId: string) {
|
|
75
102
|
const payload = await (await neonClient()).listProjectBranches({ projectId });
|
|
76
|
-
|
|
77
|
-
|
|
103
|
+
const branches = ((payload.data as { branches?: Array<{ id?: string; name?: string }> } | undefined)?.branches ?? []);
|
|
104
|
+
return branches
|
|
105
|
+
.map((branch: { id?: string; name?: string }) => ({
|
|
78
106
|
id: branch.id ?? "",
|
|
79
107
|
name: branch.name ?? branch.id ?? "",
|
|
80
108
|
}))
|
|
81
|
-
.filter((branch): branch is NeonBranch => Boolean(branch.id))
|
|
82
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
109
|
+
.filter((branch: NeonBranch): branch is NeonBranch => Boolean(branch.id))
|
|
110
|
+
.sort((left: NeonBranch, right: NeonBranch) => left.name.localeCompare(right.name));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function resolveNeonConfig(): Promise<ResolvedNeonConfig> {
|
|
114
|
+
const configuredProjectId = config.neon.projectId.trim();
|
|
115
|
+
const configuredBaseBranchId = config.neon.baseBranchId.trim();
|
|
116
|
+
const configuredBaseBranchName = config.neon.baseBranchName.trim() || "main";
|
|
117
|
+
|
|
118
|
+
if (configuredProjectId && configuredBaseBranchId) {
|
|
119
|
+
return {
|
|
120
|
+
projectId: configuredProjectId,
|
|
121
|
+
baseBranchId: configuredBaseBranchId,
|
|
122
|
+
baseBranchName: configuredBaseBranchName,
|
|
123
|
+
databaseName: config.neon.databaseName,
|
|
124
|
+
roleName: config.neon.roleName,
|
|
125
|
+
previewBranchPrefix: config.neon.previewBranchPrefix,
|
|
126
|
+
personalBranchPrefix: config.neon.personalBranchPrefix,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const projects = await listProjects();
|
|
131
|
+
const project = projects[0];
|
|
132
|
+
if (!project) {
|
|
133
|
+
throw new Error(`No Neon projects are available for ${config.serviceName}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const branches = await listBranches(project.id);
|
|
137
|
+
const branch = branches.find((candidate) => candidate.name === configuredBaseBranchName) ?? branches[0];
|
|
138
|
+
if (!branch) {
|
|
139
|
+
throw new Error(`No Neon branches are available in project ${project.id}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
projectId: project.id,
|
|
144
|
+
baseBranchId: branch.id,
|
|
145
|
+
baseBranchName: branch.name,
|
|
146
|
+
databaseName: config.neon.databaseName,
|
|
147
|
+
roleName: config.neon.roleName,
|
|
148
|
+
previewBranchPrefix: config.neon.previewBranchPrefix,
|
|
149
|
+
personalBranchPrefix: config.neon.personalBranchPrefix,
|
|
150
|
+
};
|
|
83
151
|
}
|
|
84
152
|
|
|
85
153
|
export async function ensureDatabase(projectId: string, branchId: string, databaseName: string) {
|
|
@@ -98,6 +166,7 @@ export async function ensureDatabase(projectId: string, branchId: string, databa
|
|
|
98
166
|
await client.createProjectBranchDatabase(projectId, branchId, {
|
|
99
167
|
database: {
|
|
100
168
|
name: databaseName,
|
|
169
|
+
owner_name: config.neon.roleName,
|
|
101
170
|
},
|
|
102
171
|
});
|
|
103
172
|
}
|
|
@@ -127,12 +196,12 @@ export async function ensureBranch(projectId: string, branchName: string, parent
|
|
|
127
196
|
},
|
|
128
197
|
endpoints: [
|
|
129
198
|
{
|
|
130
|
-
type: "read_write",
|
|
199
|
+
type: "read_write" as never,
|
|
131
200
|
},
|
|
132
201
|
],
|
|
133
202
|
});
|
|
134
203
|
|
|
135
|
-
const branch = payload.branch;
|
|
204
|
+
const branch = (payload.data as { branch?: { id?: string; name?: string } } | undefined)?.branch;
|
|
136
205
|
if (!branch?.id) {
|
|
137
206
|
throw new Error(`Neon did not return a branch for ${branchName}`);
|
|
138
207
|
}
|
|
@@ -158,12 +227,12 @@ export async function deleteBranch(projectId: string, branchId: string) {
|
|
|
158
227
|
export async function getConnectionUri(projectId: string, branchId: string, databaseName: string, roleName: string) {
|
|
159
228
|
const payload = await (await neonClient()).getConnectionUri({
|
|
160
229
|
projectId,
|
|
161
|
-
branchId,
|
|
162
|
-
databaseName,
|
|
163
|
-
roleName,
|
|
230
|
+
branch_id: branchId,
|
|
231
|
+
database_name: databaseName,
|
|
232
|
+
role_name: roleName,
|
|
164
233
|
});
|
|
165
234
|
|
|
166
|
-
const uri = payload.uri;
|
|
235
|
+
const uri = (payload.data as { uri?: string } | undefined)?.uri;
|
|
167
236
|
if (!uri) {
|
|
168
237
|
throw new Error(`Neon did not return a connection URI for ${databaseName} in ${config.serviceName}`);
|
|
169
238
|
}
|
|
@@ -22,7 +22,50 @@ spec:
|
|
|
22
22
|
secretKeyRef:
|
|
23
23
|
name: ${DATABASE_URL_SECRET}
|
|
24
24
|
key: latest
|
|
25
|
+
- name: CLERK_SECRET_KEY
|
|
26
|
+
valueFrom:
|
|
27
|
+
secretKeyRef:
|
|
28
|
+
name: ${CLERK_SECRET_KEY}
|
|
29
|
+
key: latest
|
|
30
|
+
- name: CLERK_WEBHOOK_SECRET
|
|
31
|
+
valueFrom:
|
|
32
|
+
secretKeyRef:
|
|
33
|
+
name: ${CLERK_WEBHOOK_SECRET}
|
|
34
|
+
key: latest
|
|
35
|
+
- name: STRIPE_SECRET_KEY
|
|
36
|
+
valueFrom:
|
|
37
|
+
secretKeyRef:
|
|
38
|
+
name: ${STRIPE_SECRET_KEY}
|
|
39
|
+
key: latest
|
|
40
|
+
- name: STRIPE_WEBHOOK_SECRET
|
|
41
|
+
valueFrom:
|
|
42
|
+
secretKeyRef:
|
|
43
|
+
name: ${STRIPE_WEBHOOK_SECRET}
|
|
44
|
+
key: latest
|
|
45
|
+
- name: REVENUECAT_API_KEY
|
|
46
|
+
valueFrom:
|
|
47
|
+
secretKeyRef:
|
|
48
|
+
name: ${REVENUECAT_API_KEY}
|
|
49
|
+
key: latest
|
|
50
|
+
- name: REVENUECAT_WEBHOOK_SECRET
|
|
51
|
+
valueFrom:
|
|
52
|
+
secretKeyRef:
|
|
53
|
+
name: ${REVENUECAT_WEBHOOK_SECRET}
|
|
54
|
+
key: latest
|
|
55
|
+
- name: RESEND_API_KEY
|
|
56
|
+
valueFrom:
|
|
57
|
+
secretKeyRef:
|
|
58
|
+
name: ${RESEND_API_KEY}
|
|
59
|
+
key: latest
|
|
60
|
+
- name: POSTHOG_API_KEY
|
|
61
|
+
valueFrom:
|
|
62
|
+
secretKeyRef:
|
|
63
|
+
name: ${POSTHOG_API_KEY}
|
|
64
|
+
key: latest
|
|
65
|
+
- name: ATTACHMENT_BUCKET
|
|
66
|
+
value: ${ATTACHMENT_BUCKET}
|
|
67
|
+
- name: ATTACHMENT_PUBLIC_BASE_URL
|
|
68
|
+
value: ${ATTACHMENT_PUBLIC_BASE_URL}
|
|
25
69
|
traffic:
|
|
26
70
|
- latestRevision: true
|
|
27
71
|
percent: 100
|
|
28
|
-
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
.PHONY: dev gen lint test bootstrap deploy cleanup
|
|
1
|
+
.PHONY: dev migrate gen lint test bootstrap deploy cleanup
|
|
2
2
|
|
|
3
3
|
CLOUDRUN := npx --no-install svc-cloudrun
|
|
4
4
|
|
|
5
5
|
dev:
|
|
6
6
|
bun run ./src/index.ts
|
|
7
7
|
|
|
8
|
+
migrate:
|
|
9
|
+
bun run ./scripts/migrate.ts
|
|
10
|
+
|
|
8
11
|
gen:
|
|
9
12
|
bun run ./scripts/codegen.ts
|
|
10
13
|
|