| 1 | import { Query, type UnwrapPlan } from "@distilled.cloud/core/query"; |
| 2 | import { |
| 3 | Railway, |
| 4 | type AuditLog as RailwayAuditLog, |
| 5 | type AuditLogFilterInput, |
| 6 | } from "@distilled.cloud/railway"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import { resolveWorkspaceId } from "./Environment.ts"; |
| 9 | |
| 10 | const 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 | }); |
| 20 | type AuditLogRow = UnwrapPlan<ReturnType<typeof auditLogFields>>; |
| 21 | |
| 22 | const 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 | |
| 31 | const readAuditLog = Query.fn((id: string, workspaceId: string) => |
| 32 | auditLogFields(Railway.auditLog({ id, workspaceId })), |
| 33 | ); |
| 34 | |
| 35 | const readAuditLogEventTypes = Query.fn(() => |
| 36 | Railway.auditLogEventTypeInfo().pipe( |
| 37 | Query.map((info) => ({ |
| 38 | description: info.description, |
| 39 | eventType: info.eventType, |
| 40 | })), |
| 41 | ), |
| 42 | ); |
| 43 | type 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 | */ |
| 51 | export 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 | */ |
| 60 | export type AuditLogEnvironment = { |
| 61 | readonly environmentId: string; |
| 62 | }; |
| 63 | |
| 64 | export 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 | */ |
| 105 | export 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 | |
| 124 | export type AuditLogEventType = AuditLogEventTypeInfoResultItem; |
| 125 | |
| 126 | const 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 | |
| 137 | const 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 | |
| 145 | const 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 | |
| 153 | const 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 | */ |
| 197 | export 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}. */ |
| 234 | export 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 | */ |
| 245 | export 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 | */ |
| 262 | export const listAuditLogEventTypes = Effect.fn(function* () { |
| 263 | const types = yield* readAuditLogEventTypes(); |
| 264 | return types ?? []; |
| 265 | }); |