History

more ffect: Service/Layer separation

Julius Marminge committed 8 months ago

Commit bbf4e6b1 parent fa1663c10 files · +110 −44Browse files

Unified · Split

10 files
  1. apps/server/src/orchestration/engine.test.ts +1 −1
  2. apps/server/src/orchestration/engine.ts +2 −2
  3. apps/server/src/orchestration/layers.ts +2 −2
  4. apps/server/src/orchestration/migrations.ts +0 −31
  5. apps/server/src/persistence/Layers/OrchestrationEvents.test.ts +8 −4
  6. apps/server/src/persistence/Layers/OrchestrationEvents.ts +3 −3
  7. apps/server/src/persistence/Migrations.ts +67 −0
  8. apps/server/src/persistence/Migrations/001_OrchestrationEvents.ts +24 −0
  9. apps/server/src/persistence/Services/OrchestrationEvents.ts +0 −0
  10. apps/server/tsconfig.json +3 −1
apps/server/src/orchestration/engine.test.ts+1−1
@@ -6,8 +6,8 @@ import type { OrchestrationEvent } from "@t3tools/contracts"; · 5 unmodified lines
66 import { Effect } from "effect";
77 import { afterEach, describe, expect, it } from "vitest";
88
9+import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents";
910 import { OrchestrationEngine } from "./engine";
10−import type { OrchestrationEventRepositoryShape } from "./eventRepository";
1111
1212 const tempDirs: string[] = [];
1313
apps/server/src/orchestration/engine.ts+2−2
@@ -20,8 +20,8 @@ import { · 19 unmodified lines
2020 } from "effect";
2121
2222 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";
2525 import { createEmptyReadModel, reduceEvent } from "./reducer";
2626 import { UI_ENTITY_CONTRACTS } from "./uiContractInventory";
2727
apps/server/src/orchestration/layers.ts+2−2
@@ -2,9 +2,9 @@ import path from "node:path"; · 1 unmodified line
22
33 import { Effect, Layer } from "effect";
44
5−import { OrchestrationEventRepository } from "./eventRepository";
5+import { OrchestrationEventRepository } from "../persistence/Services/OrchestrationEvents";
6+import { makeSqliteOrchestrationEventRepository } from "../persistence/Layers/OrchestrationEvents";
67 import { OrchestrationEngine } from "./engine";
7−import { makeSqliteOrchestrationEventRepository } from "./eventStore";
88 import { OrchestrationConfig, OrchestrationEngineService } from "./services";
99
1010 export const OrchestrationEventRepositoryLive = Layer.effect(
apps/server/src/orchestration/migrations.ts+0−31
@@ -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−});
apps/server/src/persistence/Layers/OrchestrationEvents.test.tsrenamed from apps/server/src/orchestration/eventStore.test.ts+8−4
@@ -6,7 +6,7 @@ import { DatabaseSync } from "node:sqlite"; · 5 unmodified lines
66 import { Runtime } from "effect";
77 import { afterEach, describe, expect, it } from "vitest";
88
9−import { makeSqliteOrchestrationEventRepository } from "./eventStore";
9+import { makeSqliteOrchestrationEventRepository } from "./OrchestrationEvents";
1010
1111 const tempDirs: string[] = [];
1212
@@ -69,17 +69,21 @@ describe("SqliteEventStore", () => { · 56 unmodified lines
6969 `
7070 SELECT name
7171 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
7374 `,
7475 )
7576 .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+ }
7781
7882 const migrationRows = db
7983 .prepare(
8084 `
8185 SELECT *
82− FROM orchestration_sql_migrations
86+ FROM ${tableRow.name}
8387 `,
8488 )
8589 .all() as Array<Record<string, unknown>>;
apps/server/src/persistence/Layers/OrchestrationEvents.tsrenamed from apps/server/src/orchestration/eventStore.ts+3−3
@@ -9,8 +9,8 @@ import * as SqlSchema from "@effect/sql/SqlSchema"; · 8 unmodified lines
99 import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient";
1010 import { Effect, ManagedRuntime, Schema } from "effect";
1111
12−import type { OrchestrationEventRepositoryShape } from "./eventRepository";
13−import { runOrchestrationMigrations } from "./migrations";
12+import type { OrchestrationEventRepositoryShape } from "../Services/OrchestrationEvents";
13+import { runMigrations } from "../Migrations";
1414
1515 const decodeEvent = Schema.decodeUnknownSync(OrchestrationEventSchema);
1616
@@ -135,7 +135,7 @@ export function makeSqliteOrchestrationEventRepository( · 118 unmodified lines
135135 const sql = yield* SqlClient.SqlClient;
136136 yield* sql`PRAGMA journal_mode = WAL;`;
137137 yield* sql`PRAGMA foreign_keys = ON;`;
138− yield* runOrchestrationMigrations;
138+ yield* runMigrations;
139139 });
140140
141141 const initialized = runtime.runPromise(initialize).then(() => undefined);
apps/server/src/persistence/Migrations.ts+67−0
@@ -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);
apps/server/src/persistence/Migrations/001_OrchestrationEvents.ts+24−0
@@ -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+});
apps/server/src/persistence/Services/OrchestrationEvents.tsrenamed from apps/server/src/orchestration/eventRepository.ts+0−0

File renamed without changes.

apps/server/tsconfig.json+3−1
@@ -3,7 +3,9 @@ · 2 unmodified lines
33 "compilerOptions": {
44 "composite": true,
55 "types": ["node"],
6− "lib": ["ES2023", "esnext.disposable"]
6+ "lib": ["ES2023", "esnext.disposable"],
7+ "noEmit": true,
8+ "allowImportingTsExtensions": true
79 },
810 "include": ["src", "tsdown.config.ts"]
911 }