@remit/search-index-worker 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/search-index-worker",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -21,7 +21,6 @@
21
21
  "dependencies": {
22
22
  "@remit/outbox-relay": "*",
23
23
  "p-map": "*",
24
- "pg": "^8.0.0",
25
24
  "@remit/logger-lambda": "*",
26
25
  "@remit/sqs-client": "*",
27
26
  "@remit/data-ports": "*",
@@ -30,10 +29,8 @@
30
29
  "@remit/storage-service": "*",
31
30
  "@aws-sdk/client-sqs": "*",
32
31
  "better-sqlite3": "^12.11.1",
33
- "drizzle-orm": "^0.45.2",
34
32
  "expect-env": "*",
35
33
  "@types/aws-lambda": "*",
36
- "@types/pg": "^8.0.0",
37
34
  "prom-client": "^15.1.3"
38
35
  },
39
36
  "devDependencies": {
@@ -1,5 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
- import { afterEach, test } from "node:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
3
6
  import {
4
7
  buildDataPortsFromEnv,
5
8
  type SearchIndexDataPorts,
@@ -26,55 +29,41 @@ const withEnv = async (
26
29
  }
27
30
  };
28
31
 
29
- afterEach(() => {
30
- delete process.env.DATA_BACKEND;
31
- });
32
-
33
- test("without DATA_BACKEND and no registered ports, throws (DynamoDB path is injected)", async () => {
34
- await withEnv({ DATA_BACKEND: undefined }, async () => {
32
+ test("with neither a registration nor a database, the error names setSearchIndexDataPorts", async () => {
33
+ await withEnv({ SQLITE_DB_PATH: undefined }, async () => {
35
34
  await assert.rejects(
36
35
  () => buildDataPortsFromEnv(),
37
- /no DynamoDB search-index data ports registered/,
36
+ /no search-index data ports registered/,
38
37
  );
39
38
  });
40
39
  });
41
40
 
42
- test("without DATA_BACKEND returns the injected DynamoDB ports", async () => {
41
+ test("SQLITE_DB_PATH builds Drizzle ports with a resolveAccountId hook", async () => {
42
+ const dir = mkdtempSync(join(tmpdir(), "remit-search-ports-"));
43
+ await withEnv({ SQLITE_DB_PATH: join(dir, "remit.db") }, async () => {
44
+ const ports = await buildDataPortsFromEnv();
45
+ assert.ok(ports.account, "account port must be defined");
46
+ assert.ok(ports.threadMessage, "threadMessage port must be defined");
47
+ assert.equal(
48
+ typeof ports.resolveAccountId,
49
+ "function",
50
+ "the outbox relay carries no accountId, so the consumer must derive it",
51
+ );
52
+ });
53
+ rmSync(dir, { recursive: true, force: true });
54
+ });
55
+
56
+ // Last: the registration is process-wide and takes precedence over everything
57
+ // below it, so a test registering ports cannot run before the ones that must
58
+ // reach the fallback.
59
+ test("a registered set of ports is returned without reading the environment", async () => {
43
60
  const injected = {
44
61
  account: {},
45
62
  threadMessage: {},
46
63
  } as unknown as SearchIndexDataPorts;
47
64
  setSearchIndexDataPorts(injected);
48
- await withEnv({ DATA_BACKEND: undefined }, async () => {
65
+ await withEnv({ SQLITE_DB_PATH: undefined }, async () => {
49
66
  const ports = await buildDataPortsFromEnv();
50
67
  assert.equal(ports, injected);
51
68
  });
52
69
  });
53
-
54
- test("DATA_BACKEND=postgres builds Drizzle ports with a resolveAccountId hook, without connecting", async () => {
55
- await withEnv(
56
- {
57
- DATA_BACKEND: "postgres",
58
- PG_CONNECTION_URL: "postgresql://remit:remit@localhost:5432/remit_test",
59
- },
60
- async () => {
61
- const ports = await buildDataPortsFromEnv();
62
- assert.ok(ports.account, "account port must be defined");
63
- assert.ok(ports.threadMessage, "threadMessage port must be defined");
64
- assert.equal(
65
- typeof ports.resolveAccountId,
66
- "function",
67
- "the pg outbox relay carries no accountId, so the consumer must derive it",
68
- );
69
- },
70
- );
71
- });
72
-
73
- test("DATA_BACKEND=postgres without PG_CONNECTION_URL throws", async () => {
74
- await withEnv(
75
- { DATA_BACKEND: "postgres", PG_CONNECTION_URL: undefined },
76
- async () => {
77
- await assert.rejects(() => buildDataPortsFromEnv());
78
- },
79
- );
80
- });
package/src/data-ports.ts CHANGED
@@ -2,10 +2,6 @@ import type {
2
2
  IAccountRepository,
3
3
  IThreadMessageRepository,
4
4
  } from "@remit/data-ports";
5
- // Type-only: erased at build, so it carries no runtime dependency on
6
- // drizzle-orm. The value import lives inside `buildPostgresDataPorts` as a
7
- // dynamic `import()` — see the comment there for why.
8
- import type { NodePgDatabase } from "drizzle-orm/node-postgres";
9
5
 
10
6
  export interface SearchIndexDataPorts {
11
7
  account: IAccountRepository;
@@ -25,54 +21,13 @@ export interface SearchIndexDataPorts {
25
21
  resolveAccountId?: (messageId: string) => Promise<string | null>;
26
22
  }
27
23
 
28
- // `@remit/drizzle-service` and `drizzle-orm/node-postgres` are loaded
29
- // lazily, inside this function, instead of as static top-level imports. This
30
- // branch only ever runs when `DATA_BACKEND === "postgres"` true for the
31
- // local Postgres-parity dev stack, never on the deployed Lambda but a static
32
- // import is bundled (and evaluated at module load) regardless of whether the
33
- // branch that uses it ever runs. `@remit/drizzle-service` and
34
- // `drizzle-orm` are marked `external` for the Lambda esbuild build (see
35
- // LAMBDA_ESBUILD_OPTIONS), so esbuild leaves this `import()` unresolved in the
36
- // bundle; it is only ever reached — and only ever needs to resolve — on the
37
- // Postgres path, which runs via `tsx` (no bundling, real module resolution)
38
- // and always has both packages installed. Mirrors
39
- // `packages/backend/src/service/dynamodb.ts`'s `buildPostgresClient`.
40
- const buildPostgresDataPorts = async (): Promise<SearchIndexDataPorts> => {
41
- const pgConnectionUrl = process.env.PG_CONNECTION_URL;
42
- if (!pgConnectionUrl) throw new Error("PG_CONNECTION_URL is required");
43
-
44
- const {
45
- AccountRepo,
46
- DrizzleMessageRepository,
47
- DrizzleThreadMessageRepository,
48
- MailboxRepo,
49
- messageDataSchema,
50
- } = await import("@remit/drizzle-service");
51
- const { drizzle } = await import("drizzle-orm/node-postgres");
52
-
53
- const db = drizzle(pgConnectionUrl, { schema: messageDataSchema });
54
- const genericDb = db as unknown as NodePgDatabase<Record<string, unknown>>;
55
- const messageDataDb = db as unknown as NodePgDatabase<
56
- typeof messageDataSchema
57
- >;
58
-
59
- const message = new DrizzleMessageRepository(messageDataDb);
60
- const mailbox = new MailboxRepo(genericDb);
61
-
62
- return {
63
- account: new AccountRepo(genericDb),
64
- threadMessage: new DrizzleThreadMessageRepository(pgConnectionUrl),
65
- resolveAccountId: async (messageId) => {
66
- const row = await message.get(messageId);
67
- return mailbox.resolveAccountId(row.mailboxId);
68
- },
69
- };
70
- };
71
-
72
- // The SQLite twin of `buildPostgresDataPorts` (RFC 036): the same Drizzle repos
73
- // over the one shared SQLite file instead of a Postgres connection string.
74
- // `createSqliteDatabase` (and better-sqlite3 behind it) is kept external from
75
- // this worker's DynamoDB Lambda bundle by the same dynamic-import treatment.
24
+ // `@remit/drizzle-service` is loaded lazily, inside this function, instead of as
25
+ // a static top-level import. A static import is bundled (and evaluated at module
26
+ // load) regardless of whether the branch that uses it ever runs; the package is
27
+ // marked `external` for the Lambda esbuild build (see LAMBDA_ESBUILD_OPTIONS),
28
+ // so esbuild leaves this `import()` unresolved in the bundle. It resolves only
29
+ // where the relational composition actually runs, which has the package
30
+ // installed. Mirrors `packages/backend/src/service/data-client.ts`.
76
31
  const buildSqliteDataPorts = async (): Promise<SearchIndexDataPorts> => {
77
32
  const sqliteDbPath = process.env.SQLITE_DB_PATH;
78
33
  if (!sqliteDbPath) throw new Error("SQLITE_DB_PATH is required");
@@ -89,17 +44,13 @@ const buildSqliteDataPorts = async (): Promise<SearchIndexDataPorts> => {
89
44
  const { db } = await createSqliteDatabase(messageDataSchema, {
90
45
  filename: sqliteDbPath,
91
46
  });
92
- const genericDb = db as unknown as NodePgDatabase<Record<string, unknown>>;
93
- const messageDataDb = db as unknown as NodePgDatabase<
94
- typeof messageDataSchema
95
- >;
96
47
 
97
- const message = new DrizzleMessageRepository(messageDataDb);
98
- const mailbox = new MailboxRepo(genericDb);
48
+ const message = new DrizzleMessageRepository(db);
49
+ const mailbox = new MailboxRepo(db);
99
50
 
100
51
  return {
101
- account: new AccountRepo(genericDb),
102
- threadMessage: new DrizzleThreadMessageRepository(genericDb),
52
+ account: new AccountRepo(db),
53
+ threadMessage: new DrizzleThreadMessageRepository(db),
103
54
  resolveAccountId: async (messageId) => {
104
55
  const row = await message.get(messageId);
105
56
  return mailbox.resolveAccountId(row.mailboxId);
@@ -107,40 +58,28 @@ const buildSqliteDataPorts = async (): Promise<SearchIndexDataPorts> => {
107
58
  };
108
59
  };
109
60
 
110
- /**
111
- * Select the account + threadMessage data ports from the environment, mirroring
112
- * `buildVectorStoreFromEnv`'s `DATA_BACKEND` selection so one handler serves
113
- * the DynamoDB (AWS, production), Postgres (pg-parity), and SQLite
114
- * (single-box, RFC 036) stacks:
115
- *
116
- * - `DATA_BACKEND=postgres` → Drizzle repos over `PG_CONNECTION_URL`.
117
- * - `DATA_BACKEND=sqlite` → Drizzle repos over the shared `SQLITE_DB_PATH` file.
118
- * - otherwise → ElectroDB services over `DYNAMODB_TABLE_NAME` (unchanged from
119
- * the pre-convergence worker — this is the production path).
120
- *
121
- * The DynamoDB ports are injected by the composition root, which lives outside
122
- * this shared, open-core module and is never imported here.
123
- */
124
61
  let injectedDataPorts: SearchIndexDataPorts | null = null;
125
62
 
126
63
  /**
127
64
  * Register the DynamoDB-backed search-index data ports from the composition
128
- * root. The relational backends compose in-package above and never touch this
65
+ * root. The relational backend composes in-package above and never touches this
129
66
  * seam.
130
67
  */
131
68
  export const setSearchIndexDataPorts = (ports: SearchIndexDataPorts): void => {
132
69
  injectedDataPorts = ports;
133
70
  };
134
71
 
72
+ /**
73
+ * The search-index data ports for this process: the ones a composition root
74
+ * registered, or — for a process that registered none — the SQLite composition
75
+ * this build contains. `SQLITE_DB_PATH` is that composition's precondition, not
76
+ * a backend selection.
77
+ */
135
78
  export const buildDataPortsFromEnv =
136
79
  async (): Promise<SearchIndexDataPorts> => {
137
- if (process.env.DATA_BACKEND === "postgres")
138
- return buildPostgresDataPorts();
139
- if (process.env.DATA_BACKEND === "sqlite") return buildSqliteDataPorts();
140
- if (!injectedDataPorts) {
141
- throw new Error(
142
- "no DynamoDB search-index data ports registered — register them with setSearchIndexDataPorts() from your composition root",
143
- );
144
- }
145
- return injectedDataPorts;
80
+ if (injectedDataPorts) return injectedDataPorts;
81
+ if (process.env.SQLITE_DB_PATH) return buildSqliteDataPorts();
82
+ throw new Error(
83
+ "no search-index data ports registered — register them with setSearchIndexDataPorts() from your composition root",
84
+ );
146
85
  };
package/src/services.ts CHANGED
@@ -18,7 +18,7 @@ export interface Services {
18
18
  searchService: SearchService;
19
19
  resolveAccountId?: SearchIndexDataPorts["resolveAccountId"];
20
20
  /**
21
- * Fired once per upsert outcome — the pg-only work-summary signal
21
+ * Fired once per upsert outcome — the relational work-summary signal
22
22
  * (`consumer.ts` wires this to `IndexWorkStats`). `undefined` on the Lambda
23
23
  * path, where it never fires and so never affects behavior.
24
24
  */
@@ -34,24 +34,17 @@ export const getServices = async (): Promise<Services> => {
34
34
 
35
35
  const storageService = createStorageService();
36
36
 
37
- // The worker must have a durable vector store — a typo'd or missing S3 (or,
38
- // on Postgres, PG_CONNECTION_URL) env var must not silently succeed by
39
- // falling back to the throwaway in-memory store (which emits success
40
- // metrics but drops every vector).
41
- const isPostgres = process.env.DATA_BACKEND === "postgres";
42
- const pgConnectionUrl = process.env.PG_CONNECTION_URL;
37
+ // The worker must have a durable vector store — a typo'd or missing env var
38
+ // must not silently succeed by falling back to the throwaway in-memory store
39
+ // (which emits success metrics but drops every vector).
43
40
  const localPath = process.env.LOCAL_VECTORDB_PATH;
44
41
  const bucket = process.env.S3_VECTORS_BUCKET_NAME;
45
42
  const indexName = process.env.S3_VECTORS_INDEX_NAME;
46
- if (
47
- !(isPostgres && pgConnectionUrl) &&
48
- !localPath &&
49
- !(bucket && indexName)
50
- ) {
43
+ if (!localPath && !(bucket && indexName)) {
51
44
  throw new Error(
52
- "Vector store is not configured: set PG_CONNECTION_URL for the Postgres " +
53
- "backend, LOCAL_VECTORDB_PATH for local dev, or both " +
54
- "S3_VECTORS_BUCKET_NAME and S3_VECTORS_INDEX_NAME for production.",
45
+ "Vector store is not configured: set LOCAL_VECTORDB_PATH for local dev, " +
46
+ "or both S3_VECTORS_BUCKET_NAME and S3_VECTORS_INDEX_NAME for " +
47
+ "production.",
55
48
  );
56
49
  }
57
50