@open-mercato/shared 0.5.1-develop.2691.d8a0934b37 → 0.5.1-develop.2699.f8b50c8046
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/dist/lib/api/crud.js +1 -1
- package/dist/lib/api/crud.js.map +2 -2
- package/dist/lib/auth/server.js +1 -1
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/data/engine.js +68 -27
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/mikro.js +18 -22
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/indexers/error-log.js +10 -12
- package/dist/lib/indexers/error-log.js.map +2 -2
- package/dist/lib/indexers/status-log.js +14 -16
- package/dist/lib/indexers/status-log.js.map +2 -2
- package/dist/lib/query/engine.js +220 -228
- package/dist/lib/query/engine.js.map +3 -3
- package/dist/lib/query/join-utils.js +28 -23
- package/dist/lib/query/join-utils.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/jest.config.cjs +4 -2
- package/package.json +1 -1
- package/src/lib/api/__tests__/crud.test.ts +5 -3
- package/src/lib/api/crud.ts +1 -1
- package/src/lib/auth/__tests__/server.apiKeyCache.test.ts +10 -4
- package/src/lib/auth/server.ts +1 -1
- package/src/lib/bootstrap/types.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.test.ts +27 -17
- package/src/lib/data/engine.ts +95 -47
- package/src/lib/db/mikro.ts +26 -25
- package/src/lib/indexers/error-log.ts +23 -23
- package/src/lib/indexers/status-log.ts +36 -33
- package/src/lib/query/__tests__/engine.scope-and-or.test.ts +253 -114
- package/src/lib/query/__tests__/engine.test.ts +206 -139
- package/src/lib/query/engine.ts +306 -263
- package/src/lib/query/join-utils.ts +38 -30
package/dist/lib/db/mikro.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/db/mikro.ts"],
|
|
4
|
-
"sourcesContent": ["import 'dotenv/config'\nimport 'reflect-metadata'\nimport { MikroORM } from '@mikro-orm/core'\nimport { PostgreSqlDriver } from '@mikro-orm/postgresql'\nimport { getSslConfig } from './ssl'\n\
|
|
5
|
-
"mappings": "AAAA,OAAO;AACP,OAAO;AACP,SAAS,gBAAgB;AACzB,SAAS,
|
|
4
|
+
"sourcesContent": ["import 'dotenv/config'\nimport 'reflect-metadata'\nimport { MikroORM } from '@mikro-orm/core'\nimport { ReflectMetadataProvider } from '@mikro-orm/decorators/legacy'\nimport { PostgreSqlDriver, type EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport { getSslConfig } from './ssl'\n\nexport type AppMikroORM = MikroORM<PostgreSqlDriver, PostgreSqlEntityManager<PostgreSqlDriver>>\n\nlet ormInstance: AppMikroORM | null = null\n\n// Use globalThis so standalone apps survive duplicated shared package module instances.\nconst GLOBAL_ENTITIES_KEY = '__openMercatoOrmEntities__'\n\nfunction getRegisteredEntities(): any[] | null {\n return (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] as any[] | null ?? null\n}\n\nfunction setRegisteredEntities(entities: any[]): void {\n (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] = entities\n}\n\nexport function registerOrmEntities(entities: any[]) {\n if (getRegisteredEntities() !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] ORM entities re-registered (this may occur during HMR)')\n }\n setRegisteredEntities(entities)\n}\n\nexport function getOrmEntities(): any[] {\n const entities = getRegisteredEntities()\n if (!entities) {\n throw new Error('[Bootstrap] ORM entities not registered. Call registerOrmEntities() at bootstrap.')\n }\n return entities\n}\n\nexport async function getOrm() {\n if (ormInstance) {\n return ormInstance\n }\n\n const entities = getOrmEntities()\n const clientUrl = process.env.DATABASE_URL\n if (!clientUrl) {\n throw new Error('DATABASE_URL is not set')\n }\n\n // Parse connection pool settings from environment\n const poolMin = parseInt(process.env.DB_POOL_MIN || '2')\n const poolMax = parseInt(process.env.DB_POOL_MAX || '20')\n const poolIdleTimeout = parseInt(process.env.DB_POOL_IDLE_TIMEOUT || '3000')\n const poolAcquireTimeout = parseInt(process.env.DB_POOL_ACQUIRE_TIMEOUT || '6000')\n const idleSessionTimeoutEnv = parseInt(process.env.DB_IDLE_SESSION_TIMEOUT_MS || '')\n const idleInTxTimeoutEnv = parseInt(process.env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || '')\n const idleSessionTimeoutMs = Number.isFinite(idleSessionTimeoutEnv)\n ? idleSessionTimeoutEnv\n : process.env.NODE_ENV === 'production'\n ? undefined\n : 600_000\n const idleInTransactionTimeoutMs = Number.isFinite(idleInTxTimeoutEnv)\n ? idleInTxTimeoutEnv\n : process.env.NODE_ENV === 'production'\n ? undefined\n : 120_000\n const connectionOptions =\n idleSessionTimeoutMs && idleSessionTimeoutMs > 0\n ? `-c idle_session_timeout=${idleSessionTimeoutMs}`\n : undefined\n\n const sslConfig = getSslConfig()\n\n ormInstance = await MikroORM.init<PostgreSqlDriver, PostgreSqlEntityManager<PostgreSqlDriver>>({\n driver: PostgreSqlDriver,\n clientUrl,\n entities,\n debug: false,\n // v7 no longer defaults to ReflectMetadataProvider. Entities in this repo use\n // `@mikro-orm/decorators/legacy`, which relies on TypeScript `emitDecoratorMetadata`\n // + reflect-metadata for type inference (nullability, column types). Without this,\n // inferred types are silently wrong at runtime.\n metadataProvider: ReflectMetadataProvider,\n // MikroORM v7 pool shape (min/max/idleTimeoutMillis). Knex-era `acquireTimeoutMillis` /\n // `destroyTimeoutMillis` were removed; acquire wait maps to pg `connectionTimeoutMillis`\n // below under `driverOptions`.\n pool: {\n min: poolMin,\n max: poolMax,\n idleTimeoutMillis: poolIdleTimeout,\n },\n // Driver options are merged into pg.PoolConfig (ClientConfig + pg-pool).\n driverOptions: {\n connectionTimeoutMillis: poolAcquireTimeout,\n idle_in_transaction_session_timeout: idleInTransactionTimeoutMs,\n options: connectionOptions,\n ssl: sslConfig,\n },\n })\n\n return ormInstance\n}\n\n\nasync function closeOrmIfLoaded(): Promise<void> {\n if (ormInstance) {\n await ormInstance.close(true)\n ormInstance = null\n }\n}\n\n// In dev mode, handle reloads cleanly without leaving dangling connections.\nif (process.env.NODE_ENV !== 'production') {\n void closeOrmIfLoaded()\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO;AACP,OAAO;AACP,SAAS,gBAAgB;AACzB,SAAS,+BAA+B;AACxC,SAAS,wBAAuE;AAChF,SAAS,oBAAoB;AAI7B,IAAI,cAAkC;AAGtC,MAAM,sBAAsB;AAE5B,SAAS,wBAAsC;AAC7C,SAAQ,WAAuC,mBAAmB,KAAqB;AACzF;AAEA,SAAS,sBAAsB,UAAuB;AACpD,EAAC,WAAuC,mBAAmB,IAAI;AACjE;AAEO,SAAS,oBAAoB,UAAiB;AACnD,MAAI,sBAAsB,MAAM,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,YAAQ,MAAM,oEAAoE;AAAA,EACpF;AACA,wBAAsB,QAAQ;AAChC;AAEO,SAAS,iBAAwB;AACtC,QAAM,WAAW,sBAAsB;AACvC,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,mFAAmF;AAAA,EACrG;AACA,SAAO;AACT;AAEA,eAAsB,SAAS;AAC7B,MAAI,aAAa;AACf,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,eAAe;AAChC,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AAGA,QAAM,UAAU,SAAS,QAAQ,IAAI,eAAe,GAAG;AACvD,QAAM,UAAU,SAAS,QAAQ,IAAI,eAAe,IAAI;AACxD,QAAM,kBAAkB,SAAS,QAAQ,IAAI,wBAAwB,MAAM;AAC3E,QAAM,qBAAqB,SAAS,QAAQ,IAAI,2BAA2B,MAAM;AACjF,QAAM,wBAAwB,SAAS,QAAQ,IAAI,8BAA8B,EAAE;AACnF,QAAM,qBAAqB,SAAS,QAAQ,IAAI,qCAAqC,EAAE;AACvF,QAAM,uBAAuB,OAAO,SAAS,qBAAqB,IAC9D,wBACA,QAAQ,IAAI,aAAa,eACvB,SACA;AACN,QAAM,6BAA6B,OAAO,SAAS,kBAAkB,IACjE,qBACA,QAAQ,IAAI,aAAa,eACvB,SACA;AACN,QAAM,oBACJ,wBAAwB,uBAAuB,IAC3C,2BAA2B,oBAAoB,KAC/C;AAEN,QAAM,YAAY,aAAa;AAE/B,gBAAc,MAAM,SAAS,KAAkE;AAAA,IAC7F,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKP,kBAAkB;AAAA;AAAA;AAAA;AAAA,IAIlB,MAAM;AAAA,MACJ,KAAK;AAAA,MACL,KAAK;AAAA,MACL,mBAAmB;AAAA,IACrB;AAAA;AAAA,IAEA,eAAe;AAAA,MACb,yBAAyB;AAAA,MACzB,qCAAqC;AAAA,MACrC,SAAS;AAAA,MACT,KAAK;AAAA,IACP;AAAA,EACF,CAAC;AAED,SAAO;AACT;AAGA,eAAe,mBAAkC;AAC/C,MAAI,aAAa;AACf,UAAM,YAAY,MAAM,IAAI;AAC5B,kBAAc;AAAA,EAChB;AACF;AAGA,IAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,OAAK,iBAAiB;AACxB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
1
2
|
const MAX_MESSAGE_LENGTH = 8192;
|
|
2
3
|
const MAX_STACK_LENGTH = 32768;
|
|
3
4
|
function truncate(input, limit) {
|
|
@@ -33,14 +34,11 @@ function safeJson(value) {
|
|
|
33
34
|
return value;
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
function
|
|
37
|
-
if (deps.
|
|
37
|
+
function pickDb(deps) {
|
|
38
|
+
if (deps.db) return deps.db;
|
|
38
39
|
if (deps.em) {
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
-
if (connection && typeof connection.getKnex === "function") {
|
|
42
|
-
return connection.getKnex();
|
|
43
|
-
}
|
|
41
|
+
return deps.em.getKysely();
|
|
44
42
|
} catch {
|
|
45
43
|
return null;
|
|
46
44
|
}
|
|
@@ -48,9 +46,9 @@ function pickKnex(deps) {
|
|
|
48
46
|
return null;
|
|
49
47
|
}
|
|
50
48
|
async function recordIndexerError(deps, input) {
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
53
|
-
console.error("[indexers] Unable to record indexer error (missing
|
|
49
|
+
const db = pickDb(deps);
|
|
50
|
+
if (!db) {
|
|
51
|
+
console.error("[indexers] Unable to record indexer error (missing db connection)", {
|
|
54
52
|
source: input.source,
|
|
55
53
|
handler: input.handler
|
|
56
54
|
});
|
|
@@ -60,18 +58,18 @@ async function recordIndexerError(deps, input) {
|
|
|
60
58
|
const payload = safeJson(input.payload);
|
|
61
59
|
const now = /* @__PURE__ */ new Date();
|
|
62
60
|
try {
|
|
63
|
-
await
|
|
61
|
+
await db.insertInto("indexer_error_logs").values({
|
|
64
62
|
source: input.source,
|
|
65
63
|
handler: input.handler,
|
|
66
64
|
entity_type: input.entityType ?? null,
|
|
67
65
|
record_id: input.recordId ?? null,
|
|
68
66
|
tenant_id: input.tenantId ?? null,
|
|
69
67
|
organization_id: input.organizationId ?? null,
|
|
70
|
-
payload
|
|
68
|
+
payload: payload === null ? null : sql`${JSON.stringify(payload)}::jsonb`,
|
|
71
69
|
message: truncate(message, MAX_MESSAGE_LENGTH),
|
|
72
70
|
stack: truncate(stack, MAX_STACK_LENGTH),
|
|
73
71
|
occurred_at: now
|
|
74
|
-
});
|
|
72
|
+
}).execute();
|
|
75
73
|
} catch (loggingError) {
|
|
76
74
|
console.error("[indexers] Failed to persist indexer error", loggingError);
|
|
77
75
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/indexers/error-log.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\n\nexport type IndexerErrorSource = 'query_index' | 'vector' | 'fulltext'\n\nexport type RecordIndexerErrorInput = {\n source: IndexerErrorSource\n handler: string\n error: unknown\n entityType?: string | null\n recordId?: string | null\n tenantId?: string | null\n organizationId?: string | null\n payload?: unknown\n}\n\ntype RecordIndexerErrorDeps = {\n em?: EntityManager\n db?: Kysely<any>\n}\n\nconst MAX_MESSAGE_LENGTH = 8_192\nconst MAX_STACK_LENGTH = 32_768\n\nfunction truncate(input: string | null | undefined, limit: number): string | null {\n if (!input) return null\n return input.length > limit ? `${input.slice(0, limit - 3)}...` : input\n}\n\nfunction normalizeError(error: unknown): { message: string; stack: string | null } {\n if (error instanceof Error) {\n return {\n message: error.message || error.name || 'Unknown error',\n stack: typeof error.stack === 'string' ? error.stack : null,\n }\n }\n if (typeof error === 'string') {\n return { message: error, stack: null }\n }\n try {\n const json = JSON.stringify(error)\n return { message: json, stack: null }\n } catch {\n return { message: String(error ?? 'Unknown error'), stack: null }\n }\n}\n\nfunction safeJson(value: unknown): unknown {\n if (value === undefined) return null\n try {\n return JSON.parse(JSON.stringify(value))\n } catch {\n if (value == null) return null\n if (typeof value === 'object') {\n return { note: 'unserializable', asString: String(value) }\n }\n return value\n }\n}\n\nfunction pickDb(deps: RecordIndexerErrorDeps): Kysely<any> | null {\n if (deps.db) return deps.db\n if (deps.em) {\n try {\n return deps.em.getKysely<any>()\n } catch {\n return null\n }\n }\n return null\n}\n\nexport async function recordIndexerError(deps: RecordIndexerErrorDeps, input: RecordIndexerErrorInput): Promise<void> {\n const db = pickDb(deps)\n if (!db) {\n console.error('[indexers] Unable to record indexer error (missing db connection)', {\n source: input.source,\n handler: input.handler,\n })\n return\n }\n\n const { message, stack } = normalizeError(input.error)\n const payload = safeJson(input.payload)\n const now = new Date()\n\n try {\n await db\n .insertInto('indexer_error_logs' as any)\n .values({\n source: input.source,\n handler: input.handler,\n entity_type: input.entityType ?? null,\n record_id: input.recordId ?? null,\n tenant_id: input.tenantId ?? null,\n organization_id: input.organizationId ?? null,\n payload: payload === null ? null : sql`${JSON.stringify(payload)}::jsonb`,\n message: truncate(message, MAX_MESSAGE_LENGTH),\n stack: truncate(stack, MAX_STACK_LENGTH),\n occurred_at: now,\n } as any)\n .execute()\n } catch (loggingError) {\n console.error('[indexers] Failed to persist indexer error', loggingError)\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAsB,WAAW;AAoBjC,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AAEzB,SAAS,SAAS,OAAkC,OAA8B;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,SAAS,QAAQ,GAAG,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,QAAQ;AACpE;AAEA,SAAS,eAAe,OAA2D;AACjF,MAAI,iBAAiB,OAAO;AAC1B,WAAO;AAAA,MACL,SAAS,MAAM,WAAW,MAAM,QAAQ;AAAA,MACxC,OAAO,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;AAAA,IACzD;AAAA,EACF;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,SAAS,OAAO,OAAO,KAAK;AAAA,EACvC;AACA,MAAI;AACF,UAAM,OAAO,KAAK,UAAU,KAAK;AACjC,WAAO,EAAE,SAAS,MAAM,OAAO,KAAK;AAAA,EACtC,QAAQ;AACN,WAAO,EAAE,SAAS,OAAO,SAAS,eAAe,GAAG,OAAO,KAAK;AAAA,EAClE;AACF;AAEA,SAAS,SAAS,OAAyB;AACzC,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI;AACF,WAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AAAA,EACzC,QAAQ;AACN,QAAI,SAAS,KAAM,QAAO;AAC1B,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,EAAE,MAAM,kBAAkB,UAAU,OAAO,KAAK,EAAE;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,MAAkD;AAChE,MAAI,KAAK,GAAI,QAAO,KAAK;AACzB,MAAI,KAAK,IAAI;AACX,QAAI;AACF,aAAO,KAAK,GAAG,UAAe;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAsB,mBAAmB,MAA8B,OAA+C;AACpH,QAAM,KAAK,OAAO,IAAI;AACtB,MAAI,CAAC,IAAI;AACP,YAAQ,MAAM,qEAAqE;AAAA,MACjF,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,IACjB,CAAC;AACD;AAAA,EACF;AAEA,QAAM,EAAE,SAAS,MAAM,IAAI,eAAe,MAAM,KAAK;AACrD,QAAM,UAAU,SAAS,MAAM,OAAO;AACtC,QAAM,MAAM,oBAAI,KAAK;AAErB,MAAI;AACF,UAAM,GACH,WAAW,oBAA2B,EACtC,OAAO;AAAA,MACN,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf,aAAa,MAAM,cAAc;AAAA,MACjC,WAAW,MAAM,YAAY;AAAA,MAC7B,WAAW,MAAM,YAAY;AAAA,MAC7B,iBAAiB,MAAM,kBAAkB;AAAA,MACzC,SAAS,YAAY,OAAO,OAAO,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,MAChE,SAAS,SAAS,SAAS,kBAAkB;AAAA,MAC7C,OAAO,SAAS,OAAO,gBAAgB;AAAA,MACvC,aAAa;AAAA,IACf,CAAQ,EACP,QAAQ;AAAA,EACb,SAAS,cAAc;AACrB,YAAQ,MAAM,8CAA8C,YAAY;AAAA,EAC1E;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
1
2
|
const MAX_MESSAGE_LENGTH = 4096;
|
|
2
3
|
const MAX_DELETE_BATCH = 5e3;
|
|
3
4
|
const MAX_LOGS_PER_SOURCE = 1e4;
|
|
@@ -17,31 +18,28 @@ function safeJson(value) {
|
|
|
17
18
|
return value;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
|
-
function
|
|
21
|
-
if (deps.
|
|
21
|
+
function pickDb(deps) {
|
|
22
|
+
if (deps.db) return deps.db;
|
|
22
23
|
if (deps.em) {
|
|
23
24
|
try {
|
|
24
|
-
|
|
25
|
-
if (connection && typeof connection.getKnex === "function") {
|
|
26
|
-
return connection.getKnex();
|
|
27
|
-
}
|
|
25
|
+
return deps.em.getKysely();
|
|
28
26
|
} catch {
|
|
29
27
|
return null;
|
|
30
28
|
}
|
|
31
29
|
}
|
|
32
30
|
return null;
|
|
33
31
|
}
|
|
34
|
-
async function pruneExcessLogs(
|
|
35
|
-
const rows = await
|
|
32
|
+
async function pruneExcessLogs(db, source) {
|
|
33
|
+
const rows = await db.selectFrom("indexer_status_logs").select("id").where("source", "=", source).orderBy("occurred_at", "desc").orderBy("id", "desc").offset(MAX_LOGS_PER_SOURCE).limit(MAX_DELETE_BATCH).execute();
|
|
36
34
|
if (!rows.length) return;
|
|
37
35
|
const ids = rows.map((row) => row.id).filter(Boolean);
|
|
38
36
|
if (!ids.length) return;
|
|
39
|
-
await
|
|
37
|
+
await db.deleteFrom("indexer_status_logs").where("id", "in", ids).execute();
|
|
40
38
|
}
|
|
41
39
|
async function recordIndexerLog(deps, input) {
|
|
42
|
-
const
|
|
43
|
-
if (!
|
|
44
|
-
console.warn("[indexers] Unable to record indexer log (missing
|
|
40
|
+
const db = pickDb(deps);
|
|
41
|
+
if (!db) {
|
|
42
|
+
console.warn("[indexers] Unable to record indexer log (missing db connection)", {
|
|
45
43
|
source: input.source,
|
|
46
44
|
handler: input.handler
|
|
47
45
|
});
|
|
@@ -52,7 +50,7 @@ async function recordIndexerLog(deps, input) {
|
|
|
52
50
|
const details = safeJson(input.details);
|
|
53
51
|
const occurredAt = /* @__PURE__ */ new Date();
|
|
54
52
|
try {
|
|
55
|
-
await
|
|
53
|
+
await db.insertInto("indexer_status_logs").values({
|
|
56
54
|
source: input.source,
|
|
57
55
|
handler: input.handler,
|
|
58
56
|
level,
|
|
@@ -61,15 +59,15 @@ async function recordIndexerLog(deps, input) {
|
|
|
61
59
|
tenant_id: input.tenantId ?? null,
|
|
62
60
|
organization_id: input.organizationId ?? null,
|
|
63
61
|
message,
|
|
64
|
-
details
|
|
62
|
+
details: details === null ? null : sql`${JSON.stringify(details)}::jsonb`,
|
|
65
63
|
occurred_at: occurredAt
|
|
66
|
-
});
|
|
64
|
+
}).execute();
|
|
67
65
|
} catch (error) {
|
|
68
66
|
console.error("[indexers] Failed to persist indexer log", error);
|
|
69
67
|
return;
|
|
70
68
|
}
|
|
71
69
|
try {
|
|
72
|
-
await pruneExcessLogs(
|
|
70
|
+
await pruneExcessLogs(db, input.source);
|
|
73
71
|
} catch (pruneError) {
|
|
74
72
|
console.warn("[indexers] Failed to prune indexer logs", pruneError);
|
|
75
73
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/indexers/status-log.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport type { IndexerErrorSource } from './error-log'\n\nexport type IndexerLogLevel = 'info' | 'warn'\n\nexport type RecordIndexerLogInput = {\n source: IndexerErrorSource\n handler: string\n message: string\n level?: IndexerLogLevel\n entityType?: string | null\n recordId?: string | null\n tenantId?: string | null\n organizationId?: string | null\n details?: unknown\n}\n\ntype RecordIndexerLogDeps = {\n em?: EntityManager\n db?: Kysely<any>\n}\n\nconst MAX_MESSAGE_LENGTH = 4_096\nconst MAX_DELETE_BATCH = 5_000\nconst MAX_LOGS_PER_SOURCE = 10_000\n\nfunction truncate(input: string | null | undefined, limit: number): string | null {\n if (!input) return null\n return input.length > limit ? `${input.slice(0, limit - 3)}...` : input\n}\n\nfunction safeJson(value: unknown): unknown {\n if (value === undefined) return null\n try {\n return JSON.parse(JSON.stringify(value))\n } catch {\n if (value == null) return null\n if (typeof value === 'object') {\n return { note: 'unserializable', asString: String(value) }\n }\n return value\n }\n}\n\nfunction pickDb(deps: RecordIndexerLogDeps): Kysely<any> | null {\n if (deps.db) return deps.db\n if (deps.em) {\n try {\n return deps.em.getKysely<any>()\n } catch {\n return null\n }\n }\n return null\n}\n\nasync function pruneExcessLogs(db: Kysely<any>, source: IndexerErrorSource): Promise<void> {\n const rows = await db\n .selectFrom('indexer_status_logs' as any)\n .select('id' as any)\n .where('source' as any, '=', source)\n .orderBy('occurred_at' as any, 'desc')\n .orderBy('id' as any, 'desc')\n .offset(MAX_LOGS_PER_SOURCE)\n .limit(MAX_DELETE_BATCH)\n .execute()\n\n if (!rows.length) return\n const ids = rows.map((row: any) => row.id).filter(Boolean)\n if (!ids.length) return\n await db\n .deleteFrom('indexer_status_logs' as any)\n .where('id' as any, 'in', ids)\n .execute()\n}\n\nexport async function recordIndexerLog(\n deps: RecordIndexerLogDeps,\n input: RecordIndexerLogInput,\n): Promise<void> {\n const db = pickDb(deps)\n if (!db) {\n console.warn('[indexers] Unable to record indexer log (missing db connection)', {\n source: input.source,\n handler: input.handler,\n })\n return\n }\n\n const level: IndexerLogLevel = input.level === 'warn' ? 'warn' : 'info'\n const message = truncate(input.message, MAX_MESSAGE_LENGTH) ?? '\u2014'\n const details = safeJson(input.details)\n const occurredAt = new Date()\n\n try {\n await db\n .insertInto('indexer_status_logs' as any)\n .values({\n source: input.source,\n handler: input.handler,\n level,\n entity_type: input.entityType ?? null,\n record_id: input.recordId ?? null,\n tenant_id: input.tenantId ?? null,\n organization_id: input.organizationId ?? null,\n message,\n details: details === null ? null : sql`${JSON.stringify(details)}::jsonb`,\n occurred_at: occurredAt,\n } as any)\n .execute()\n } catch (error) {\n console.error('[indexers] Failed to persist indexer log', error)\n return\n }\n\n try {\n await pruneExcessLogs(db, input.source)\n } catch (pruneError) {\n console.warn('[indexers] Failed to prune indexer logs', pruneError)\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAsB,WAAW;AAsBjC,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,sBAAsB;AAE5B,SAAS,SAAS,OAAkC,OAA8B;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,SAAS,QAAQ,GAAG,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,QAAQ;AACpE;AAEA,SAAS,SAAS,OAAyB;AACzC,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI;AACF,WAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AAAA,EACzC,QAAQ;AACN,QAAI,SAAS,KAAM,QAAO;AAC1B,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,EAAE,MAAM,kBAAkB,UAAU,OAAO,KAAK,EAAE;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,MAAgD;AAC9D,MAAI,KAAK,GAAI,QAAO,KAAK;AACzB,MAAI,KAAK,IAAI;AACX,QAAI;AACF,aAAO,KAAK,GAAG,UAAe;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,gBAAgB,IAAiB,QAA2C;AACzF,QAAM,OAAO,MAAM,GAChB,WAAW,qBAA4B,EACvC,OAAO,IAAW,EAClB,MAAM,UAAiB,KAAK,MAAM,EAClC,QAAQ,eAAsB,MAAM,EACpC,QAAQ,MAAa,MAAM,EAC3B,OAAO,mBAAmB,EAC1B,MAAM,gBAAgB,EACtB,QAAQ;AAEX,MAAI,CAAC,KAAK,OAAQ;AAClB,QAAM,MAAM,KAAK,IAAI,CAAC,QAAa,IAAI,EAAE,EAAE,OAAO,OAAO;AACzD,MAAI,CAAC,IAAI,OAAQ;AACjB,QAAM,GACH,WAAW,qBAA4B,EACvC,MAAM,MAAa,MAAM,GAAG,EAC5B,QAAQ;AACb;AAEA,eAAsB,iBACpB,MACA,OACe;AACf,QAAM,KAAK,OAAO,IAAI;AACtB,MAAI,CAAC,IAAI;AACP,YAAQ,KAAK,mEAAmE;AAAA,MAC9E,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,IACjB,CAAC;AACD;AAAA,EACF;AAEA,QAAM,QAAyB,MAAM,UAAU,SAAS,SAAS;AACjE,QAAM,UAAU,SAAS,MAAM,SAAS,kBAAkB,KAAK;AAC/D,QAAM,UAAU,SAAS,MAAM,OAAO;AACtC,QAAM,aAAa,oBAAI,KAAK;AAE5B,MAAI;AACF,UAAM,GACH,WAAW,qBAA4B,EACvC,OAAO;AAAA,MACN,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf;AAAA,MACA,aAAa,MAAM,cAAc;AAAA,MACjC,WAAW,MAAM,YAAY;AAAA,MAC7B,WAAW,MAAM,YAAY;AAAA,MAC7B,iBAAiB,MAAM,kBAAkB;AAAA,MACzC;AAAA,MACA,SAAS,YAAY,OAAO,OAAO,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,MAChE,aAAa;AAAA,IACf,CAAQ,EACP,QAAQ;AAAA,EACb,SAAS,OAAO;AACd,YAAQ,MAAM,4CAA4C,KAAK;AAC/D;AAAA,EACF;AAEA,MAAI;AACF,UAAM,gBAAgB,IAAI,MAAM,MAAM;AAAA,EACxC,SAAS,YAAY;AACnB,YAAQ,KAAK,2CAA2C,UAAU;AAAA,EACpE;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|