Unified · Split
| @@ -6,8 +6,8 @@ import type { OrchestrationEvent } from "@t3tools/contracts"; · 5 unmodified lines | |||
| 6 | import { Effect } from "effect"; | 6 | import { Effect } from "effect"; |
| 7 | import { afterEach, describe, expect, it } from "vitest"; | 7 | import { afterEach, describe, expect, it } from "vitest"; |
| 8 | 8 | ||
| 9 | import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents"; | ||
| 9 | import { OrchestrationEngine } from "./engine"; | 10 | import { OrchestrationEngine } from "./engine"; |
| 10 | import type { OrchestrationEventRepositoryShape } from "./eventRepository"; | ||
| 11 | 11 | ||
| 12 | const tempDirs: string[] = []; | 12 | const tempDirs: string[] = []; |
| 13 | 13 | ||
| @@ -20,8 +20,8 @@ import { · 19 unmodified lines | |||
| 20 | } from "effect"; | 20 | } from "effect"; |
| 21 | 21 | ||
| 22 | import { createLogger } from "../logger"; | 22 | import { createLogger } from "../logger"; |
| 23 | import type { OrchestrationEventRepositoryShape } from "./eventRepository"; | 23 | import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents"; |
| 24 | import { makeSqliteOrchestrationEventRepository } from "./eventStore"; | 24 | import { makeSqliteOrchestrationEventRepository } from "../persistence/Layers/OrchestrationEvents"; |
| 25 | import { createEmptyReadModel, reduceEvent } from "./reducer"; | 25 | import { createEmptyReadModel, reduceEvent } from "./reducer"; |
| 26 | import { UI_ENTITY_CONTRACTS } from "./uiContractInventory"; | 26 | import { UI_ENTITY_CONTRACTS } from "./uiContractInventory"; |
| 27 | 27 | ||
| @@ -2,9 +2,9 @@ import path from "node:path"; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | import { Effect, Layer } from "effect"; | 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 | import { OrchestrationEngine } from "./engine"; | 7 | import { OrchestrationEngine } from "./engine"; |
| 7 | import { makeSqliteOrchestrationEventRepository } from "./eventStore"; | ||
| 8 | import { OrchestrationConfig, OrchestrationEngineService } from "./services"; | 8 | import { OrchestrationConfig, OrchestrationEngineService } from "./services"; |
| 9 | 9 | ||
| 10 | export const OrchestrationEventRepositoryLive = Layer.effect( | 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 | import { Runtime } from "effect"; | 6 | import { Runtime } from "effect"; |
| 7 | import { afterEach, describe, expect, it } from "vitest"; | 7 | import { afterEach, describe, expect, it } from "vitest"; |
| 8 | 8 | ||
| 9 | import { makeSqliteOrchestrationEventRepository } from "./eventStore"; | 9 | import { makeSqliteOrchestrationEventRepository } from "./OrchestrationEvents"; |
| 10 | 10 | ||
| 11 | const tempDirs: string[] = []; | 11 | const tempDirs: string[] = []; |
| 12 | 12 | ||
| @@ -69,17 +69,21 @@ describe("SqliteEventStore", () => { · 56 unmodified lines | |||
| 69 | ` | 69 | ` |
| 70 | SELECT name | 70 | SELECT name |
| 71 | FROM sqlite_master | 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 | .get() as { name: string } | undefined; | 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 | const migrationRows = db | 82 | const migrationRows = db |
| 79 | .prepare( | 83 | .prepare( |
| 80 | ` | 84 | ` |
| 81 | SELECT * | 85 | SELECT * |
| 82 | FROM orchestration_sql_migrations | 86 | FROM ${tableRow.name} |
| 83 | `, | 87 | `, |
| 84 | ) | 88 | ) |
| 85 | .all() as Array<Record<string, unknown>>; | 89 | .all() as Array<Record<string, unknown>>; |
| @@ -9,8 +9,8 @@ import * as SqlSchema from "@effect/sql/SqlSchema"; · 8 unmodified lines | |||
| 9 | import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient"; | 9 | import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient"; |
| 10 | import { Effect, ManagedRuntime, Schema } from "effect"; | 10 | import { Effect, ManagedRuntime, Schema } from "effect"; |
| 11 | 11 | ||
| 12 | import type { OrchestrationEventRepositoryShape } from "./eventRepository"; | 12 | import type { OrchestrationEventRepositoryShape } from "../Services/OrchestrationEvents"; |
| 13 | import { runOrchestrationMigrations } from "./migrations"; | 13 | import { runMigrations } from "../Migrations"; |
| 14 | 14 | ||
| 15 | const decodeEvent = Schema.decodeUnknownSync(OrchestrationEventSchema); | 15 | const decodeEvent = Schema.decodeUnknownSync(OrchestrationEventSchema); |
| 16 | 16 | ||
| @@ -135,7 +135,7 @@ export function makeSqliteOrchestrationEventRepository( · 118 unmodified lines | |||
| 135 | const sql = yield* SqlClient.SqlClient; | 135 | const sql = yield* SqlClient.SqlClient; |
| 136 | yield* sql`PRAGMA journal_mode = WAL;`; | 136 | yield* sql`PRAGMA journal_mode = WAL;`; |
| 137 | yield* sql`PRAGMA foreign_keys = ON;`; | 137 | yield* sql`PRAGMA foreign_keys = ON;`; |
| 138 | yield* runOrchestrationMigrations; | 138 | yield* runMigrations; |
| 139 | }); | 139 | }); |
| 140 | 140 | ||
| 141 | const initialized = runtime.runPromise(initialize).then(() => undefined); | 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 | "compilerOptions": { | 3 | "compilerOptions": { |
| 4 | "composite": true, | 4 | "composite": true, |
| 5 | "types": ["node"], | 5 | "types": ["node"], |
| 6 | "lib": ["ES2023", "esnext.disposable"] | 6 | "lib": ["ES2023", "esnext.disposable"], |
| 7 | "noEmit": true, | ||
| 8 | "allowImportingTsExtensions": true | ||
| 7 | }, | 9 | }, |
| 8 | "include": ["src", "tsdown.config.ts"] | 10 | "include": ["src", "tsdown.config.ts"] |
| 9 | } | 11 | } |