@open-mercato/core 0.6.3-develop.3586.1.d5a44c810f → 0.6.3-develop.3593.1.8eda8943fa
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/modules/customers/lib/personCompanyLinkTable.js +4 -30
- package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260521175027.js +12 -0
- package/dist/modules/customers/migrations/Migration20260521175027.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/customers/lib/personCompanyLinkTable.ts +33 -41
- package/src/modules/customers/migrations/Migration20260521175027.ts +39 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,35 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const PERSON_COMPANY_LINKS_DELETED_AT_COLUMN = "deleted_at";
|
|
4
|
-
let supportsDeletedAtColumnPromise = null;
|
|
5
|
-
let warnedAboutMissingDeletedAtColumn = false;
|
|
6
|
-
async function customerPersonCompanyLinksSupportDeletedAt(em) {
|
|
7
|
-
if (typeof em.getKysely !== "function") {
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
if (!supportsDeletedAtColumnPromise) {
|
|
11
|
-
const db = em.getKysely();
|
|
12
|
-
const probe = db.selectFrom("information_schema.columns").select(["column_name"]).where(sql`table_schema = current_schema()`).where("table_name", "=", PERSON_COMPANY_LINKS_TABLE).where("column_name", "=", PERSON_COMPANY_LINKS_DELETED_AT_COLUMN).executeTakeFirst().then((row) => !!row).catch(() => false);
|
|
13
|
-
supportsDeletedAtColumnPromise = probe;
|
|
14
|
-
return probe;
|
|
15
|
-
}
|
|
16
|
-
return supportsDeletedAtColumnPromise;
|
|
1
|
+
async function customerPersonCompanyLinksSupportDeletedAt(_em) {
|
|
2
|
+
return true;
|
|
17
3
|
}
|
|
18
|
-
function warnMissingCustomerPersonCompanyLinksDeletedAt(
|
|
19
|
-
if (warnedAboutMissingDeletedAtColumn) {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
warnedAboutMissingDeletedAtColumn = true;
|
|
23
|
-
console.warn(
|
|
24
|
-
`[${source}] missing ${PERSON_COMPANY_LINKS_TABLE}.${PERSON_COMPANY_LINKS_DELETED_AT_COLUMN}; continuing without link soft-delete filtering. Run yarn db:migrate.`
|
|
25
|
-
);
|
|
4
|
+
function warnMissingCustomerPersonCompanyLinksDeletedAt(_source) {
|
|
26
5
|
}
|
|
27
|
-
async function withActiveCustomerPersonCompanyLinkFilter(
|
|
28
|
-
const supportsDeletedAt = await customerPersonCompanyLinksSupportDeletedAt(em);
|
|
29
|
-
if (!supportsDeletedAt) {
|
|
30
|
-
warnMissingCustomerPersonCompanyLinksDeletedAt(source);
|
|
31
|
-
return { ...where };
|
|
32
|
-
}
|
|
6
|
+
async function withActiveCustomerPersonCompanyLinkFilter(_em, where, _source) {
|
|
33
7
|
return { ...where, deletedAt: null };
|
|
34
8
|
}
|
|
35
9
|
function filterActivePersonCompanyLinks(links) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/lib/personCompanyLinkTable.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\n\n// The previous implementation probed `information_schema.columns` on every\n// first call via `em.getKysely()` to decide whether to add a `deletedAt: null`\n// filter. When that probe ran inside an already-active MikroORM transaction it\n// shared the transaction's connection \u2014 and any failure (including transient\n// connection-pool state, an aborted TX from a sibling query, or pg returning\n// 25P02) propagated back into the host transaction, poisoning every following\n// query with `current transaction is aborted, commands ignored until end of\n// transaction block`. That is what produced the standalone-CI failure burst\n// (loadPersonCompanyLinks \u2192 25P02 \u2192 customers.people.create \u2192 500) and the\n// sync_excel worker `failedCount: 1` regression.\n//\n// The `deleted_at` column on `customer_person_company_links` has been part of\n// the committed module snapshot since `Migration20260415095203` (April 2026)\n// and is referenced by `Migration20260417140000` (partial unique index uses\n// `where deleted_at is null`). Any environment running these migrations\n// already has the column. So the probe is no longer load-bearing \u2014 it only\n// adds risk. We now unconditionally include `deletedAt: null` in the filter.\n// If a downstream DB is genuinely un-migrated, the underlying `em.find` will\n// surface the authentic `column \"deleted_at\" of relation\n// \"customer_person_company_links\" does not exist` (pg 42703) pointing the\n// operator at the real fix (`yarn db:migrate`).\n//\n// `customerPersonCompanyLinksSupportDeletedAt` and\n// `warnMissingCustomerPersonCompanyLinksDeletedAt` are kept as exported\n// no-op-equivalents so external callers (and the published @open-mercato/core\n// API surface) don't break.\n\nexport async function customerPersonCompanyLinksSupportDeletedAt(_em: EntityManager): Promise<boolean> {\n return true\n}\n\nexport function warnMissingCustomerPersonCompanyLinksDeletedAt(_source: string): void {\n // No-op: column is guaranteed present by Migration20260415095203 and the\n // module snapshot. Kept for backward-compatible exports.\n}\n\nexport async function withActiveCustomerPersonCompanyLinkFilter<T extends Record<string, unknown>>(\n _em: EntityManager,\n where: T,\n _source: string,\n): Promise<T & { deletedAt?: null }> {\n return { ...where, deletedAt: null }\n}\n\n/**\n * Drop soft-deleted link rows from a result set as a defense-in-depth fallback.\n * MikroORM has historically dropped `deletedAt: null` from the WHERE clause for\n * nullable date columns under certain configurations, so callers SHOULD apply this\n * after `findWithDecryption(...)` until the upstream query filter is verified to\n * fully cover all callers.\n */\nexport function filterActivePersonCompanyLinks<T extends { deletedAt?: Date | string | null | undefined }>(\n links: T[] | null | undefined,\n): T[] {\n if (!Array.isArray(links)) return []\n return links.filter((entry) => entry?.deletedAt == null)\n}\n"],
|
|
5
|
+
"mappings": "AA6BA,eAAsB,2CAA2C,KAAsC;AACrG,SAAO;AACT;AAEO,SAAS,+CAA+C,SAAuB;AAGtF;AAEA,eAAsB,0CACpB,KACA,OACA,SACmC;AACnC,SAAO,EAAE,GAAG,OAAO,WAAW,KAAK;AACrC;AASO,SAAS,+BACd,OACK;AACL,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC;AACnC,SAAO,MAAM,OAAO,CAAC,UAAU,OAAO,aAAa,IAAI;AACzD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260521175027 extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(`alter table "customer_person_company_links" add column if not exists "deleted_at" timestamptz null;`);
|
|
5
|
+
}
|
|
6
|
+
async down() {
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
Migration20260521175027
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=Migration20260521175027.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customers/migrations/Migration20260521175027.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\n/**\n * Idempotent backstop for `customer_person_company_links.deleted_at`.\n *\n * The column was originally added by `Migration20260415095203` and is\n * referenced by `Migration20260417140000`'s partial unique index\n * (`where \"deleted_at\" is null`). It is part of the committed\n * `migrations/.snapshot-open-mercato.json`.\n *\n * However, the runtime defensive probe that lived in\n * `packages/core/src/modules/customers/lib/personCompanyLinkTable.ts`\n * historically existed because an aborted-transaction state on a\n * shared connection could (very rarely) make `loadPersonCompanyLinks`\n * fail in a way that *looked* like the column was missing \u2014 and the\n * cached fallback then silently dropped the `deletedAt: null` filter.\n * That probe is removed in the same change as this migration (no\n * Kysely query is ever issued from inside an active MikroORM TX);\n * this migration is the matching belt-and-suspenders guarantee that\n * the column is physically present so the unconditional `deletedAt:\n * null` filter cannot 42703.\n *\n * Postgres-native `add column if not exists` is used so this re-applies\n * cleanly on already-migrated databases without raising.\n */\nexport class Migration20260521175027 extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`alter table \"customer_person_company_links\" add column if not exists \"deleted_at\" timestamptz null;`);\n }\n\n override async down(): Promise<void> {\n // No-op on the way down: dropping the column would break\n // `Migration20260417140000`'s partial unique index, which is the\n // authoritative owner of the column going forward. The original\n // `Migration20260415095203` down still drops it for full rollback.\n }\n\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAyBnB,MAAM,gCAAgC,UAAU;AAAA,EAErD,MAAe,KAAoB;AACjC,SAAK,OAAO,qGAAqG;AAAA,EACnH;AAAA,EAEA,MAAe,OAAsB;AAAA,EAKrC;AAEF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.3-develop.
|
|
3
|
+
"version": "0.6.3-develop.3593.1.8eda8943fa",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -242,16 +242,16 @@
|
|
|
242
242
|
"zod": "^4.4.3"
|
|
243
243
|
},
|
|
244
244
|
"peerDependencies": {
|
|
245
|
-
"@open-mercato/ai-assistant": "0.6.3-develop.
|
|
246
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
247
|
-
"@open-mercato/ui": "0.6.3-develop.
|
|
245
|
+
"@open-mercato/ai-assistant": "0.6.3-develop.3593.1.8eda8943fa",
|
|
246
|
+
"@open-mercato/shared": "0.6.3-develop.3593.1.8eda8943fa",
|
|
247
|
+
"@open-mercato/ui": "0.6.3-develop.3593.1.8eda8943fa",
|
|
248
248
|
"react": "^19.0.0",
|
|
249
249
|
"react-dom": "^19.0.0"
|
|
250
250
|
},
|
|
251
251
|
"devDependencies": {
|
|
252
|
-
"@open-mercato/ai-assistant": "0.6.3-develop.
|
|
253
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
254
|
-
"@open-mercato/ui": "0.6.3-develop.
|
|
252
|
+
"@open-mercato/ai-assistant": "0.6.3-develop.3593.1.8eda8943fa",
|
|
253
|
+
"@open-mercato/shared": "0.6.3-develop.3593.1.8eda8943fa",
|
|
254
|
+
"@open-mercato/ui": "0.6.3-develop.3593.1.8eda8943fa",
|
|
255
255
|
"@testing-library/dom": "^10.4.1",
|
|
256
256
|
"@testing-library/jest-dom": "^6.9.1",
|
|
257
257
|
"@testing-library/react": "^16.3.1",
|
|
@@ -1,54 +1,46 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
-
import { sql } from '@mikro-orm/postgresql'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// The previous implementation probed `information_schema.columns` on every
|
|
4
|
+
// first call via `em.getKysely()` to decide whether to add a `deletedAt: null`
|
|
5
|
+
// filter. When that probe ran inside an already-active MikroORM transaction it
|
|
6
|
+
// shared the transaction's connection — and any failure (including transient
|
|
7
|
+
// connection-pool state, an aborted TX from a sibling query, or pg returning
|
|
8
|
+
// 25P02) propagated back into the host transaction, poisoning every following
|
|
9
|
+
// query with `current transaction is aborted, commands ignored until end of
|
|
10
|
+
// transaction block`. That is what produced the standalone-CI failure burst
|
|
11
|
+
// (loadPersonCompanyLinks → 25P02 → customers.people.create → 500) and the
|
|
12
|
+
// sync_excel worker `failedCount: 1` regression.
|
|
13
|
+
//
|
|
14
|
+
// The `deleted_at` column on `customer_person_company_links` has been part of
|
|
15
|
+
// the committed module snapshot since `Migration20260415095203` (April 2026)
|
|
16
|
+
// and is referenced by `Migration20260417140000` (partial unique index uses
|
|
17
|
+
// `where deleted_at is null`). Any environment running these migrations
|
|
18
|
+
// already has the column. So the probe is no longer load-bearing — it only
|
|
19
|
+
// adds risk. We now unconditionally include `deletedAt: null` in the filter.
|
|
20
|
+
// If a downstream DB is genuinely un-migrated, the underlying `em.find` will
|
|
21
|
+
// surface the authentic `column "deleted_at" of relation
|
|
22
|
+
// "customer_person_company_links" does not exist` (pg 42703) pointing the
|
|
23
|
+
// operator at the real fix (`yarn db:migrate`).
|
|
24
|
+
//
|
|
25
|
+
// `customerPersonCompanyLinksSupportDeletedAt` and
|
|
26
|
+
// `warnMissingCustomerPersonCompanyLinksDeletedAt` are kept as exported
|
|
27
|
+
// no-op-equivalents so external callers (and the published @open-mercato/core
|
|
28
|
+
// API surface) don't break.
|
|
6
29
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export async function customerPersonCompanyLinksSupportDeletedAt(em: EntityManager): Promise<boolean> {
|
|
11
|
-
if (typeof (em as { getKysely?: unknown }).getKysely !== 'function') {
|
|
12
|
-
return true
|
|
13
|
-
}
|
|
14
|
-
if (!supportsDeletedAtColumnPromise) {
|
|
15
|
-
const db = em.getKysely<any>() as any
|
|
16
|
-
const probe: Promise<boolean> = db
|
|
17
|
-
.selectFrom('information_schema.columns')
|
|
18
|
-
.select(['column_name'])
|
|
19
|
-
.where(sql<boolean>`table_schema = current_schema()`)
|
|
20
|
-
.where('table_name', '=', PERSON_COMPANY_LINKS_TABLE)
|
|
21
|
-
.where('column_name', '=', PERSON_COMPANY_LINKS_DELETED_AT_COLUMN)
|
|
22
|
-
.executeTakeFirst()
|
|
23
|
-
.then((row: unknown) => !!row)
|
|
24
|
-
.catch(() => false)
|
|
25
|
-
supportsDeletedAtColumnPromise = probe
|
|
26
|
-
return probe
|
|
27
|
-
}
|
|
28
|
-
return supportsDeletedAtColumnPromise
|
|
30
|
+
export async function customerPersonCompanyLinksSupportDeletedAt(_em: EntityManager): Promise<boolean> {
|
|
31
|
+
return true
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
export function warnMissingCustomerPersonCompanyLinksDeletedAt(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
warnedAboutMissingDeletedAtColumn = true
|
|
36
|
-
console.warn(
|
|
37
|
-
`[${source}] missing ${PERSON_COMPANY_LINKS_TABLE}.${PERSON_COMPANY_LINKS_DELETED_AT_COLUMN}; ` +
|
|
38
|
-
'continuing without link soft-delete filtering. Run yarn db:migrate.',
|
|
39
|
-
)
|
|
34
|
+
export function warnMissingCustomerPersonCompanyLinksDeletedAt(_source: string): void {
|
|
35
|
+
// No-op: column is guaranteed present by Migration20260415095203 and the
|
|
36
|
+
// module snapshot. Kept for backward-compatible exports.
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
export async function withActiveCustomerPersonCompanyLinkFilter<T extends Record<string, unknown>>(
|
|
43
|
-
|
|
40
|
+
_em: EntityManager,
|
|
44
41
|
where: T,
|
|
45
|
-
|
|
42
|
+
_source: string,
|
|
46
43
|
): Promise<T & { deletedAt?: null }> {
|
|
47
|
-
const supportsDeletedAt = await customerPersonCompanyLinksSupportDeletedAt(em)
|
|
48
|
-
if (!supportsDeletedAt) {
|
|
49
|
-
warnMissingCustomerPersonCompanyLinksDeletedAt(source)
|
|
50
|
-
return { ...where }
|
|
51
|
-
}
|
|
52
44
|
return { ...where, deletedAt: null }
|
|
53
45
|
}
|
|
54
46
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Idempotent backstop for `customer_person_company_links.deleted_at`.
|
|
5
|
+
*
|
|
6
|
+
* The column was originally added by `Migration20260415095203` and is
|
|
7
|
+
* referenced by `Migration20260417140000`'s partial unique index
|
|
8
|
+
* (`where "deleted_at" is null`). It is part of the committed
|
|
9
|
+
* `migrations/.snapshot-open-mercato.json`.
|
|
10
|
+
*
|
|
11
|
+
* However, the runtime defensive probe that lived in
|
|
12
|
+
* `packages/core/src/modules/customers/lib/personCompanyLinkTable.ts`
|
|
13
|
+
* historically existed because an aborted-transaction state on a
|
|
14
|
+
* shared connection could (very rarely) make `loadPersonCompanyLinks`
|
|
15
|
+
* fail in a way that *looked* like the column was missing — and the
|
|
16
|
+
* cached fallback then silently dropped the `deletedAt: null` filter.
|
|
17
|
+
* That probe is removed in the same change as this migration (no
|
|
18
|
+
* Kysely query is ever issued from inside an active MikroORM TX);
|
|
19
|
+
* this migration is the matching belt-and-suspenders guarantee that
|
|
20
|
+
* the column is physically present so the unconditional `deletedAt:
|
|
21
|
+
* null` filter cannot 42703.
|
|
22
|
+
*
|
|
23
|
+
* Postgres-native `add column if not exists` is used so this re-applies
|
|
24
|
+
* cleanly on already-migrated databases without raising.
|
|
25
|
+
*/
|
|
26
|
+
export class Migration20260521175027 extends Migration {
|
|
27
|
+
|
|
28
|
+
override async up(): Promise<void> {
|
|
29
|
+
this.addSql(`alter table "customer_person_company_links" add column if not exists "deleted_at" timestamptz null;`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override async down(): Promise<void> {
|
|
33
|
+
// No-op on the way down: dropping the column would break
|
|
34
|
+
// `Migration20260417140000`'s partial unique index, which is the
|
|
35
|
+
// authoritative owner of the column going forward. The original
|
|
36
|
+
// `Migration20260415095203` down still drops it for full rollback.
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|