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
6import { Effect } from "effect";6import { Effect } from "effect";
7import { afterEach, describe, expect, it } from "vitest";7import { afterEach, describe, expect, it } from "vitest";
8 8
9import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents";
9import { OrchestrationEngine } from "./engine";10import { OrchestrationEngine } from "./engine";
10import type { OrchestrationEventRepositoryShape } from "./eventRepository";
11 11
12const tempDirs: string[] = [];12const tempDirs: string[] = [];
13 13
apps/server/src/orchestration/engine.ts+2−2
@@ -20,8 +20,8 @@ import { · 19 unmodified lines
20} from "effect";20} from "effect";
21 21
22import { createLogger } from "../logger";22import { createLogger } from "../logger";
23import type { OrchestrationEventRepositoryShape } from "./eventRepository";23import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents";
24import { makeSqliteOrchestrationEventRepository } from "./eventStore";24import { makeSqliteOrchestrationEventRepository } from "../persistence/Layers/OrchestrationEvents";
25import { createEmptyReadModel, reduceEvent } from "./reducer";25import { createEmptyReadModel, reduceEvent } from "./reducer";
26import { UI_ENTITY_CONTRACTS } from "./uiContractInventory";26import { UI_ENTITY_CONTRACTS } from "./uiContractInventory";
27 27
apps/server/src/orchestration/layers.ts+2−2
@@ -2,9 +2,9 @@ import path from "node:path"; · 1 unmodified line
2 2
3import { Effect, Layer } from "effect";3import { Effect, Layer } from "effect";
4 4
5import { OrchestrationEventRepository } from "./eventRepository";5import { OrchestrationEventRepository } from "../persistence/Services/OrchestrationEvents";
6import { makeSqliteOrchestrationEventRepository } from "../persistence/Layers/OrchestrationEvents";
6import { OrchestrationEngine } from "./engine";7import { OrchestrationEngine } from "./engine";
7import { makeSqliteOrchestrationEventRepository } from "./eventStore";
8import { OrchestrationConfig, OrchestrationEngineService } from "./services";8import { OrchestrationConfig, OrchestrationEngineService } from "./services";
9 9
10export const OrchestrationEventRepositoryLive = Layer.effect(10export const OrchestrationEventRepositoryLive = Layer.effect(
apps/server/src/orchestration/migrations.ts+0−31
@@ -1,31 +0,0 @@
1import * as Migrator from "@effect/sql/Migrator";
2import * as SqlClient from "@effect/sql/SqlClient";
3import { Effect } from "effect";
4
5const 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
28export 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
6import { Runtime } from "effect";6import { Runtime } from "effect";
7import { afterEach, describe, expect, it } from "vitest";7import { afterEach, describe, expect, it } from "vitest";
8 8
9import { makeSqliteOrchestrationEventRepository } from "./eventStore";9import { makeSqliteOrchestrationEventRepository } from "./OrchestrationEvents";
10 10
11const tempDirs: string[] = [];11const tempDirs: string[] = [];
12 12
@@ -69,17 +69,21 @@ describe("SqliteEventStore", () => { · 56 unmodified lines
69 `69 `
70 SELECT name70 SELECT name
71 FROM sqlite_master71 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 = db82 const migrationRows = db
79 .prepare(83 .prepare(
80 `84 `
81 SELECT *85 SELECT *
82 FROM orchestration_sql_migrations86 FROM ${tableRow.name}
83 `,87 `,
84 )88 )
85 .all() as Array<Record<string, unknown>>;89 .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
9import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient";9import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient";
10import { Effect, ManagedRuntime, Schema } from "effect";10import { Effect, ManagedRuntime, Schema } from "effect";
11 11
12import type { OrchestrationEventRepositoryShape } from "./eventRepository";12import type { OrchestrationEventRepositoryShape } from "../Services/OrchestrationEvents";
13import { runOrchestrationMigrations } from "./migrations";13import { runMigrations } from "../Migrations";
14 14
15const decodeEvent = Schema.decodeUnknownSync(OrchestrationEventSchema);15const 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);
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
11import * as Migrator from "@effect/sql/Migrator";
12import * as Layer from "effect/Layer";
13
14// Import all migrations statically
15import 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 */
28const 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 */
36const 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 */
48export 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 */
67export const MigrationsLive = Layer.effectDiscard(runMigrations);
apps/server/src/persistence/Migrations/001_OrchestrationEvents.ts+24−0
@@ -0,0 +1,24 @@
1import { SqlClient } from "@effect/sql";
2import * as Effect from "effect/Effect";
3
4export 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
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}