@open-mercato/shared 0.6.7-develop.6660.1.90e1e2eef6 → 0.6.7-develop.6661.1.0043ed1d03
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/db/pg-errors.js +11 -0
- package/dist/lib/db/pg-errors.js.map +7 -0
- package/dist/lib/indexers/error-log.js +9 -0
- package/dist/lib/indexers/error-log.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/db/pg-errors.ts +12 -0
- package/src/lib/indexers/error-log.ts +14 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 241 entry points
|
|
2
2
|
[build:shared] built successfully
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function isUniqueViolation(err) {
|
|
2
|
+
if (!err || typeof err !== "object") return false;
|
|
3
|
+
const code = err.code;
|
|
4
|
+
if (code === "23505") return true;
|
|
5
|
+
const message = err.message;
|
|
6
|
+
return typeof message === "string" && /duplicate key value|unique constraint/i.test(message);
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
isUniqueViolation
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=pg-errors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/db/pg-errors.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Detect a Postgres unique-constraint violation (SQLSTATE 23505) regardless of\n * the ORM/driver layer that surfaces it. Shared across modules so duplicate-insert\n * handling stays consistent platform-wide.\n */\nexport function isUniqueViolation(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false\n const code = (err as { code?: string }).code\n if (code === '23505') return true // Postgres unique_violation\n const message = (err as { message?: string }).message\n return typeof message === 'string' && /duplicate key value|unique constraint/i.test(message)\n}\n"],
|
|
5
|
+
"mappings": "AAKO,SAAS,kBAAkB,KAAuB;AACvD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,OAAQ,IAA0B;AACxC,MAAI,SAAS,QAAS,QAAO;AAC7B,QAAM,UAAW,IAA6B;AAC9C,SAAO,OAAO,YAAY,YAAY,yCAAyC,KAAK,OAAO;AAC7F;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -59,6 +59,15 @@ async function recordIndexerError(deps, input) {
|
|
|
59
59
|
const { message, stack } = normalizeError(input.error);
|
|
60
60
|
const payload = safeJson(input.payload);
|
|
61
61
|
const now = /* @__PURE__ */ new Date();
|
|
62
|
+
logger.error("Indexer error recorded", {
|
|
63
|
+
source: input.source,
|
|
64
|
+
handler: input.handler,
|
|
65
|
+
entityType: input.entityType ?? null,
|
|
66
|
+
recordId: input.recordId ?? null,
|
|
67
|
+
tenantId: input.tenantId ?? null,
|
|
68
|
+
organizationId: input.organizationId ?? null,
|
|
69
|
+
err: input.error
|
|
70
|
+
});
|
|
62
71
|
try {
|
|
63
72
|
await db.insertInto("indexer_error_logs").values({
|
|
64
73
|
source: input.source,
|
|
@@ -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 Kysely, sql } from 'kysely'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'indexers' })\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 logger.error('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 logger.error('Failed to persist indexer error', { err: loggingError })\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAsB,WAAW;AACjC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,WAAW,CAAC;AAoBrE,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,WAAO,MAAM,0DAA0D;AAAA,MACrE,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;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'indexers' })\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 logger.error('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 // Persisting to indexer_error_logs is not enough on its own: nobody watches that\n // table, and a failing database is exactly when the insert below is least likely\n // to land. Emit through the log facade too so the failure survives the process.\n // `input.payload` is deliberately omitted \u2014 it can carry record documents.\n logger.error('Indexer error recorded', {\n source: input.source,\n handler: input.handler,\n entityType: input.entityType ?? null,\n recordId: input.recordId ?? null,\n tenantId: input.tenantId ?? null,\n organizationId: input.organizationId ?? null,\n err: input.error,\n })\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 logger.error('Failed to persist indexer error', { err: loggingError })\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAsB,WAAW;AACjC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,WAAW,CAAC;AAoBrE,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,WAAO,MAAM,0DAA0D;AAAA,MACrE,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;AAMrB,SAAO,MAAM,0BAA0B;AAAA,IACrC,QAAQ,MAAM;AAAA,IACd,SAAS,MAAM;AAAA,IACf,YAAY,MAAM,cAAc;AAAA,IAChC,UAAU,MAAM,YAAY;AAAA,IAC5B,UAAU,MAAM,YAAY;AAAA,IAC5B,gBAAgB,MAAM,kBAAkB;AAAA,IACxC,KAAK,MAAM;AAAA,EACb,CAAC;AAED,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,WAAO,MAAM,mCAAmC,EAAE,KAAK,aAAa,CAAC;AAAA,EACvE;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6661.1.0043ed1d03'\nexport const appVersion = APP_VERSION\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6661.1.0043ed1d03",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"@mikro-orm/core": "^7.1.5",
|
|
98
98
|
"@mikro-orm/decorators": "^7.1.5",
|
|
99
99
|
"@mikro-orm/postgresql": "^7.1.5",
|
|
100
|
-
"@open-mercato/cache": "0.6.7-develop.
|
|
100
|
+
"@open-mercato/cache": "0.6.7-develop.6661.1.0043ed1d03",
|
|
101
101
|
"@types/sanitize-html": "^2.16.1",
|
|
102
102
|
"dotenv": "^17.4.2",
|
|
103
103
|
"pino": "^10.3.1",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect a Postgres unique-constraint violation (SQLSTATE 23505) regardless of
|
|
3
|
+
* the ORM/driver layer that surfaces it. Shared across modules so duplicate-insert
|
|
4
|
+
* handling stays consistent platform-wide.
|
|
5
|
+
*/
|
|
6
|
+
export function isUniqueViolation(err: unknown): boolean {
|
|
7
|
+
if (!err || typeof err !== 'object') return false
|
|
8
|
+
const code = (err as { code?: string }).code
|
|
9
|
+
if (code === '23505') return true // Postgres unique_violation
|
|
10
|
+
const message = (err as { message?: string }).message
|
|
11
|
+
return typeof message === 'string' && /duplicate key value|unique constraint/i.test(message)
|
|
12
|
+
}
|
|
@@ -87,6 +87,20 @@ export async function recordIndexerError(deps: RecordIndexerErrorDeps, input: Re
|
|
|
87
87
|
const payload = safeJson(input.payload)
|
|
88
88
|
const now = new Date()
|
|
89
89
|
|
|
90
|
+
// Persisting to indexer_error_logs is not enough on its own: nobody watches that
|
|
91
|
+
// table, and a failing database is exactly when the insert below is least likely
|
|
92
|
+
// to land. Emit through the log facade too so the failure survives the process.
|
|
93
|
+
// `input.payload` is deliberately omitted — it can carry record documents.
|
|
94
|
+
logger.error('Indexer error recorded', {
|
|
95
|
+
source: input.source,
|
|
96
|
+
handler: input.handler,
|
|
97
|
+
entityType: input.entityType ?? null,
|
|
98
|
+
recordId: input.recordId ?? null,
|
|
99
|
+
tenantId: input.tenantId ?? null,
|
|
100
|
+
organizationId: input.organizationId ?? null,
|
|
101
|
+
err: input.error,
|
|
102
|
+
})
|
|
103
|
+
|
|
90
104
|
try {
|
|
91
105
|
await db
|
|
92
106
|
.insertInto('indexer_error_logs' as any)
|