1import { Query, type UnwrapPlan } from "@distilled.cloud/core/query";
2import {
3 Railway,
4 type AuditLog as RailwayAuditLog,
5 type AuditLogFilterInput,
6} from "@distilled.cloud/railway";
7import * as Effect from "effect/Effect";
8import { resolveWorkspaceId } from "./Environment.ts";
9
10const auditLogFields = <E>(row: Query<RailwayAuditLog, E>) => ({
11 id: row.id,
12 eventType: row.eventType,
13 createdAt: row.createdAt,
14 workspaceId: row.workspaceId,
15 projectId: row.projectId,
16 environmentId: row.environmentId,
17 payload: row.payload,
18 context: row.context,
19});
20type AuditLogRow = UnwrapPlan<ReturnType<typeof auditLogFields>>;
21
22const readAuditLogs = Query.fn(
23 (args: {
24 workspaceId: string;
25 first: number;
26 sort?: "asc" | "desc";
27 filter?: AuditLogFilterInput;
28 }) => Railway.auditLogs(args).pipe(Query.map(auditLogFields)),
29);
30
31const readAuditLog = Query.fn((id: string, workspaceId: string) =>
32 auditLogFields(Railway.auditLog({ id, workspaceId })),
33);
34
35const readAuditLogEventTypes = Query.fn(() =>
36 Railway.auditLogEventTypeInfo().pipe(
37 Query.map((info) => ({
38 description: info.description,
39 eventType: info.eventType,
40 })),
41 ),
42);
43type AuditLogEventTypeInfoResultItem = UnwrapPlan<
44 ReturnType<typeof readAuditLogEventTypes>
45>[number];
46
47/**
48 * Project identity for {@link listAuditLogs}. Accepts a `Railway.Project`
49 * or a `{ projectId }` stub.
50 */
51export type AuditLogProject = {
52 readonly projectId: string;
53};
54
55/**
56 * Environment identity for {@link listAuditLogs}. Accepts a
57 * `Railway.Project` (primary environment), a `Railway.Environment`, or
58 * `{ environmentId }`.
59 */
60export type AuditLogEnvironment = {
61 readonly environmentId: string;
62};
63
64export interface ListAuditLogsOptions {
65 /**
66 * Restrict to a project. Accepts a `Railway.Project` or `{ projectId }`.
67 */
68 project?: AuditLogProject | string;
69 /**
70 * Restrict to an environment. Accepts a `Railway.Project`, a
71 * `Railway.Environment`, or `{ environmentId }`.
72 */
73 environment?: AuditLogEnvironment | string;
74 /**
75 * Event type names from {@link listAuditLogEventTypes}.
76 */
77 eventTypes?: readonly string[];
78 /**
79 * Inclusive start (RFC3339).
80 */
81 startDate?: string;
82 /**
83 * Inclusive end (RFC3339).
84 */
85 endDate?: string;
86 /**
87 * Workspace id. Defaults to the current token workspace.
88 */
89 workspaceId?: string;
90 /**
91 * Page size. Defaults to 50. This helper fetches one page — it does
92 * not walk the whole workspace history.
93 * @default 50
94 */
95 first?: number;
96 /**
97 * Sort by `createdAt`. Railway default is `desc`.
98 */
99 sort?: "asc" | "desc";
100}
101
102/**
103 * One workspace audit-log row from `auditLogs` / `auditLog`.
104 */
105export interface AuditLogEntry {
106 /** Audit log id. */
107 readonly id: string;
108 /** Event type name (see {@link listAuditLogEventTypes}). */
109 readonly eventType: string;
110 /** RFC3339 creation timestamp. */
111 readonly createdAt: string;
112 /** Workspace the event was recorded in. */
113 readonly workspaceId: string | undefined;
114 /** Project the event targeted, when Railway attached one. */
115 readonly projectId: string | undefined;
116 /** Environment the event targeted, when Railway attached one. */
117 readonly environmentId: string | undefined;
118 /** Event payload. Shape depends on `eventType`. */
119 readonly payload: unknown;
120 /** Extra context Railway attached to the event. */
121 readonly context: unknown;
122}
123
124export type AuditLogEventType = AuditLogEventTypeInfoResultItem;
125
126const toEntry = (row: AuditLogRow): AuditLogEntry => ({
127 id: row.id,
128 eventType: row.eventType,
129 createdAt: row.createdAt,
130 workspaceId: row.workspaceId ?? undefined,
131 projectId: row.projectId ?? undefined,
132 environmentId: row.environmentId ?? undefined,
133 payload: row.payload ?? undefined,
134 context: row.context ?? undefined,
135});
136
137const projectIdOf = (
138 value: AuditLogProject | string | undefined,
139): string | undefined => {
140 if (value === undefined) return undefined;
141 if (typeof value === "string") return value;
142 return value.projectId;
143};
144
145const environmentIdOf = (
146 value: AuditLogEnvironment | string | undefined,
147): string | undefined => {
148 if (value === undefined) return undefined;
149 if (typeof value === "string") return value;
150 return value.environmentId;
151};
152
153const workspaceOf = (workspaceId: string | undefined) =>
154 workspaceId !== undefined
155 ? Effect.succeed(workspaceId)
156 : resolveWorkspaceId();
157
158/**
159 * List workspace audit logs. Query-only — Railway has no audit-log
160 * create/update/delete, so this is a catalog helper, not a resource.
161 *
162 * Defaults to the current token workspace. Pass `project` /
163 * `environment` to filter. The result may be empty.
164 *
165 * @see https://docs.railway.com/reference/audit-logs
166 *
167 * ### List logs
168 * **Example:** Workspace logs
169 * ```typescript
170 * const logs = yield* Railway.listAuditLogs();
171 * ```
172 *
173 * **Example:** Filter by project
174 * ```typescript
175 * const site = yield* Railway.Project("Site");
176 * const logs = yield* Railway.listAuditLogs({
177 * project: site,
178 * environment: site,
179 * });
180 * ```
181 *
182 * ### Get by id
183 * **Example:** One row
184 * ```typescript
185 * const log = yield* Railway.getAuditLog({ id: logs[0].id });
186 * ```
187 *
188 * ### Event types
189 * **Example:** Catalog
190 * ```typescript
191 * const types = yield* Railway.listAuditLogEventTypes();
192 * ```
193 *
194 * @resource
195 * @product Workspace
196 */
197export const AuditLog = Effect.fn(function* (options?: ListAuditLogsOptions) {
198 const workspaceId = yield* workspaceOf(options?.workspaceId);
199 const projectId = projectIdOf(options?.project);
200 const environmentId = environmentIdOf(options?.environment);
201 const first = options?.first ?? 50;
202 const filter =
203 projectId !== undefined ||
204 environmentId !== undefined ||
205 options?.eventTypes !== undefined ||
206 options?.startDate !== undefined ||
207 options?.endDate !== undefined
208 ? {
209 ...(projectId !== undefined ? { projectId } : {}),
210 ...(environmentId !== undefined ? { environmentId } : {}),
211 ...(options?.eventTypes !== undefined
212 ? { eventTypes: [...options.eventTypes] }
213 : {}),
214 ...(options?.startDate !== undefined
215 ? { startDate: options.startDate }
216 : {}),
217 ...(options?.endDate !== undefined
218 ? { endDate: options.endDate }
219 : {}),
220 }
221 : undefined;
222
223 const rows = yield* readAuditLogs({
224 workspaceId,
225 first,
226 ...(options?.sort !== undefined ? { sort: options.sort } : {}),
227 ...(filter !== undefined ? { filter } : {}),
228 });
229
230 return rows.map(toEntry);
231});
232
233/** Alias of {@link AuditLog}. */
234export const listAuditLogs = AuditLog;
235
236/**
237 * Fetch one audit log by id. Workspace defaults to the current token
238 * workspace.
239 *
240 * @example Get by id
241 * ```typescript
242 * const log = yield* Railway.getAuditLog({ id: logs[0].id });
243 * ```
244 */
245export const getAuditLog = Effect.fn(function* (options: {
246 id: string;
247 workspaceId?: string;
248}) {
249 const workspaceId = yield* workspaceOf(options.workspaceId);
250 const row = yield* readAuditLog(options.id, workspaceId);
251 return toEntry(row);
252});
253
254/**
255 * Event types Railway can record, with descriptions.
256 *
257 * @example Event type catalog
258 * ```typescript
259 * const types = yield* Railway.listAuditLogEventTypes();
260 * ```
261 */
262export const listAuditLogEventTypes = Effect.fn(function* () {
263 const types = yield* readAuditLogEventTypes();
264 return types ?? [];
265});