Unified · Split
| @@ -6,8 +6,8 @@ import type { OrchestrationEvent } from "@t3tools/contracts"; · 5 unmodified lines | ||
| 6 | 6 | import { Effect } from "effect"; |
| 7 | 7 | import { afterEach, describe, expect, it } from "vitest"; |
| 8 | 8 | |
| 9 | +import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents"; | |
| 9 | 10 | import { OrchestrationEngine } from "./engine"; |
| 10 | −import type { OrchestrationEventRepositoryShape } from "./eventRepository"; | |
| 11 | 11 | |
| 12 | 12 | const tempDirs: string[] = []; |
| 13 | 13 | |
| @@ -20,8 +20,8 @@ import { · 19 unmodified lines | ||
| 20 | 20 | } from "effect"; |
| 21 | 21 | |
| 22 | 22 | import { createLogger } from "../logger"; |
| 23 | −import type { OrchestrationEventRepositoryShape } from "./eventRepository"; | |
| 24 | −import { makeSqliteOrchestrationEventRepository } from "./eventStore"; | |
| 23 | +import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents"; | |
| 24 | +import { makeSqliteOrchestrationEventRepository } from "../persistence/Layers/OrchestrationEvents"; | |
| 25 | 25 | import { createEmptyReadModel, reduceEvent } from "./reducer"; |
| 26 | 26 | import { UI_ENTITY_CONTRACTS } from "./uiContractInventory"; |
| 27 | 27 | |
| @@ -2,9 +2,9 @@ import path from "node:path"; · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | import { Effect, Layer } from "effect"; |
| 4 | 4 | |
| 5 | −import { OrchestrationEventRepository } from "./eventRepository"; | |
| 5 | +import { OrchestrationEventRepository } from "../persistence/Services/OrchestrationEvents"; | |
| 6 | +import { makeSqliteOrchestrationEventRepository } from "../persistence/Layers/OrchestrationEvents"; | |
| 6 | 7 | import { OrchestrationEngine } from "./engine"; |
| 7 | −import { makeSqliteOrchestrationEventRepository } from "./eventStore"; | |
| 8 | 8 | import { OrchestrationConfig, OrchestrationEngineService } from "./services"; |
| 9 | 9 | |
| 10 | 10 | export const OrchestrationEventRepositoryLive = Layer.effect( |
| @@ -1,31 +0,0 @@ | ||
| 1 | −import * as Migrator from "@effect/sql/Migrator"; | |
| 2 | −import * as SqlClient from "@effect/sql/SqlClient"; | |
| 3 | −import { Effect } from "effect"; | |
| 4 | − | |
| 5 | −const ORCHESTRATION_MIGRATIONS = Migrator.fromRecord({ | |
| 6 | − "0001_create_orchestration_events": Effect.flatMap(SqlClient.SqlClient, (sql) => | |
| 7 | − Effect.gen(function* () { | |
| 8 | − yield* sql` | |
| 9 | − CREATE TABLE IF NOT EXISTS orchestration_events ( | |
| 10 | − sequence INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 11 | − event_id TEXT NOT NULL UNIQUE, | |
| 12 | − event_type TEXT NOT NULL, | |
| 13 | − aggregate_type TEXT NOT NULL, | |
| 14 | − aggregate_id TEXT NOT NULL, | |
| 15 | − occurred_at TEXT NOT NULL, | |
| 16 | − command_id TEXT, | |
| 17 | − payload_json TEXT NOT NULL | |
| 18 | − ) | |
| 19 | − `; | |
| 20 | − yield* sql` | |
| 21 | − CREATE INDEX IF NOT EXISTS idx_orch_events_aggregate | |
| 22 | − ON orchestration_events(aggregate_type, aggregate_id, sequence) | |
| 23 | − `; | |
| 24 | − }), | |
| 25 | − ), | |
| 26 | −}); | |
| 27 | − | |
| 28 | −export const runOrchestrationMigrations = Migrator.make({})({ | |
| 29 | − loader: ORCHESTRATION_MIGRATIONS, | |
| 30 | − table: "orchestration_sql_migrations", | |
| 31 | −}); | |
| @@ -6,7 +6,7 @@ import { DatabaseSync } from "node:sqlite"; · 5 unmodified lines | ||
| 6 | 6 | import { Runtime } from "effect"; |
| 7 | 7 | import { afterEach, describe, expect, it } from "vitest"; |
| 8 | 8 | |
| 9 | −import { makeSqliteOrchestrationEventRepository } from "./eventStore"; | |
| 9 | +import { makeSqliteOrchestrationEventRepository } from "./OrchestrationEvents"; | |
| 10 | 10 | |
| 11 | 11 | const tempDirs: string[] = []; |
| 12 | 12 | |
| @@ -69,17 +69,21 @@ describe("SqliteEventStore", () => { · 56 unmodified lines | ||
| 69 | 69 | ` |
| 70 | 70 | SELECT name |
| 71 | 71 | FROM sqlite_master |
| 72 | − WHERE type = 'table' AND name = 'orchestration_sql_migrations' | |
| 72 | + WHERE type = 'table' AND name IN ('effect_sql_migrations', 'orchestration_sql_migrations') | |
| 73 | + ORDER BY name ASC | |
| 73 | 74 | `, |
| 74 | 75 | ) |
| 75 | 76 | .get() as { name: string } | undefined; |
| 76 | − expect(tableRow?.name).toBe("orchestration_sql_migrations"); | |
| 77 | + expect(tableRow?.name).toBeDefined(); | |
| 78 | + if (!tableRow) { | |
| 79 | + throw new Error("Expected migrations table to exist"); | |
| 80 | + } | |
| 77 | 81 | |
| 78 | 82 | const migrationRows = db |
| 79 | 83 | .prepare( |
| 80 | 84 | ` |
| 81 | 85 | SELECT * |
| 82 | − FROM orchestration_sql_migrations | |
| 86 | + FROM ${tableRow.name} | |
| 83 | 87 | `, |
| 84 | 88 | ) |
| 85 | 89 | .all() as Array<Record<string, unknown>>; |
| @@ -9,8 +9,8 @@ import * as SqlSchema from "@effect/sql/SqlSchema"; · 8 unmodified lines | ||
| 9 | 9 | import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient"; |
| 10 | 10 | import { Effect, ManagedRuntime, Schema } from "effect"; |
| 11 | 11 | |
| 12 | −import type { OrchestrationEventRepositoryShape } from "./eventRepository"; | |
| 13 | −import { runOrchestrationMigrations } from "./migrations"; | |
| 12 | +import type { OrchestrationEventRepositoryShape } from "../Services/OrchestrationEvents"; | |
| 13 | +import { runMigrations } from "../Migrations"; | |
| 14 | 14 | |
| 15 | 15 | const decodeEvent = Schema.decodeUnknownSync(OrchestrationEventSchema); |
| 16 | 16 | |
| @@ -135,7 +135,7 @@ export function makeSqliteOrchestrationEventRepository( · 118 unmodified lines | ||
| 135 | 135 | const sql = yield* SqlClient.SqlClient; |
| 136 | 136 | yield* sql`PRAGMA journal_mode = WAL;`; |
| 137 | 137 | yield* sql`PRAGMA foreign_keys = ON;`; |
| 138 | − yield* runOrchestrationMigrations; | |
| 138 | + yield* runMigrations; | |
| 139 | 139 | }); |
| 140 | 140 | |
| 141 | 141 | const initialized = runtime.runPromise(initialize).then(() => undefined); |
| @@ -0,0 +1,67 @@ | ||
| 1 | +/** | |
| 2 | + * MigrationsLive - Migration runner with inline loader | |
| 3 | + * | |
| 4 | + * Uses Migrator.make with fromRecord to define migrations inline. | |
| 5 | + * All migrations are statically imported - no dynamic file system loading. | |
| 6 | + * | |
| 7 | + * Migrations run automatically when the MigrationLayer is provided, | |
| 8 | + * ensuring the database schema is always up-to-date before the application starts. | |
| 9 | + */ | |
| 10 | + | |
| 11 | +import * as Migrator from "@effect/sql/Migrator"; | |
| 12 | +import * as Layer from "effect/Layer"; | |
| 13 | + | |
| 14 | +// Import all migrations statically | |
| 15 | +import Migration0001 from "./Migrations/001_OrchestrationEvents.ts"; | |
| 16 | + | |
| 17 | + | |
| 18 | +/** | |
| 19 | + * Migration loader with all migrations defined inline. | |
| 20 | + * | |
| 21 | + * Key format: "{id}_{name}" where: | |
| 22 | + * - id: numeric migration ID (determines execution order) | |
| 23 | + * - name: descriptive name for the migration | |
| 24 | + * | |
| 25 | + * Uses Migrator.fromRecord which parses the key format and | |
| 26 | + * returns migrations sorted by ID. | |
| 27 | + */ | |
| 28 | +const loader = Migrator.fromRecord({ | |
| 29 | + "1_OrchestrationEvents": Migration0001, | |
| 30 | +}); | |
| 31 | + | |
| 32 | +/** | |
| 33 | + * Migrator run function - no schema dumping needed | |
| 34 | + * Uses the base Migrator.make without platform dependencies | |
| 35 | + */ | |
| 36 | +const run = Migrator.make({}); | |
| 37 | + | |
| 38 | +/** | |
| 39 | + * Run all pending migrations. | |
| 40 | + * | |
| 41 | + * Creates the migrations tracking table (effect_sql_migrations) if it doesn't exist, | |
| 42 | + * then runs any migrations with ID greater than the latest recorded migration. | |
| 43 | + * | |
| 44 | + * Returns array of [id, name] tuples for migrations that were run. | |
| 45 | + * | |
| 46 | + * @returns Effect containing array of executed migrations | |
| 47 | + */ | |
| 48 | +export const runMigrations = run({ loader }); | |
| 49 | + | |
| 50 | +/** | |
| 51 | + * Layer that runs migrations when the layer is built. | |
| 52 | + * | |
| 53 | + * Use this to ensure migrations run before your application starts. | |
| 54 | + * Migrations are run automatically - no separate script is needed. | |
| 55 | + * | |
| 56 | + * @example | |
| 57 | + * ```typescript | |
| 58 | + * import { MigrationsLive } from "@acme/db/Migrations" | |
| 59 | + * import { PgClient } from "@effect/sql-pg" | |
| 60 | + * | |
| 61 | + * // Migrations run automatically when PgClient is provided | |
| 62 | + * const AppLayer = MigrationsLive.pipe( | |
| 63 | + * Layer.provideMerge(PgClient.layer({ url: Redacted.make("postgresql://...") })) | |
| 64 | + * ) | |
| 65 | + * ``` | |
| 66 | + */ | |
| 67 | +export const MigrationsLive = Layer.effectDiscard(runMigrations); | |
| @@ -0,0 +1,24 @@ | ||
| 1 | +import { SqlClient } from "@effect/sql"; | |
| 2 | +import * as Effect from "effect/Effect"; | |
| 3 | + | |
| 4 | +export default Effect.gen(function* () { | |
| 5 | + const sql = yield* SqlClient.SqlClient; | |
| 6 | + | |
| 7 | + yield* sql` | |
| 8 | + CREATE TABLE IF NOT EXISTS orchestration_events ( | |
| 9 | + sequence INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 10 | + event_id TEXT NOT NULL UNIQUE, | |
| 11 | + event_type TEXT NOT NULL, | |
| 12 | + aggregate_type TEXT NOT NULL, | |
| 13 | + aggregate_id TEXT NOT NULL, | |
| 14 | + occurred_at TEXT NOT NULL, | |
| 15 | + command_id TEXT, | |
| 16 | + payload_json TEXT NOT NULL | |
| 17 | + ) | |
| 18 | + `; | |
| 19 | + | |
| 20 | + yield* sql` | |
| 21 | + CREATE INDEX IF NOT EXISTS idx_orch_events_aggregate | |
| 22 | + ON orchestration_events(aggregate_type, aggregate_id, sequence) | |
| 23 | + `; | |
| 24 | +}); | |
File renamed without changes.
| @@ -3,7 +3,9 @@ · 2 unmodified lines | ||
| 3 | 3 | "compilerOptions": { |
| 4 | 4 | "composite": true, |
| 5 | 5 | "types": ["node"], |
| 6 | − "lib": ["ES2023", "esnext.disposable"] | |
| 6 | + "lib": ["ES2023", "esnext.disposable"], | |
| 7 | + "noEmit": true, | |
| 8 | + "allowImportingTsExtensions": true | |
| 7 | 9 | }, |
| 8 | 10 | "include": ["src", "tsdown.config.ts"] |
| 9 | 11 | } |