1import { Query } from "@distilled.cloud/core/query";
2import { Railway as RailwaySdk } from "@distilled.cloud/railway";
3import * as Railway from "@/Railway";
4import { suitePartition } from "./suiteProject.ts";
5import * as Test from "@/Test/Alchemy";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: Railway.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const asVariableMap = (value: unknown): Record<string, string> => {
19 if (value === null || typeof value !== "object" || Array.isArray(value)) {
20 return {};
21 }
22 const out: Record<string, string> = {};
23 for (const [key, item] of Object.entries(value)) {
24 if (typeof item === "string") {
25 out[key] = item;
26 }
27 }
28 return out;
29};
30
31const queryVariables = Query.fn(
32 (projectId: string, environmentId: string, serviceId?: string) =>
33 RailwaySdk.variables({
34 projectId,
35 environmentId,
36 ...(serviceId !== undefined ? { serviceId } : {}),
37 unrendered: true,
38 }),
39);
40
41const readVariables = (
42 projectId: string,
43 environmentId: string,
44 serviceId?: string,
45) =>
46 queryVariables(projectId, environmentId, serviceId).pipe(
47 Effect.map(asVariableMap),
48 Effect.catchTag("RailwayNotFound", () =>
49 Effect.succeed({} as Record<string, string>),
50 ),
51 );
52
53const waitUntilVariableGone = (
54 projectId: string,
55 environmentId: string,
56 name: string,
57 serviceId?: string,
58) =>
59 readVariables(projectId, environmentId, serviceId).pipe(
60 Effect.map((vars) =>
61 Object.hasOwn(vars, name) ? ("found" as const) : ("gone" as const),
62 ),
63 Effect.repeat({
64 schedule: Schedule.spaced("1 second"),
65 until: (status) => status === "gone",
66 times: 10,
67 }),
68 );
69
70const isPostgresUri = (value: string | undefined) =>
71 value !== undefined &&
72 (value.startsWith("postgres://") || value.startsWith("postgresql://"));
73
74test.provider(
75 "upserting DATABASE_URL: Railway.ref(Db, DATABASE_URL) stores the template, not a resolved URI",
76 (stack) =>
77 Effect.gen(function* () {
78 expect(Railway.ref({ LogicalId: "Db" }, "DATABASE_URL")).toEqual(
79 "${{Db.DATABASE_URL}}",
80 );
81 expect(Railway.ref("shared", "SENTRY_DSN")).toEqual(
82 "${{shared.SENTRY_DSN}}",
83 );
84
85 yield* stack.destroy();
86
87 const created = yield* stack.deploy(
88 Effect.gen(function* () {
89 const { project, environment } = yield* suitePartition;
90 const db = yield* Railway.Postgres("Db", {
91 project,
92 environment,
93 public: false,
94 });
95 const template = Railway.ref(db, "DATABASE_URL");
96 const databaseUrl = yield* Railway.Variable("DatabaseUrl", {
97 project,
98 environment,
99 name: "DATABASE_URL",
100 value: template,
101 });
102 const sentry = yield* Railway.Variable("SentryDsn", {
103 project,
104 environment,
105 name: "SENTRY_DSN",
106 value: "https://example.ingest.sentry.io/1",
107 });
108 const sentryRef = yield* Railway.Variable("SentryDsnRef", {
109 project,
110 environment,
111 service: db,
112 name: "SENTRY_DSN",
113 value: Railway.ref("shared", "SENTRY_DSN"),
114 });
115 return {
116 project,
117 environment,
118 db,
119 databaseUrl,
120 sentry,
121 sentryRef,
122 template,
123 };
124 }),
125 );
126
127 expect(created.template).toEqual("${{Db.DATABASE_URL}}");
128 expect(isPostgresUri(created.template)).toBe(false);
129
130 expect(created.databaseUrl.name).toEqual("DATABASE_URL");
131 expect(created.databaseUrl.serviceId).toBeUndefined();
132 expect(Object.hasOwn(created.databaseUrl, "value")).toBe(false);
133
134 const shared = yield* readVariables(
135 created.databaseUrl.projectId,
136 created.databaseUrl.environmentId,
137 );
138 expect(shared.DATABASE_URL).toEqual(created.template);
139 expect(shared.DATABASE_URL).toEqual("${{Db.DATABASE_URL}}");
140 expect(shared.DATABASE_URL).not.toEqual(created.db.connectionUri);
141 expect(isPostgresUri(shared.DATABASE_URL)).toBe(false);
142 expect(shared.SENTRY_DSN).toEqual("https://example.ingest.sentry.io/1");
143
144 const onDb = yield* readVariables(
145 created.db.projectId,
146 created.db.environmentId,
147 created.db.serviceId,
148 );
149 expect(onDb.SENTRY_DSN).toEqual(Railway.ref("shared", "SENTRY_DSN"));
150 expect(onDb.SENTRY_DSN).toEqual("${{shared.SENTRY_DSN}}");
151 expect(onDb.DATABASE_URL).not.toEqual(created.template);
152 expect((onDb.DATABASE_URL ?? "").includes("${{")).toBe(true);
153
154 yield* stack.destroy();
155
156 const variableGone = yield* waitUntilVariableGone(
157 created.databaseUrl.projectId,
158 created.databaseUrl.environmentId,
159 created.databaseUrl.name,
160 );
161 expect(variableGone).toEqual("gone");
162 }).pipe(logLevel),
163 {
164 tags: [
165 "provider:railway",
166 "provider:railway:postgres",
167 "provider:railway:project",
168 "provider:railway:projectenvironment",
169 "provider:railway:variable",
170 "live",
171 ],
172 timeout: 120_000,
173 },
174);