@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 +1 -4
- package/src/data-ports.test.ts +27 -38
- package/src/data-ports.ts +23 -84
- package/src/services.ts +8 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/search-index-worker",
|
|
3
|
-
"version": "0.0.
|
|
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": {
|
package/src/data-ports.test.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
36
|
+
/no search-index data ports registered/,
|
|
38
37
|
);
|
|
39
38
|
});
|
|
40
39
|
});
|
|
41
40
|
|
|
42
|
-
test("
|
|
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({
|
|
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`
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
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(
|
|
98
|
-
const mailbox = new MailboxRepo(
|
|
48
|
+
const message = new DrizzleMessageRepository(db);
|
|
49
|
+
const mailbox = new MailboxRepo(db);
|
|
99
50
|
|
|
100
51
|
return {
|
|
101
|
-
account: new AccountRepo(
|
|
102
|
-
threadMessage: new DrizzleThreadMessageRepository(
|
|
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
|
|
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 (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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
|
|
38
|
-
//
|
|
39
|
-
//
|
|
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
|
|
53
|
-
"
|
|
54
|
-
"
|
|
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
|
|