@open-mercato/shared 0.7.1-develop.7122.1.421cefe668 → 0.7.1-develop.7130.1.fef2396fd8
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/duplicateEntities.js +52 -0
- package/dist/lib/db/duplicateEntities.js.map +7 -0
- package/dist/lib/db/duplicateEntityClassNames.js +66 -0
- package/dist/lib/db/duplicateEntityClassNames.js.map +7 -0
- package/dist/lib/db/mikro.js +31 -0
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +6 -2
- package/src/lib/db/__tests__/duplicateEntities.test.ts +260 -0
- package/src/lib/db/__tests__/duplicateEntityClassNames.test.ts +172 -0
- package/src/lib/db/__tests__/duplicateEntityClassNamesExport.test.ts +63 -0
- package/src/lib/db/__tests__/fixtures/invoiceBilling.ts +7 -0
- package/src/lib/db/__tests__/fixtures/invoiceReporting.ts +7 -0
- package/src/lib/db/__tests__/fixtures/invoiceSubscriptions.ts +7 -0
- package/src/lib/db/__tests__/fixtures/ledgerBilling.ts +7 -0
- package/src/lib/db/__tests__/fixtures/ledgerReporting.ts +7 -0
- package/src/lib/db/__tests__/fixtures/ledgerSubscriptions.ts +7 -0
- package/src/lib/db/__tests__/registerOrmEntities.duplicates.test.ts +164 -0
- package/src/lib/db/duplicateEntities.ts +89 -0
- package/src/lib/db/duplicateEntityClassNames.ts +150 -0
- package/src/lib/db/mikro.ts +55 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 270 entry points
|
|
2
2
|
[build:shared] built successfully
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { EntitySchema, MetadataStorage } from "@mikro-orm/core";
|
|
2
|
+
import {
|
|
3
|
+
findDuplicateEntityClassNames
|
|
4
|
+
} from "./duplicateEntityClassNames.js";
|
|
5
|
+
function readString(value) {
|
|
6
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
7
|
+
}
|
|
8
|
+
function readModuleIdFromStamp(stamp) {
|
|
9
|
+
const value = readString(stamp);
|
|
10
|
+
if (!value) return void 0;
|
|
11
|
+
const separator = value.lastIndexOf(".");
|
|
12
|
+
return separator > 0 ? value.slice(0, separator) : void 0;
|
|
13
|
+
}
|
|
14
|
+
function toEntityClassNameEntry(value) {
|
|
15
|
+
if (EntitySchema.is(value)) {
|
|
16
|
+
const className2 = readString(value.meta?.className);
|
|
17
|
+
if (!className2) return null;
|
|
18
|
+
return { className: className2, sourcePath: readString(value.meta?.path), target: value };
|
|
19
|
+
}
|
|
20
|
+
if (typeof value !== "function") return null;
|
|
21
|
+
if (!Object.prototype.hasOwnProperty.call(value, MetadataStorage.PATH_SYMBOL)) return null;
|
|
22
|
+
const entity = value;
|
|
23
|
+
const className = readString(entity.name);
|
|
24
|
+
if (!className) return null;
|
|
25
|
+
return {
|
|
26
|
+
className,
|
|
27
|
+
moduleId: readModuleIdFromStamp(entity.entityName),
|
|
28
|
+
sourcePath: readString(entity[MetadataStorage.PATH_SYMBOL]),
|
|
29
|
+
target: value
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function collectEntityClassNameEntries(entities) {
|
|
33
|
+
const entries = [];
|
|
34
|
+
for (const value of entities) {
|
|
35
|
+
let entry = null;
|
|
36
|
+
try {
|
|
37
|
+
entry = toEntityClassNameEntry(value);
|
|
38
|
+
} catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (entry) entries.push(entry);
|
|
42
|
+
}
|
|
43
|
+
return entries;
|
|
44
|
+
}
|
|
45
|
+
function findDuplicateRegisteredEntityClassNames(entities) {
|
|
46
|
+
return findDuplicateEntityClassNames(collectEntityClassNameEntries(entities));
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
collectEntityClassNameEntries,
|
|
50
|
+
findDuplicateRegisteredEntityClassNames
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=duplicateEntities.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/db/duplicateEntities.ts"],
|
|
4
|
+
"sourcesContent": ["import { EntitySchema, MetadataStorage } from '@mikro-orm/core'\nimport {\n findDuplicateEntityClassNames,\n type DuplicateEntityClassNameGroup,\n type EntityClassNameEntry,\n} from './duplicateEntityClassNames'\n\n/**\n * Adapts a registered ORM entity array to the dependency-free collision detector in\n * `./duplicateEntityClassNames`, which explains the underlying MikroORM behaviour.\n */\n\ntype DecoratedEntityClass = {\n readonly name?: unknown\n readonly entityName?: unknown\n readonly [MetadataStorage.PATH_SYMBOL]?: unknown\n}\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\n/**\n * `enhanceEntities()` in the generated entity registry stamps `<moduleId>.<ExportName>`\n * onto every entity export. MikroORM ignores that stamp, but it is the only place the\n * contributing module id survives to runtime. Module ids may contain dots; the export\n * name never does, so split on the last one. A class that declares its own `entityName`\n * keeps it \u2014 `enhanceEntities()` never overwrites one \u2014 so a declared value containing a\n * dot yields a bogus module id here; cosmetic in the warning text, and a dot-free value\n * degrades to `undefined`.\n */\nfunction readModuleIdFromStamp(stamp: unknown): string | undefined {\n const value = readString(stamp)\n if (!value) return undefined\n const separator = value.lastIndexOf('.')\n return separator > 0 ? value.slice(0, separator) : undefined\n}\n\n/**\n * The registered array holds every function export of each module's entity file, so\n * plain helper functions travel alongside real entities. Only classes touched by a\n * MikroORM decorator carry `MetadataStorage.PATH_SYMBOL` as an own property; everything\n * else is skipped so helpers that happen to share a name are never reported.\n */\nfunction toEntityClassNameEntry(value: unknown): EntityClassNameEntry | null {\n // `enhanceEntities()` in the generated registry keeps only `typeof value === 'function'`\n // exports, so an EntitySchema a module exports never arrives through it. This branch is\n // live only for direct callers such as the testing bootstrap \u2014 and since the module id\n // stamp is applied to those same function exports, an EntitySchema carries no module id\n // and reports by path alone.\n if (EntitySchema.is(value)) {\n const className = readString(value.meta?.className)\n if (!className) return null\n return { className, sourcePath: readString(value.meta?.path), target: value }\n }\n if (typeof value !== 'function') return null\n if (!Object.prototype.hasOwnProperty.call(value, MetadataStorage.PATH_SYMBOL)) return null\n const entity = value as DecoratedEntityClass\n const className = readString(entity.name)\n if (!className) return null\n return {\n className,\n moduleId: readModuleIdFromStamp(entity.entityName),\n sourcePath: readString(entity[MetadataStorage.PATH_SYMBOL]),\n target: value,\n }\n}\n\nexport function collectEntityClassNameEntries(entities: readonly unknown[]): EntityClassNameEntry[] {\n const entries: EntityClassNameEntry[] = []\n for (const value of entities) {\n let entry: EntityClassNameEntry | null = null\n try {\n entry = toEntityClassNameEntry(value)\n } catch {\n // Reading a name off an exotic export (a throwing getter, a proxy) must not turn\n // a diagnostic into a boot failure. Skip the value and keep checking the rest.\n continue\n }\n if (entry) entries.push(entry)\n }\n return entries\n}\n\nexport function findDuplicateRegisteredEntityClassNames(\n entities: readonly unknown[],\n): DuplicateEntityClassNameGroup[] {\n return findDuplicateEntityClassNames(collectEntityClassNameEntries(entities))\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,cAAc,uBAAuB;AAC9C;AAAA,EACE;AAAA,OAGK;AAaP,SAAS,WAAW,OAAoC;AACtD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAWA,SAAS,sBAAsB,OAAoC;AACjE,QAAM,QAAQ,WAAW,KAAK;AAC9B,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,YAAY,MAAM,YAAY,GAAG;AACvC,SAAO,YAAY,IAAI,MAAM,MAAM,GAAG,SAAS,IAAI;AACrD;AAQA,SAAS,uBAAuB,OAA6C;AAM3E,MAAI,aAAa,GAAG,KAAK,GAAG;AAC1B,UAAMA,aAAY,WAAW,MAAM,MAAM,SAAS;AAClD,QAAI,CAACA,WAAW,QAAO;AACvB,WAAO,EAAE,WAAAA,YAAW,YAAY,WAAW,MAAM,MAAM,IAAI,GAAG,QAAQ,MAAM;AAAA,EAC9E;AACA,MAAI,OAAO,UAAU,WAAY,QAAO;AACxC,MAAI,CAAC,OAAO,UAAU,eAAe,KAAK,OAAO,gBAAgB,WAAW,EAAG,QAAO;AACtF,QAAM,SAAS;AACf,QAAM,YAAY,WAAW,OAAO,IAAI;AACxC,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO;AAAA,IACL;AAAA,IACA,UAAU,sBAAsB,OAAO,UAAU;AAAA,IACjD,YAAY,WAAW,OAAO,gBAAgB,WAAW,CAAC;AAAA,IAC1D,QAAQ;AAAA,EACV;AACF;AAEO,SAAS,8BAA8B,UAAsD;AAClG,QAAM,UAAkC,CAAC;AACzC,aAAW,SAAS,UAAU;AAC5B,QAAI,QAAqC;AACzC,QAAI;AACF,cAAQ,uBAAuB,KAAK;AAAA,IACtC,QAAQ;AAGN;AAAA,IACF;AACA,QAAI,MAAO,SAAQ,KAAK,KAAK;AAAA,EAC/B;AACA,SAAO;AACT;AAEO,SAAS,wCACd,UACiC;AACjC,SAAO,8BAA8B,8BAA8B,QAAQ,CAAC;AAC9E;",
|
|
6
|
+
"names": ["className"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const DUPLICATE_ENTITY_CLASS_NAMES_REASON = "MikroORM keeps one metadata entry per constructor, so class-based lookups such as em.find(<Name>) stay correct, but every name-based resolution \u2014 string relation targets, getRepository('<Name>'), relation discovery and serialization \u2014 goes through the name-keyed map, where only one of the same-named classes survives. Which one wins depends on registration order, so the other silently reads and writes the surviving class's table.";
|
|
2
|
+
const DUPLICATE_ENTITY_CLASS_NAMES_REMEDIATION = "Rename all but one of the colliding classes so every entity class name is unique across enabled modules, then update their exports, relation targets and imports. Table names may stay as they are.";
|
|
3
|
+
function identify(entry) {
|
|
4
|
+
if (entry.target) return entry.target;
|
|
5
|
+
if (entry.moduleId || entry.sourcePath) return `${entry.moduleId ?? ""}|${entry.sourcePath ?? ""}`;
|
|
6
|
+
return entry;
|
|
7
|
+
}
|
|
8
|
+
function findDuplicateEntityClassNames(entries) {
|
|
9
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
if (!entry.className) continue;
|
|
12
|
+
let bucket = buckets.get(entry.className);
|
|
13
|
+
if (!bucket) {
|
|
14
|
+
bucket = /* @__PURE__ */ new Map();
|
|
15
|
+
buckets.set(entry.className, bucket);
|
|
16
|
+
}
|
|
17
|
+
const identity = identify(entry);
|
|
18
|
+
if (!bucket.has(identity)) {
|
|
19
|
+
bucket.set(identity, { moduleId: entry.moduleId, sourcePath: entry.sourcePath });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const groups = [];
|
|
23
|
+
for (const [className, bucket] of buckets) {
|
|
24
|
+
if (bucket.size < 2) continue;
|
|
25
|
+
groups.push({ className, sources: Array.from(bucket.values()) });
|
|
26
|
+
}
|
|
27
|
+
return groups;
|
|
28
|
+
}
|
|
29
|
+
function formatSource(source) {
|
|
30
|
+
const path = source.sourcePath && /[\\/]/.test(source.sourcePath) ? source.sourcePath : void 0;
|
|
31
|
+
if (source.moduleId && path) return ` - ${source.moduleId} (${path})`;
|
|
32
|
+
if (source.moduleId) return ` - ${source.moduleId}`;
|
|
33
|
+
if (path) return ` - ${path}`;
|
|
34
|
+
return " - unknown module";
|
|
35
|
+
}
|
|
36
|
+
function formatDuplicateEntityClassNamesWarning(groups) {
|
|
37
|
+
const names = groups.map((group) => `"${group.className}"`).join(", ");
|
|
38
|
+
const lines = [
|
|
39
|
+
`Duplicate entity class name(s) defined by more than one enabled module: ${names}.`,
|
|
40
|
+
DUPLICATE_ENTITY_CLASS_NAMES_REASON,
|
|
41
|
+
DUPLICATE_ENTITY_CLASS_NAMES_REMEDIATION
|
|
42
|
+
];
|
|
43
|
+
for (const group of groups) {
|
|
44
|
+
lines.push(` ${group.className}`);
|
|
45
|
+
for (const source of group.sources) {
|
|
46
|
+
lines.push(formatSource(source));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
function toDuplicateEntityClassNameFields(groups) {
|
|
52
|
+
return {
|
|
53
|
+
classNames: groups.map((group) => group.className),
|
|
54
|
+
duplicates: groups.map((group) => ({ className: group.className, sources: group.sources })),
|
|
55
|
+
reason: DUPLICATE_ENTITY_CLASS_NAMES_REASON,
|
|
56
|
+
remediation: DUPLICATE_ENTITY_CLASS_NAMES_REMEDIATION
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
DUPLICATE_ENTITY_CLASS_NAMES_REASON,
|
|
61
|
+
DUPLICATE_ENTITY_CLASS_NAMES_REMEDIATION,
|
|
62
|
+
findDuplicateEntityClassNames,
|
|
63
|
+
formatDuplicateEntityClassNamesWarning,
|
|
64
|
+
toDuplicateEntityClassNameFields
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=duplicateEntityClassNames.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/db/duplicateEntityClassNames.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Detection and reporting for entity class names contributed by more than one module.\n *\n * MikroORM keys metadata by the JS class name. Discovery does keep a separate metadata\n * entry per constructor, so class-based lookups such as `em.find(Invoice)` stay correct,\n * but every name-based resolution \u2014 a string relation target, `getRepository('<Name>')`,\n * relation discovery, serialization \u2014 goes through the name-keyed map, where only one of\n * the same-named classes survives. Which one wins is decided by registration order, and\n * the loser is reachable by class only. Nothing fails; the wrong table is simply used.\n *\n * Nothing upstream catches it: `discovery.checkDuplicateEntities` is declared as a\n * default in @mikro-orm/core 7.1.9 but is read nowhere, and the one live check compares\n * table names rather than class names, which never collide here because every entity\n * declares an explicit `tableName`.\n *\n * This module is deliberately dependency-free so both the build-time generator and the\n * runtime bootstrap can share it without pulling the ORM into the generator. Detection\n * is kept separate from reporting so each caller decides whether a collision warns or\n * throws.\n */\n\n/**\n * Stable, constant sentences shared by both reporting surfaces: the structured runtime\n * log line puts them in fields, the generator renders them inline.\n */\nexport const DUPLICATE_ENTITY_CLASS_NAMES_REASON =\n \"MikroORM keeps one metadata entry per constructor, so class-based lookups such as em.find(<Name>) stay correct, but every name-based resolution \u2014 string relation targets, getRepository('<Name>'), relation discovery and serialization \u2014 goes through the name-keyed map, where only one of the same-named classes survives. Which one wins depends on registration order, so the other silently reads and writes the surviving class's table.\"\n\nexport const DUPLICATE_ENTITY_CLASS_NAMES_REMEDIATION =\n 'Rename all but one of the colliding classes so every entity class name is unique across enabled modules, then update their exports, relation targets and imports. Table names may stay as they are.'\n\nexport type EntityClassNameEntry = {\n className: string\n moduleId?: string\n sourcePath?: string\n /**\n * Runtime identity of the class. Two entries sharing a target are the same class\n * reached twice \u2014 re-exported through a second import path, or re-registered by an\n * HMR reload \u2014 and never count as a collision. Absent at build time, where the\n * declaring module and file identify the class instead.\n */\n target?: object\n}\n\nexport type DuplicateEntityClassNameSource = {\n moduleId?: string\n sourcePath?: string\n}\n\nexport type DuplicateEntityClassNameGroup = {\n className: string\n sources: DuplicateEntityClassNameSource[]\n}\n\n/**\n * Prefer the runtime class, then the declaring module and file. When an entry carries\n * none of those, fall back to the entry itself so it stays distinct: an unidentifiable\n * entry should fail open and surface a possible collision rather than collapse into a\n * shared bucket key and hide one.\n */\nfunction identify(entry: EntityClassNameEntry): unknown {\n if (entry.target) return entry.target\n if (entry.moduleId || entry.sourcePath) return `${entry.moduleId ?? ''}|${entry.sourcePath ?? ''}`\n return entry\n}\n\n/**\n * Group entries by class name, keeping only names contributed by two or more distinct\n * classes. Order follows first appearance, which is the module registration order.\n */\nexport function findDuplicateEntityClassNames(\n entries: readonly EntityClassNameEntry[],\n): DuplicateEntityClassNameGroup[] {\n const buckets = new Map<string, Map<unknown, DuplicateEntityClassNameSource>>()\n for (const entry of entries) {\n if (!entry.className) continue\n let bucket = buckets.get(entry.className)\n if (!bucket) {\n bucket = new Map<unknown, DuplicateEntityClassNameSource>()\n buckets.set(entry.className, bucket)\n }\n const identity = identify(entry)\n if (!bucket.has(identity)) {\n bucket.set(identity, { moduleId: entry.moduleId, sourcePath: entry.sourcePath })\n }\n }\n const groups: DuplicateEntityClassNameGroup[] = []\n for (const [className, bucket] of buckets) {\n if (bucket.size < 2) continue\n groups.push({ className, sources: Array.from(bucket.values()) })\n }\n return groups\n}\n\n/**\n * At runtime the source path comes from MikroORM's decorator, which derives it by\n * parsing a stack trace and falls back to the bare class name when that parse fails, so\n * only render a value that still looks like a path.\n */\nfunction formatSource(source: DuplicateEntityClassNameSource): string {\n const path = source.sourcePath && /[\\\\/]/.test(source.sourcePath) ? source.sourcePath : undefined\n if (source.moduleId && path) return ` - ${source.moduleId} (${path})`\n if (source.moduleId) return ` - ${source.moduleId}`\n if (path) return ` - ${path}`\n return ' - unknown module'\n}\n\n/**\n * Render every collision in one message, so a fix does not have to be discovered one\n * rerun at a time. Callers prepend their own surface prefix.\n */\nexport function formatDuplicateEntityClassNamesWarning(\n groups: readonly DuplicateEntityClassNameGroup[],\n): string {\n const names = groups.map((group) => `\"${group.className}\"`).join(', ')\n const lines = [\n `Duplicate entity class name(s) defined by more than one enabled module: ${names}.`,\n DUPLICATE_ENTITY_CLASS_NAMES_REASON,\n DUPLICATE_ENTITY_CLASS_NAMES_REMEDIATION,\n ]\n for (const group of groups) {\n lines.push(` ${group.className}`)\n for (const source of group.sources) {\n lines.push(formatSource(source))\n }\n }\n return lines.join('\\n')\n}\n\nexport type DuplicateEntityClassNameFields = {\n classNames: string[]\n duplicates: Array<{ className: string; sources: DuplicateEntityClassNameSource[] }>\n reason: string\n remediation: string\n}\n\n/**\n * The same collisions as queryable fields, for callers logging through the structured\n * facade, where the message must stay constant and the dynamic values live beside it.\n */\nexport function toDuplicateEntityClassNameFields(\n groups: readonly DuplicateEntityClassNameGroup[],\n): DuplicateEntityClassNameFields {\n return {\n classNames: groups.map((group) => group.className),\n duplicates: groups.map((group) => ({ className: group.className, sources: group.sources })),\n reason: DUPLICATE_ENTITY_CLASS_NAMES_REASON,\n remediation: DUPLICATE_ENTITY_CLASS_NAMES_REMEDIATION,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAyBO,MAAM,sCACX;AAEK,MAAM,2CACX;AA+BF,SAAS,SAAS,OAAsC;AACtD,MAAI,MAAM,OAAQ,QAAO,MAAM;AAC/B,MAAI,MAAM,YAAY,MAAM,WAAY,QAAO,GAAG,MAAM,YAAY,EAAE,IAAI,MAAM,cAAc,EAAE;AAChG,SAAO;AACT;AAMO,SAAS,8BACd,SACiC;AACjC,QAAM,UAAU,oBAAI,IAA0D;AAC9E,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,UAAW;AACtB,QAAI,SAAS,QAAQ,IAAI,MAAM,SAAS;AACxC,QAAI,CAAC,QAAQ;AACX,eAAS,oBAAI,IAA6C;AAC1D,cAAQ,IAAI,MAAM,WAAW,MAAM;AAAA,IACrC;AACA,UAAM,WAAW,SAAS,KAAK;AAC/B,QAAI,CAAC,OAAO,IAAI,QAAQ,GAAG;AACzB,aAAO,IAAI,UAAU,EAAE,UAAU,MAAM,UAAU,YAAY,MAAM,WAAW,CAAC;AAAA,IACjF;AAAA,EACF;AACA,QAAM,SAA0C,CAAC;AACjD,aAAW,CAAC,WAAW,MAAM,KAAK,SAAS;AACzC,QAAI,OAAO,OAAO,EAAG;AACrB,WAAO,KAAK,EAAE,WAAW,SAAS,MAAM,KAAK,OAAO,OAAO,CAAC,EAAE,CAAC;AAAA,EACjE;AACA,SAAO;AACT;AAOA,SAAS,aAAa,QAAgD;AACpE,QAAM,OAAO,OAAO,cAAc,QAAQ,KAAK,OAAO,UAAU,IAAI,OAAO,aAAa;AACxF,MAAI,OAAO,YAAY,KAAM,QAAO,SAAS,OAAO,QAAQ,KAAK,IAAI;AACrE,MAAI,OAAO,SAAU,QAAO,SAAS,OAAO,QAAQ;AACpD,MAAI,KAAM,QAAO,SAAS,IAAI;AAC9B,SAAO;AACT;AAMO,SAAS,uCACd,QACQ;AACR,QAAM,QAAQ,OAAO,IAAI,CAAC,UAAU,IAAI,MAAM,SAAS,GAAG,EAAE,KAAK,IAAI;AACrE,QAAM,QAAQ;AAAA,IACZ,2EAA2E,KAAK;AAAA,IAChF;AAAA,IACA;AAAA,EACF;AACA,aAAW,SAAS,QAAQ;AAC1B,UAAM,KAAK,KAAK,MAAM,SAAS,EAAE;AACjC,eAAW,UAAU,MAAM,SAAS;AAClC,YAAM,KAAK,aAAa,MAAM,CAAC;AAAA,IACjC;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAaO,SAAS,iCACd,QACgC;AAChC,SAAO;AAAA,IACL,YAAY,OAAO,IAAI,CAAC,UAAU,MAAM,SAAS;AAAA,IACjD,YAAY,OAAO,IAAI,CAAC,WAAW,EAAE,WAAW,MAAM,WAAW,SAAS,MAAM,QAAQ,EAAE;AAAA,IAC1F,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/db/mikro.js
CHANGED
|
@@ -5,16 +5,47 @@ import { ReflectMetadataProvider } from "@mikro-orm/decorators/legacy";
|
|
|
5
5
|
import { PostgreSqlDriver } from "@mikro-orm/postgresql";
|
|
6
6
|
import { getSslConfig } from "./ssl.js";
|
|
7
7
|
import { createLogger } from "../logger/index.js";
|
|
8
|
+
import { findDuplicateRegisteredEntityClassNames } from "./duplicateEntities.js";
|
|
9
|
+
import {
|
|
10
|
+
toDuplicateEntityClassNameFields
|
|
11
|
+
} from "./duplicateEntityClassNames.js";
|
|
8
12
|
const logger = createLogger("shared").child({ component: "orm" });
|
|
9
13
|
let ormInstance = null;
|
|
10
14
|
const GLOBAL_ENTITIES_KEY = "__openMercatoOrmEntities__";
|
|
15
|
+
const GLOBAL_REPORTED_DUPLICATE_ENTITY_NAMES_KEY = "__openMercatoReportedDuplicateEntityClassNames__";
|
|
16
|
+
function getReportedDuplicateEntityClassNames() {
|
|
17
|
+
const globals = globalThis;
|
|
18
|
+
const existing = globals[GLOBAL_REPORTED_DUPLICATE_ENTITY_NAMES_KEY];
|
|
19
|
+
if (existing instanceof Map) return existing;
|
|
20
|
+
const created = /* @__PURE__ */ new Map();
|
|
21
|
+
globals[GLOBAL_REPORTED_DUPLICATE_ENTITY_NAMES_KEY] = created;
|
|
22
|
+
return created;
|
|
23
|
+
}
|
|
24
|
+
function fingerprintCollision(group) {
|
|
25
|
+
return group.sources.map((source) => `${source.moduleId ?? ""}|${source.sourcePath ?? ""}`).sort((left, right) => left.localeCompare(right)).join(",");
|
|
26
|
+
}
|
|
11
27
|
function getRegisteredEntities() {
|
|
12
28
|
return globalThis[GLOBAL_ENTITIES_KEY] ?? null;
|
|
13
29
|
}
|
|
14
30
|
function setRegisteredEntities(entities) {
|
|
15
31
|
globalThis[GLOBAL_ENTITIES_KEY] = entities;
|
|
16
32
|
}
|
|
33
|
+
function warnOnDuplicateEntityClassNames(entities) {
|
|
34
|
+
try {
|
|
35
|
+
const duplicates = findDuplicateRegisteredEntityClassNames(entities);
|
|
36
|
+
const reported = getReportedDuplicateEntityClassNames();
|
|
37
|
+
const current = new Map(duplicates.map((group) => [group.className, fingerprintCollision(group)]));
|
|
38
|
+
const fresh = duplicates.filter((group) => reported.get(group.className) !== current.get(group.className));
|
|
39
|
+
reported.clear();
|
|
40
|
+
for (const [className, fingerprint] of current) reported.set(className, fingerprint);
|
|
41
|
+
if (fresh.length === 0) return;
|
|
42
|
+
logger.warn("Duplicate entity class names across enabled modules", toDuplicateEntityClassNameFields(fresh));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
logger.debug("Duplicate entity class name check skipped", { err });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
17
47
|
function registerOrmEntities(entities) {
|
|
48
|
+
warnOnDuplicateEntityClassNames(entities);
|
|
18
49
|
if (getRegisteredEntities() !== null && process.env.NODE_ENV === "development") {
|
|
19
50
|
logger.debug("ORM entities re-registered (this may occur during HMR)");
|
|
20
51
|
}
|
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 { ReflectMetadataProvider } from '@mikro-orm/decorators/legacy'\nimport { PostgreSqlDriver, type EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport { getSslConfig } from './ssl'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'orm' })\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 logger.debug('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 type ResolvedPoolConfig = {\n poolMin: number\n poolMax: number\n poolIdleTimeout: number\n poolAcquireTimeout: number\n idleSessionTimeoutMs: number | undefined\n idleInTransactionTimeoutMs: number | undefined\n statementTimeoutMs: number | undefined\n lockTimeoutMs: number | undefined\n}\n\n// Parse an optional positive-millisecond env var. Returns undefined when unset,\n// non-numeric, or non-positive so callers treat \"no value\" as \"no timeout\".\nfunction parsePositiveIntEnv(raw: string | undefined): number | undefined {\n const parsed = parseInt(raw || '')\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined\n}\n\nexport function resolvePoolConfig(env: NodeJS.ProcessEnv = process.env): ResolvedPoolConfig {\n const idleSessionTimeoutEnv = parseInt(env.DB_IDLE_SESSION_TIMEOUT_MS || '')\n const idleInTxTimeoutEnv = parseInt(env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || '')\n return {\n poolMin: parseInt(env.DB_POOL_MIN || '2'),\n poolMax: parseInt(env.DB_POOL_MAX || '20'),\n poolIdleTimeout: parseInt(env.DB_POOL_IDLE_TIMEOUT || '3000'),\n poolAcquireTimeout: parseInt(env.DB_POOL_ACQUIRE_TIMEOUT || '6000'),\n idleSessionTimeoutMs: Number.isFinite(idleSessionTimeoutEnv)\n ? idleSessionTimeoutEnv\n : env.NODE_ENV === 'production'\n ? undefined\n : 600_000,\n // Finite default in every environment (including production) so a leaked or idle\n // open transaction cannot pin a pool connection indefinitely and exhaust the pool.\n // Mirrors the long-standing dev value; override (incl. 0 to disable) via env.\n idleInTransactionTimeoutMs: Number.isFinite(idleInTxTimeoutEnv) ? idleInTxTimeoutEnv : 120_000,\n // Opt-in guards against runaway statements and lock waits. No timeout when unset.\n statementTimeoutMs: parsePositiveIntEnv(env.DB_STATEMENT_TIMEOUT_MS),\n lockTimeoutMs: parsePositiveIntEnv(env.DB_LOCK_TIMEOUT_MS),\n }\n}\n\ntype PoolLike = {\n on(event: 'error', listener: (err: unknown) => void): unknown\n on(event: 'connect', listener: (client: { on(event: 'error', listener: (err: unknown) => void): unknown }) => void): unknown\n options?: Record<string, unknown>\n}\n\n// Postgres can terminate a connection at any moment (admin termination, network\n// drop, and \u2014 most relevantly for long-running daemons \u2014 the\n// `idle_in_transaction_session_timeout` configured above, FATAL 25P03). Where\n// node-postgres surfaces that depends on the client's state:\n// - IDLE (checked into the pool): pg-pool re-emits on the pool's 'error' event.\n// - CHECKED OUT (e.g. a connection pinned by an open transaction while the app\n// awaits non-DB work): pg-pool removes its idle listener, so the FATAL emits\n// on the Client itself.\n// Either way an unlistened 'error' event crashes the whole process (\"Scheduler\n// polling engine exited unexpectedly with exit code 1\"). Swallow both: the pool\n// discards the dead client, and any in-flight transaction still fails normally\n// on its next query/commit against the dead connection.\n// The per-client listener is deliberately attached once on 'connect' and never\n// removed: it is a last-resort sink whose only job is to guarantee the 'error'\n// event always has a listener, in every client state. It is not error handling\n// and must not be \"cleaned up\" \u2014 removing it reintroduces the process crash.\n// A reaped IDLE client therefore logs twice (once here, once via the pool-level\n// handler that pg-pool's own idle listener re-emits); the pool-level line is the\n// one that identifies the client as idle.\nexport function attachPoolErrorHandlers(pool: PoolLike): void {\n pool.on('error', (err: unknown) => {\n logger.warn('Idle pg pool client error (connection reaped/terminated)', { err })\n })\n pool.on('connect', (client) => {\n client.on('error', (err: unknown) => {\n logger.warn('pg client error (connection reaped/terminated)', { err })\n })\n })\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 {\n poolMin,\n poolMax,\n poolIdleTimeout,\n poolAcquireTimeout,\n idleSessionTimeoutMs,\n idleInTransactionTimeoutMs,\n statementTimeoutMs,\n lockTimeoutMs,\n } = resolvePoolConfig()\n const connectionOptions =\n idleSessionTimeoutMs && idleSessionTimeoutMs > 0\n ? `-c idle_session_timeout=${idleSessionTimeoutMs}`\n : undefined\n\n const sslConfig = getSslConfig()\n\n if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {\n logger.info('Pool config', {\n poolMin,\n poolMax,\n poolIdleTimeout,\n poolAcquireTimeout,\n idleSessionTimeoutMs,\n idleInTransactionTimeoutMs,\n statementTimeoutMs,\n lockTimeoutMs,\n nodeEnv: process.env.NODE_ENV,\n })\n }\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`. Mirror `connectionTimeoutMillis` here too \u2014 older Mikro\n // versions read it from `pool`; v7 reads from `driverOptions` but accepting both\n // costs nothing and protects us from upstream config-merge regressions.\n pool: {\n min: poolMin,\n max: poolMax,\n idleTimeoutMillis: poolIdleTimeout,\n acquireTimeoutMillis: poolAcquireTimeout,\n } as any,\n // Driver options are merged into pg.PoolConfig (ClientConfig + pg-pool).\n driverOptions: {\n connectionTimeoutMillis: poolAcquireTimeout,\n idle_in_transaction_session_timeout: idleInTransactionTimeoutMs,\n statement_timeout: statementTimeoutMs,\n lock_timeout: lockTimeoutMs,\n options: connectionOptions,\n ssl: sslConfig,\n onPoolCreated: (pool: PoolLike) => {\n attachPoolErrorHandlers(pool)\n if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {\n logger.info('pg pool created with options', {\n max: pool.options?.max,\n min: pool.options?.min,\n idleTimeoutMillis: pool.options?.idleTimeoutMillis,\n connectionTimeoutMillis: pool.options?.connectionTimeoutMillis,\n })\n }\n },\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;AAC7B,SAAS,oBAAoB;
|
|
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'\nimport { createLogger } from '../logger'\nimport { findDuplicateRegisteredEntityClassNames } from './duplicateEntities'\nimport {\n toDuplicateEntityClassNameFields,\n type DuplicateEntityClassNameGroup,\n} from './duplicateEntityClassNames'\n\nconst logger = createLogger('shared').child({ component: 'orm' })\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// Same reason, plus HMR: a module-level map would reset on the very reloads it exists to\n// deduplicate across.\nconst GLOBAL_REPORTED_DUPLICATE_ENTITY_NAMES_KEY = '__openMercatoReportedDuplicateEntityClassNames__'\n\nfunction getReportedDuplicateEntityClassNames(): Map<string, string> {\n const globals = globalThis as Record<string, unknown>\n const existing = globals[GLOBAL_REPORTED_DUPLICATE_ENTITY_NAMES_KEY]\n if (existing instanceof Map) return existing as Map<string, string>\n const created = new Map<string, string>()\n globals[GLOBAL_REPORTED_DUPLICATE_ENTITY_NAMES_KEY] = created\n return created\n}\n\n/**\n * Identifies a collision by the modules and files that contribute to it, so a\n * re-registration reporting the same name from a different pair of modules is a new\n * collision rather than a repeat.\n */\nfunction fingerprintCollision(group: DuplicateEntityClassNameGroup): string {\n return group.sources\n .map((source) => `${source.moduleId ?? ''}|${source.sourcePath ?? ''}`)\n .sort((left, right) => left.localeCompare(right))\n .join(',')\n}\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\n/**\n * A duplicate entity class name across modules corrupts entity resolution silently, and\n * no build step catches it. Report it here \u2014 the one point every registration path goes\n * through \u2014 so the logs name the cause instead of only its distant symptoms.\n */\nfunction warnOnDuplicateEntityClassNames(entities: readonly unknown[]): void {\n try {\n const duplicates = findDuplicateRegisteredEntityClassNames(entities)\n // Development re-runs registration on every HMR reload, so report a collision only\n // when it appears or its contributing modules change. Reprinting the same warning on\n // every reload buries it, while tracking the previous registration rather than every\n // name ever seen keeps a collision that was fixed and reintroduced reportable.\n const reported = getReportedDuplicateEntityClassNames()\n const current = new Map(duplicates.map((group) => [group.className, fingerprintCollision(group)]))\n const fresh = duplicates.filter((group) => reported.get(group.className) !== current.get(group.className))\n reported.clear()\n for (const [className, fingerprint] of current) reported.set(className, fingerprint)\n if (fresh.length === 0) return\n logger.warn('Duplicate entity class names across enabled modules', toDuplicateEntityClassNameFields(fresh))\n } catch (err) {\n // This check is a diagnostic. It must never be the reason a bootstrap fails.\n logger.debug('Duplicate entity class name check skipped', { err })\n }\n}\n\nexport function registerOrmEntities(entities: any[]) {\n warnOnDuplicateEntityClassNames(entities)\n if (getRegisteredEntities() !== null && process.env.NODE_ENV === 'development') {\n logger.debug('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 type ResolvedPoolConfig = {\n poolMin: number\n poolMax: number\n poolIdleTimeout: number\n poolAcquireTimeout: number\n idleSessionTimeoutMs: number | undefined\n idleInTransactionTimeoutMs: number | undefined\n statementTimeoutMs: number | undefined\n lockTimeoutMs: number | undefined\n}\n\n// Parse an optional positive-millisecond env var. Returns undefined when unset,\n// non-numeric, or non-positive so callers treat \"no value\" as \"no timeout\".\nfunction parsePositiveIntEnv(raw: string | undefined): number | undefined {\n const parsed = parseInt(raw || '')\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined\n}\n\nexport function resolvePoolConfig(env: NodeJS.ProcessEnv = process.env): ResolvedPoolConfig {\n const idleSessionTimeoutEnv = parseInt(env.DB_IDLE_SESSION_TIMEOUT_MS || '')\n const idleInTxTimeoutEnv = parseInt(env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || '')\n return {\n poolMin: parseInt(env.DB_POOL_MIN || '2'),\n poolMax: parseInt(env.DB_POOL_MAX || '20'),\n poolIdleTimeout: parseInt(env.DB_POOL_IDLE_TIMEOUT || '3000'),\n poolAcquireTimeout: parseInt(env.DB_POOL_ACQUIRE_TIMEOUT || '6000'),\n idleSessionTimeoutMs: Number.isFinite(idleSessionTimeoutEnv)\n ? idleSessionTimeoutEnv\n : env.NODE_ENV === 'production'\n ? undefined\n : 600_000,\n // Finite default in every environment (including production) so a leaked or idle\n // open transaction cannot pin a pool connection indefinitely and exhaust the pool.\n // Mirrors the long-standing dev value; override (incl. 0 to disable) via env.\n idleInTransactionTimeoutMs: Number.isFinite(idleInTxTimeoutEnv) ? idleInTxTimeoutEnv : 120_000,\n // Opt-in guards against runaway statements and lock waits. No timeout when unset.\n statementTimeoutMs: parsePositiveIntEnv(env.DB_STATEMENT_TIMEOUT_MS),\n lockTimeoutMs: parsePositiveIntEnv(env.DB_LOCK_TIMEOUT_MS),\n }\n}\n\ntype PoolLike = {\n on(event: 'error', listener: (err: unknown) => void): unknown\n on(event: 'connect', listener: (client: { on(event: 'error', listener: (err: unknown) => void): unknown }) => void): unknown\n options?: Record<string, unknown>\n}\n\n// Postgres can terminate a connection at any moment (admin termination, network\n// drop, and \u2014 most relevantly for long-running daemons \u2014 the\n// `idle_in_transaction_session_timeout` configured above, FATAL 25P03). Where\n// node-postgres surfaces that depends on the client's state:\n// - IDLE (checked into the pool): pg-pool re-emits on the pool's 'error' event.\n// - CHECKED OUT (e.g. a connection pinned by an open transaction while the app\n// awaits non-DB work): pg-pool removes its idle listener, so the FATAL emits\n// on the Client itself.\n// Either way an unlistened 'error' event crashes the whole process (\"Scheduler\n// polling engine exited unexpectedly with exit code 1\"). Swallow both: the pool\n// discards the dead client, and any in-flight transaction still fails normally\n// on its next query/commit against the dead connection.\n// The per-client listener is deliberately attached once on 'connect' and never\n// removed: it is a last-resort sink whose only job is to guarantee the 'error'\n// event always has a listener, in every client state. It is not error handling\n// and must not be \"cleaned up\" \u2014 removing it reintroduces the process crash.\n// A reaped IDLE client therefore logs twice (once here, once via the pool-level\n// handler that pg-pool's own idle listener re-emits); the pool-level line is the\n// one that identifies the client as idle.\nexport function attachPoolErrorHandlers(pool: PoolLike): void {\n pool.on('error', (err: unknown) => {\n logger.warn('Idle pg pool client error (connection reaped/terminated)', { err })\n })\n pool.on('connect', (client) => {\n client.on('error', (err: unknown) => {\n logger.warn('pg client error (connection reaped/terminated)', { err })\n })\n })\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 {\n poolMin,\n poolMax,\n poolIdleTimeout,\n poolAcquireTimeout,\n idleSessionTimeoutMs,\n idleInTransactionTimeoutMs,\n statementTimeoutMs,\n lockTimeoutMs,\n } = resolvePoolConfig()\n const connectionOptions =\n idleSessionTimeoutMs && idleSessionTimeoutMs > 0\n ? `-c idle_session_timeout=${idleSessionTimeoutMs}`\n : undefined\n\n const sslConfig = getSslConfig()\n\n if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {\n logger.info('Pool config', {\n poolMin,\n poolMax,\n poolIdleTimeout,\n poolAcquireTimeout,\n idleSessionTimeoutMs,\n idleInTransactionTimeoutMs,\n statementTimeoutMs,\n lockTimeoutMs,\n nodeEnv: process.env.NODE_ENV,\n })\n }\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`. Mirror `connectionTimeoutMillis` here too \u2014 older Mikro\n // versions read it from `pool`; v7 reads from `driverOptions` but accepting both\n // costs nothing and protects us from upstream config-merge regressions.\n pool: {\n min: poolMin,\n max: poolMax,\n idleTimeoutMillis: poolIdleTimeout,\n acquireTimeoutMillis: poolAcquireTimeout,\n } as any,\n // Driver options are merged into pg.PoolConfig (ClientConfig + pg-pool).\n driverOptions: {\n connectionTimeoutMillis: poolAcquireTimeout,\n idle_in_transaction_session_timeout: idleInTransactionTimeoutMs,\n statement_timeout: statementTimeoutMs,\n lock_timeout: lockTimeoutMs,\n options: connectionOptions,\n ssl: sslConfig,\n onPoolCreated: (pool: PoolLike) => {\n attachPoolErrorHandlers(pool)\n if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {\n logger.info('pg pool created with options', {\n max: pool.options?.max,\n min: pool.options?.min,\n idleTimeoutMillis: pool.options?.idleTimeoutMillis,\n connectionTimeoutMillis: pool.options?.connectionTimeoutMillis,\n })\n }\n },\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": "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",
|
|
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.7.1-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.7.1-develop.7130.1.fef2396fd8';\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.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7130.1.fef2396fd8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"types": "./src/lib/events/patterns.ts",
|
|
53
53
|
"default": "./dist/lib/events/patterns.js"
|
|
54
54
|
},
|
|
55
|
+
"./lib/db/duplicateEntityClassNames": {
|
|
56
|
+
"types": "./src/lib/db/duplicateEntityClassNames.ts",
|
|
57
|
+
"default": "./dist/lib/db/duplicateEntityClassNames.js"
|
|
58
|
+
},
|
|
55
59
|
"./lib/data/consistency": {
|
|
56
60
|
"types": "./src/lib/data/consistency.ts",
|
|
57
61
|
"default": "./dist/lib/data/consistency.js"
|
|
@@ -109,7 +113,7 @@
|
|
|
109
113
|
"@mikro-orm/core": "^7.1.8",
|
|
110
114
|
"@mikro-orm/decorators": "^7.1.8",
|
|
111
115
|
"@mikro-orm/postgresql": "^7.1.8",
|
|
112
|
-
"@open-mercato/cache": "0.7.1-develop.
|
|
116
|
+
"@open-mercato/cache": "0.7.1-develop.7130.1.fef2396fd8",
|
|
113
117
|
"@types/html-to-text": "^9.0.4",
|
|
114
118
|
"@types/sanitize-html": "^2.16.1",
|
|
115
119
|
"dotenv": "^17.4.2",
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
import { EntitySchema, MetadataStorage } from '@mikro-orm/core'
|
|
3
|
+
import { ReflectMetadataProvider } from '@mikro-orm/decorators/legacy'
|
|
4
|
+
import { MikroORM } from '@mikro-orm/postgresql'
|
|
5
|
+
import { findDuplicateRegisteredEntityClassNames } from '../duplicateEntities'
|
|
6
|
+
import { Invoice as InvoiceBilling } from './fixtures/invoiceBilling'
|
|
7
|
+
import { Invoice as InvoiceSubscriptions } from './fixtures/invoiceSubscriptions'
|
|
8
|
+
import { Ledger as LedgerBilling } from './fixtures/ledgerBilling'
|
|
9
|
+
import { Ledger as LedgerSubscriptions } from './fixtures/ledgerSubscriptions'
|
|
10
|
+
import { Ledger as LedgerReporting } from './fixtures/ledgerReporting'
|
|
11
|
+
|
|
12
|
+
function readSourcePath(entity: unknown): string {
|
|
13
|
+
return (entity as Record<symbol, string>)[MetadataStorage.PATH_SYMBOL]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mirrors `enhanceEntities()` in the generated entity registry, which stamps
|
|
18
|
+
* `<moduleId>.<ExportName>` onto every entity export. The generator marks the property
|
|
19
|
+
* configurable, so tests can restamp between cases.
|
|
20
|
+
*/
|
|
21
|
+
function stampModuleId(entity: unknown, stamp: string): void {
|
|
22
|
+
Object.defineProperty(entity, 'entityName', {
|
|
23
|
+
value: stamp,
|
|
24
|
+
configurable: true,
|
|
25
|
+
enumerable: false,
|
|
26
|
+
writable: false,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clearModuleIdStamp(entity: unknown): void {
|
|
31
|
+
delete (entity as Record<string, unknown>).entityName
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('MikroORM name-based resolution (upstream behaviour pin)', () => {
|
|
35
|
+
// Pins the @mikro-orm/core 7.1.9 behaviour this guard exists for, through real
|
|
36
|
+
// discovery rather than bare MetadataStorage calls. Discovery keeps one metadata entry
|
|
37
|
+
// per constructor, so class-based lookups stay correct; the name-keyed map holds only
|
|
38
|
+
// one of the same-named classes, so everything resolved by name silently picks a
|
|
39
|
+
// winner. If a future MikroORM starts detecting class-name collisions itself, this
|
|
40
|
+
// test fails and the guard can be reconsidered.
|
|
41
|
+
//
|
|
42
|
+
// `connect: false` keeps it DB-free: discovery and metadata validation run in full,
|
|
43
|
+
// no connection is opened.
|
|
44
|
+
let orm: MikroORM
|
|
45
|
+
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
orm = await MikroORM.init({
|
|
48
|
+
entities: [InvoiceBilling, InvoiceSubscriptions],
|
|
49
|
+
metadataProvider: ReflectMetadataProvider,
|
|
50
|
+
dbName: 'duplicate-entity-class-names-probe',
|
|
51
|
+
connect: false,
|
|
52
|
+
discovery: { warnWhenNoEntities: false },
|
|
53
|
+
allowGlobalContext: true,
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
afterAll(async () => {
|
|
58
|
+
await orm?.close(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('accepts both same-named classes without any duplicate error', () => {
|
|
62
|
+
expect(InvoiceBilling).not.toBe(InvoiceSubscriptions)
|
|
63
|
+
expect(InvoiceBilling.name).toBe('Invoice')
|
|
64
|
+
expect(InvoiceSubscriptions.name).toBe('Invoice')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('keeps class-based lookups correct, which is why this never fails loudly', () => {
|
|
68
|
+
const metadata = orm.getMetadata()
|
|
69
|
+
|
|
70
|
+
expect(metadata.find(InvoiceBilling)?.tableName).toBe('duplicate_entity_fixture_billing')
|
|
71
|
+
expect(metadata.find(InvoiceSubscriptions)?.tableName).toBe('duplicate_entity_fixture_subscriptions')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('resolves the name to exactly one of them, so the other is unreachable by name', () => {
|
|
75
|
+
const metadata = orm.getMetadata()
|
|
76
|
+
const byName = metadata.find('Invoice')
|
|
77
|
+
|
|
78
|
+
expect(byName).toBeDefined()
|
|
79
|
+
// Registration order decides the winner; the point is that one of the two is simply
|
|
80
|
+
// gone from every name-based path.
|
|
81
|
+
expect([
|
|
82
|
+
'duplicate_entity_fixture_billing',
|
|
83
|
+
'duplicate_entity_fixture_subscriptions',
|
|
84
|
+
]).toContain(byName?.tableName)
|
|
85
|
+
expect(metadata.find(InvoiceBilling)?.tableName === byName?.tableName).not.toBe(
|
|
86
|
+
metadata.find(InvoiceSubscriptions)?.tableName === byName?.tableName,
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('hands a name-based repository the winner regardless of which module asked', () => {
|
|
91
|
+
const repositoryTable = orm.em.getRepository('Invoice').getEntityManager().getMetadata().find('Invoice')?.tableName
|
|
92
|
+
|
|
93
|
+
expect(repositoryTable).toBe(orm.getMetadata().find('Invoice')?.tableName)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('findDuplicateRegisteredEntityClassNames', () => {
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
for (const entity of [InvoiceBilling, InvoiceSubscriptions, LedgerBilling, LedgerSubscriptions, LedgerReporting]) {
|
|
100
|
+
clearModuleIdStamp(entity)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('reports nothing for an empty registration', () => {
|
|
105
|
+
expect(findDuplicateRegisteredEntityClassNames([])).toEqual([])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('ignores non-entity exports that share a name', () => {
|
|
109
|
+
// The generated registry spreads every function export of a module's entity file,
|
|
110
|
+
// so plain helpers travel alongside entities and must never be reported.
|
|
111
|
+
const first = function helper(): void {}
|
|
112
|
+
const second = function helper(): void {}
|
|
113
|
+
const values = [first, second, { name: 'TestEntity' }, { name: 'TestEntity' }, null, undefined, 'Invoice']
|
|
114
|
+
|
|
115
|
+
expect(findDuplicateRegisteredEntityClassNames(values)).toEqual([])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('ignores the same class registered twice (re-export or HMR re-registration)', () => {
|
|
119
|
+
expect(findDuplicateRegisteredEntityClassNames([InvoiceBilling, InvoiceBilling])).toEqual([])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('reports two distinct classes sharing a name, with module ids and source paths', () => {
|
|
123
|
+
stampModuleId(InvoiceBilling, 'billing.Invoice')
|
|
124
|
+
stampModuleId(InvoiceSubscriptions, 'subscriptions.Invoice')
|
|
125
|
+
|
|
126
|
+
const groups = findDuplicateRegisteredEntityClassNames([InvoiceBilling, InvoiceSubscriptions])
|
|
127
|
+
|
|
128
|
+
expect(groups).toHaveLength(1)
|
|
129
|
+
expect(groups[0].className).toBe('Invoice')
|
|
130
|
+
expect(groups[0].sources).toEqual([
|
|
131
|
+
{ moduleId: 'billing', sourcePath: readSourcePath(InvoiceBilling) },
|
|
132
|
+
{ moduleId: 'subscriptions', sourcePath: readSourcePath(InvoiceSubscriptions) },
|
|
133
|
+
])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('recovers module ids that contain dots', () => {
|
|
137
|
+
stampModuleId(InvoiceBilling, 'acme.billing.Invoice')
|
|
138
|
+
stampModuleId(InvoiceSubscriptions, 'subscriptions.Invoice')
|
|
139
|
+
|
|
140
|
+
const groups = findDuplicateRegisteredEntityClassNames([InvoiceBilling, InvoiceSubscriptions])
|
|
141
|
+
|
|
142
|
+
expect(groups[0].sources.map((source) => source.moduleId)).toEqual(['acme.billing', 'subscriptions'])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('still reports a collision when no module id stamp is present', () => {
|
|
146
|
+
const groups = findDuplicateRegisteredEntityClassNames([InvoiceBilling, InvoiceSubscriptions])
|
|
147
|
+
|
|
148
|
+
expect(groups).toHaveLength(1)
|
|
149
|
+
expect(groups[0].sources.map((source) => source.moduleId)).toEqual([undefined, undefined])
|
|
150
|
+
expect(groups[0].sources.map((source) => source.sourcePath)).toEqual([
|
|
151
|
+
readSourcePath(InvoiceBilling),
|
|
152
|
+
readSourcePath(InvoiceSubscriptions),
|
|
153
|
+
])
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('detects collisions between EntitySchema instances', () => {
|
|
157
|
+
const first = new EntitySchema({ name: 'Invoice', properties: {} })
|
|
158
|
+
const second = new EntitySchema({ name: 'Invoice', properties: {} })
|
|
159
|
+
|
|
160
|
+
const groups = findDuplicateRegisteredEntityClassNames([first, second])
|
|
161
|
+
|
|
162
|
+
expect(groups).toHaveLength(1)
|
|
163
|
+
expect(groups[0].className).toBe('Invoice')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('detects a collision between a decorated class and an EntitySchema', () => {
|
|
167
|
+
const schema = new EntitySchema({ name: 'Invoice', properties: {} })
|
|
168
|
+
|
|
169
|
+
const groups = findDuplicateRegisteredEntityClassNames([InvoiceBilling, schema])
|
|
170
|
+
|
|
171
|
+
expect(groups).toHaveLength(1)
|
|
172
|
+
expect(groups[0].sources).toHaveLength(2)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('reports every colliding name in one pass, including three-way collisions', () => {
|
|
176
|
+
const groups = findDuplicateRegisteredEntityClassNames([
|
|
177
|
+
InvoiceBilling,
|
|
178
|
+
InvoiceSubscriptions,
|
|
179
|
+
LedgerBilling,
|
|
180
|
+
LedgerSubscriptions,
|
|
181
|
+
LedgerReporting,
|
|
182
|
+
])
|
|
183
|
+
|
|
184
|
+
expect(groups.map((group) => group.className).sort()).toEqual(['Invoice', 'Ledger'])
|
|
185
|
+
expect(groups.find((group) => group.className === 'Ledger')?.sources).toHaveLength(3)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('resilience', () => {
|
|
190
|
+
// The check is a diagnostic; a hostile or exotic export must never be able to turn it
|
|
191
|
+
// into a bootstrap failure.
|
|
192
|
+
it('skips an entity whose name getter throws', () => {
|
|
193
|
+
const hostile = function () {} as unknown as Record<string, unknown>
|
|
194
|
+
Object.defineProperty(hostile, MetadataStorage.PATH_SYMBOL, { value: '/hostile.ts' })
|
|
195
|
+
Object.defineProperty(hostile, 'name', {
|
|
196
|
+
get() {
|
|
197
|
+
throw new Error('name is not readable')
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
expect(() => findDuplicateRegisteredEntityClassNames([hostile])).not.toThrow()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('skips an entity whose module id stamp throws', () => {
|
|
205
|
+
const hostile = function Invoice() {} as unknown as Record<string, unknown>
|
|
206
|
+
Object.defineProperty(hostile, MetadataStorage.PATH_SYMBOL, { value: '/hostile.ts' })
|
|
207
|
+
Object.defineProperty(hostile, 'entityName', {
|
|
208
|
+
get() {
|
|
209
|
+
throw new Error('entityName is not readable')
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
expect(() => findDuplicateRegisteredEntityClassNames([hostile])).not.toThrow()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('skips a proxy that throws on every trap', () => {
|
|
217
|
+
const hostile = new Proxy(function Invoice() {}, {
|
|
218
|
+
get() {
|
|
219
|
+
throw new Error('trapped')
|
|
220
|
+
},
|
|
221
|
+
has() {
|
|
222
|
+
throw new Error('trapped')
|
|
223
|
+
},
|
|
224
|
+
getOwnPropertyDescriptor() {
|
|
225
|
+
throw new Error('trapped')
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
expect(() => findDuplicateRegisteredEntityClassNames([hostile])).not.toThrow()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('still finds a real collision alongside a hostile export', () => {
|
|
233
|
+
const hostile = function () {} as unknown as Record<string, unknown>
|
|
234
|
+
Object.defineProperty(hostile, MetadataStorage.PATH_SYMBOL, { value: '/hostile.ts' })
|
|
235
|
+
Object.defineProperty(hostile, 'name', {
|
|
236
|
+
get() {
|
|
237
|
+
throw new Error('name is not readable')
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const groups = findDuplicateRegisteredEntityClassNames([hostile, InvoiceBilling, InvoiceSubscriptions])
|
|
242
|
+
|
|
243
|
+
expect(groups.map((group) => group.className)).toEqual(['Invoice'])
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('tolerates values that are not entities at all', () => {
|
|
247
|
+
expect(() =>
|
|
248
|
+
findDuplicateRegisteredEntityClassNames([
|
|
249
|
+
null,
|
|
250
|
+
undefined,
|
|
251
|
+
0,
|
|
252
|
+
'',
|
|
253
|
+
Symbol('entity'),
|
|
254
|
+
Object.create(null),
|
|
255
|
+
[],
|
|
256
|
+
new Map(),
|
|
257
|
+
]),
|
|
258
|
+
).not.toThrow()
|
|
259
|
+
})
|
|
260
|
+
})
|