@objectstack/metadata 17.2.0 → 17.3.0
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/CHANGELOG.md +1435 -0
- package/dist/errors.cjs +1 -85
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +1 -22
- package/dist/errors.d.ts +1 -22
- package/dist/errors.js +2 -84
- package/dist/errors.js.map +1 -1
- package/dist/index.cjs +718 -269
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +327 -27
- package/dist/index.d.ts +327 -27
- package/dist/index.js +722 -267
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.cjs +72 -46
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +57 -14
- package/dist/migrations/index.d.ts +57 -14
- package/dist/migrations/index.js +72 -46
- package/dist/migrations/index.js.map +1 -1
- package/dist/node.cjs +718 -269
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +133 -3
- package/dist/node.d.ts +133 -3
- package/dist/node.js +722 -267
- package/dist/node.js.map +1 -1
- package/package.json +42 -21
package/dist/errors.cjs
CHANGED
|
@@ -20,94 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/errors.ts
|
|
21
21
|
var errors_exports = {};
|
|
22
22
|
__export(errors_exports, {
|
|
23
|
-
isMissingTableError: () => isMissingTableError
|
|
23
|
+
isMissingTableError: () => import_types.isMissingTableError
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(errors_exports);
|
|
26
|
-
|
|
27
|
-
// src/utils/schema-sync-errors.ts
|
|
28
26
|
var import_types = require("@objectstack/types");
|
|
29
|
-
var MISSING_TABLE = {
|
|
30
|
-
codes: /* @__PURE__ */ new Set([
|
|
31
|
-
"42P01",
|
|
32
|
-
// PostgreSQL undefined_table
|
|
33
|
-
"ER_NO_SUCH_TABLE"
|
|
34
|
-
// MySQL / MariaDB 1146
|
|
35
|
-
]),
|
|
36
|
-
errnos: /* @__PURE__ */ new Set([1146]),
|
|
37
|
-
/**
|
|
38
|
-
* - SQLite / libsql: `no such table: sys_metadata_history`
|
|
39
|
-
* - PostgreSQL: `relation "sys_metadata_history" does not exist`
|
|
40
|
-
* - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
|
|
41
|
-
*/
|
|
42
|
-
message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i,
|
|
43
|
-
excludes: {
|
|
44
|
-
/**
|
|
45
|
-
* Exactly the three SQLSTATEs the docblock above already names as
|
|
46
|
-
* must-stay-loud neighbours of `does not exist`. They are listed here
|
|
47
|
-
* rather than merely trusted to miss the message test, because two of
|
|
48
|
-
* them (42703 columns, 42704 constraints/triggers) have a phrasing that
|
|
49
|
-
* *does* hit it, and because a code is a fact where prose is a guess.
|
|
50
|
-
*
|
|
51
|
-
* Postgres-shaped on purpose: measured, neither MySQL
|
|
52
|
-
* (`Unknown column 'label' in 'field list'`) nor SQLite
|
|
53
|
-
* (`no such column: bogus`, `table t has no column named label`)
|
|
54
|
-
* phrases a sub-object failure so that a missing-table phrase falls out
|
|
55
|
-
* of it, so there is nothing there to exclude. Adding their codes would
|
|
56
|
-
* be surface with no defect behind it.
|
|
57
|
-
*/
|
|
58
|
-
codes: /* @__PURE__ */ new Set([
|
|
59
|
-
"42703",
|
|
60
|
-
// undefined_column
|
|
61
|
-
"42704",
|
|
62
|
-
// undefined_object — constraint, trigger, role, type, …
|
|
63
|
-
"3D000"
|
|
64
|
-
// invalid_catalog_name — `database "x" does not exist`
|
|
65
|
-
]),
|
|
66
|
-
/**
|
|
67
|
-
* `«sub-object» "x" of relation "y" …` — Postgres' phrasing for a
|
|
68
|
-
* failure about something *inside* a relation, which therefore says the
|
|
69
|
-
* relation itself is present. The two in-repo siblings that carry this
|
|
70
|
-
* phrase are `mapDataError` (`packages/rest`, #5352) and
|
|
71
|
-
* `service-analytics`'s missing-column subtraction (#6035/PR #6346).
|
|
72
|
-
*
|
|
73
|
-
* [#6615] All three now read one home — `@objectstack/types` — instead
|
|
74
|
-
* of three hand-kept copies, so the phrase can no longer be taught to
|
|
75
|
-
* the repo a fourth time or drift in one package only. The **width**
|
|
76
|
-
* difference that used to justify the copy is preserved and is the
|
|
77
|
-
* reason the home exports two functions rather than one: those two
|
|
78
|
-
* *extract* the column name to phrase a better error, so a miss costs a
|
|
79
|
-
* vaguer message; this one *excludes*, so a miss restores the
|
|
80
|
-
* corruption. {@link isRelationSubObjectPhrase} is therefore the wider
|
|
81
|
-
* question — it drops their `column`/`[a-z0-9_]+`/`does not exist`
|
|
82
|
-
* anchors: any sub-object, any quoted identifier, any verdict.
|
|
83
|
-
* Over-matching here only ever converts a benign verdict into a loud
|
|
84
|
-
* one, which is the direction this whole module already errs in.
|
|
85
|
-
*/
|
|
86
|
-
matchesMessage: import_types.isRelationSubObjectPhrase
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
var MAX_CAUSE_DEPTH = 4;
|
|
90
|
-
function matchesDriverError(error, signature, depth) {
|
|
91
|
-
if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
|
|
92
|
-
if (typeof error === "string") {
|
|
93
|
-
if (signature.excludes?.matchesMessage(error)) return false;
|
|
94
|
-
return signature.message.test(error);
|
|
95
|
-
}
|
|
96
|
-
if (typeof error !== "object") return false;
|
|
97
|
-
const err = error;
|
|
98
|
-
const excludes = signature.excludes;
|
|
99
|
-
if (excludes) {
|
|
100
|
-
if (typeof err.code === "string" && excludes.codes.has(err.code)) return false;
|
|
101
|
-
if (typeof err.message === "string" && excludes.matchesMessage(err.message)) return false;
|
|
102
|
-
}
|
|
103
|
-
if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
|
|
104
|
-
if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
|
|
105
|
-
if (typeof err.message === "string" && signature.message.test(err.message)) return true;
|
|
106
|
-
return matchesDriverError(err.cause, signature, depth + 1);
|
|
107
|
-
}
|
|
108
|
-
function isMissingTableError(error, depth = 0) {
|
|
109
|
-
return matchesDriverError(error, MISSING_TABLE, depth);
|
|
110
|
-
}
|
|
111
27
|
// Annotate the CommonJS export names for ESM import in node:
|
|
112
28
|
0 && (module.exports = {
|
|
113
29
|
isMissingTableError
|
package/dist/errors.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/utils/schema-sync-errors.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `@objectstack/metadata/errors` — the shared driver-error discriminators for\n * the metadata storage seams (#4728 / #4825 / #4867 family).\n *\n * ## Why this subpath exists\n *\n * The \"which driver failures may be silenced?\" question is not local to one\n * package. It was answered first for DDL in `@objectstack/metadata`\n * (`ensureSchema`, #4728), then for reads on the legacy `DatabaseLoader` path\n * (`nextEventSeq`, #4825) — and the *canonical* transactional producer of the\n * very same numbers, `SysMetadataRepository`, lives in a different package\n * (`@objectstack/metadata-protocol`, #4867) and carried the identical defect.\n *\n * Three ways to serve that second package were considered; the third is the\n * one taken, and the first is the one this module exists to prevent:\n *\n * 1. **Copy the predicate.** Rejected. Two hand-rolled vocabularies of\n * \"benign driver error\" is precisely the dual-source debt #4825 killed:\n * a driver quirk taught to one copy and not the other produces two\n * packages that disagree about whether data may be silently invented.\n * 2. **Sink it into a common dependency** (`@objectstack/types`,\n * `@objectstack/spec/shared`). Architecturally attractive and explicitly\n * *not* precluded by this module — but out of scope on the round that\n * needed it (spec was frozen; types was under concurrent change).\n * 3. **Export it deliberately from its current home** — this file. One\n * declaration, one implementation, one place a new driver quirk is taught.\n *\n * ## Why a subpath and not the package entry\n *\n * `@objectstack/metadata`'s root entry pulls the manager, every loader and the\n * YAML/filesystem machinery behind them. A consumer that wants a 40-line\n * predicate should not have to load any of that, and the weight is exactly\n * what would tempt the next author back to option 1. This entry re-exports\n * one leaf module and nothing else, so the cross-package edge stays a leaf\n * edge — and stays a single, greppable seam to delete if the maintainer later\n * takes option 2.\n *\n * ## Scope of the promise\n *\n * Only {@link isMissingTableError} is exported: it has a cross-package\n * consumer today. Its sibling `isSchemaAlreadyExistsError` deliberately stays\n * internal to this package — it has no consumer outside it, and an exported\n * symbol nobody imports is a promise made for nothing (Prime Directive #10,\n * pointed at our own API surface). Add it here the day something outside\n * `@objectstack/metadata` needs it, not before.\n */\n\nexport { isMissingTableError } from './utils/schema-sync-errors.js';\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Driver-error classification for the metadata storage seams (#4728, #4825;\n * rule from #4632).\n *\n * Two questions live here, and they share one mechanism on purpose. A second\n * hand-rolled `catch`-and-guess elsewhere in this package would be a second\n * de-facto vocabulary of \"which driver errors are benign\" — the exact debt this\n * module exists to retire. Both predicates below are thin wrappers over one\n * signature matcher, so a driver quirk is taught to the package once.\n *\n * 1. {@link isSchemaAlreadyExistsError} — \"was this DDL failure just the table\n * already being there?\" (#4728, `ensureSchema` / `ensureHistorySchema`).\n * 2. {@link isMissingTableError} — \"did this READ fail because the table has\n * not been provisioned yet?\" (#4825, `nextEventSeq`).\n *\n * They are deliberately **not** each other's negation. Each answers \"is this\n * the one benign reason?\" and defaults to *not benign*, so an error neither\n * recognises is loud under both.\n *\n * ---\n *\n * ## 1. DDL failure classification (#4728)\n *\n * `IDataDriver.syncSchema()` is contractually **idempotent** (\"creates tables if\n * missing, adds columns, updates indexes\"), so in principle a re-sync of an\n * existing table should not throw at all. In practice a driver may surface the\n * already-provisioned case as an error instead of a no-op — `CREATE TABLE`\n * without `IF NOT EXISTS`, an `ALTER TABLE ADD COLUMN` for a column that is\n * already there. That single failure reason is benign: the table and its columns\n * exist, so the bytes will land.\n *\n * **Every other** DDL failure is not benign, and the difference is the whole\n * point of this module. Insufficient privileges, a datasource that never\n * connected, an incompatible column type — after those, the table or column does\n * not exist, yet the process keeps looking healthy while everything it claims to\n * persist has nowhere to land. That is the #4420 shape, and AGENTS.md →\n * \"Degradation log levels\" requires it to be reported at `error`.\n *\n * The defect this replaces was a `catch` whose comment named the benign reason\n * (\"e.g. table already exists\") and used it to excuse **all** of them. Callers\n * must therefore ask the question by error *type*:\n *\n * ```ts\n * catch (error) {\n * if (!isSchemaAlreadyExistsError(error)) {\n * console.error('… consequence … fix …', error); // loud, and stay not-ready\n * return;\n * }\n * // benign only: the table is already provisioned, carry on\n * }\n * ```\n *\n * Classification is deliberately conservative — anything not positively\n * recognised as \"already exists\" is treated as a real failure, because the cost\n * of a false \"benign\" (silent data loss) is far higher than the cost of a false\n * \"real\" (one extra error line).\n *\n * ---\n *\n * ## 2. Missing-table classification for reads (#4825)\n *\n * `DatabaseLoader.nextEventSeq()` reads `sys_metadata_history` to decide what\n * `event_seq` the NEXT history row gets. Its `catch` named both reasons a read\n * can fail — \"table not provisioned yet\" (benign: 1 really is the next number)\n * and \"driver error\" (**not** benign) — and answered both with `return 1`.\n *\n * That is the #4728 shape one layer down, but the damage is the opposite kind\n * and worse. #4728 was *bytes that never landed*; this is **bytes that land\n * wrong**: with N rows already in the table, one flaky read hands the next row\n * `event_seq = 1`, colliding with an existing row. The insert **succeeds**, no\n * line is logged, and `event_seq` — the ordering key that history listing and\n * rollback targeting both stand on — is now silently untrustworthy.\n *\n * So the read seam gets the same treatment, with the same conservative default:\n *\n * ```ts\n * catch (error) {\n * if (isMissingTableError(error)) return 1; // benign: nothing to collide with\n * throw error; // caller reports the consequence\n * }\n * ```\n */\n\n// [#6615] The Postgres `\"x\" of relation \"y\"` phrase, owned once — see the\n// module docblock in `@objectstack/types` for the superstring hole it closes\n// and for why the exclusion's width deliberately differs from the extractor's.\nimport { isRelationSubObjectPhrase } from '@objectstack/types';\n\n/** One \"which errors mean X?\" vocabulary, in the three forms drivers use. */\ninterface DriverErrorSignature {\n /** `error.code` — Postgres SQLSTATE, or mysql2's symbolic name. */\n readonly codes: ReadonlySet<string>;\n /** `error.errno` — MySQL/MariaDB numeric equivalents. */\n readonly errnos: ReadonlySet<number>;\n /** `error.message` — the only signal SQLite-family drivers give. */\n readonly message: RegExp;\n /**\n * Optional **front-exclusion**, evaluated before any positive test (#6347).\n *\n * A message test can never exclude a *superstring*: once a legal phrase for\n * X appears inside a longer phrase that means NOT-X, no amount of widening\n * the X regex removes the match — the phrase really is in there. The only\n * repair is to recognise the not-X shape first and stop. So this is a\n * separate channel rather than another alternation in {@link message}.\n */\n readonly excludes?: {\n /** SQLSTATEs / driver codes that positively mean \"**not** this case\". */\n readonly codes: ReadonlySet<string>;\n /**\n * Message shapes that carry a legal match for this case as a substring.\n *\n * A predicate rather than a `RegExp` since #6615, so this channel can be\n * satisfied by a shared, named question from `@objectstack/types` instead\n * of a pattern this file owns alone. The phrase it tests is the same one\n * `@objectstack/rest` and `@objectstack/service-analytics` read.\n */\n readonly matchesMessage: (message: string) => boolean;\n };\n}\n\n/**\n * Driver/SQLSTATE codes that mean \"the thing you asked me to create is already\n * there\". Postgres reports SQLSTATE on `code`; mysql2 reports its symbolic name.\n */\nconst ALREADY_EXISTS: DriverErrorSignature = {\n codes: new Set([\n // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)\n '42P07', // duplicate_table\n '42701', // duplicate_column\n '42710', // duplicate_object — index / constraint already exists\n // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)\n 'ER_TABLE_EXISTS_ERROR', // 1050\n 'ER_DUP_FIELDNAME', // 1060\n 'ER_DUP_KEYNAME', // 1061\n ]),\n errnos: new Set([1050, 1060, 1061]),\n /**\n * Message fallback for drivers that carry no machine-readable code —\n * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for\n * every DDL failure, so the message is the only signal available:\n * - `table sys_metadata already exists`\n * - `duplicate column name: environment_id`\n * - `index idx_x already exists`\n * Postgres phrases its own as `relation \"x\" already exists` /\n * `column \"x\" of relation \"y\" already exists`, which matches the same test.\n */\n message: /already exists|duplicate column name|duplicate key name/i,\n};\n\n/**\n * Codes/messages that mean \"the table you tried to READ has not been created\".\n *\n * Narrower than it looks, on purpose. `does not exist` on its own also covers\n * `role \"x\" does not exist` (42704), `database \"x\" does not exist` (3D000) and\n * `column \"x\" does not exist` (42703) — every one of them a **real** failure\n * that must stay loud, and every one of them a case where \"start numbering at\n * 1\" would be the wrong answer against a table that may be full of rows. So the\n * message test demands the word table/relation next to the phrase rather than\n * the phrase alone, and the code set carries only the table-scoped SQLSTATEs.\n *\n * That was not enough on its own, and #6347 is why. Postgres has **two**\n * missing-column phrasings, one per direction:\n *\n * | path | phrase | SQLSTATE | matched the message test? |\n * |:---|:---|:---|:---|\n * | read (`SELECT`) | `column \"bogus\" does not exist` | 42703 | no |\n * | write (`INSERT`/`UPDATE`/`ALTER`) | `column \"label\" of relation \"sys_team\" does not exist` | 42703 | **yes** |\n *\n * The write-path phrase contains a complete, legal missing-table phrase —\n * `relation \"sys_team\" does not exist` — as a substring, so the table-scoped\n * test above matched it and answered *benign* about an error the docblock two\n * paragraphs up already named as one that must stay loud. The same holds for\n * every other sub-object of a relation Postgres phrases this way, e.g.\n * `constraint \"uq_x\" of relation \"sys_team\" does not exist` (42704). And\n * code-first does not rescue it: {@link matchesDriverError} is a sequential OR,\n * so a `code: '42703'` error simply falls past the two code lines and is\n * decided by the message.\n *\n * Hence {@link DriverErrorSignature.excludes}: the not-a-table shapes are\n * recognised FIRST, and recognition ends the question with `false`.\n */\nconst MISSING_TABLE: DriverErrorSignature = {\n codes: new Set([\n '42P01', // PostgreSQL undefined_table\n 'ER_NO_SUCH_TABLE', // MySQL / MariaDB 1146\n ]),\n errnos: new Set([1146]),\n /**\n * - SQLite / libsql: `no such table: sys_metadata_history`\n * - PostgreSQL: `relation \"sys_metadata_history\" does not exist`\n * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`\n */\n message:\n /no such table|relation [\"'`][^\"'`]+[\"'`] does not exist|table [\"'`][^\"'`]+[\"'`] doesn'?t exist|unknown table/i,\n excludes: {\n /**\n * Exactly the three SQLSTATEs the docblock above already names as\n * must-stay-loud neighbours of `does not exist`. They are listed here\n * rather than merely trusted to miss the message test, because two of\n * them (42703 columns, 42704 constraints/triggers) have a phrasing that\n * *does* hit it, and because a code is a fact where prose is a guess.\n *\n * Postgres-shaped on purpose: measured, neither MySQL\n * (`Unknown column 'label' in 'field list'`) nor SQLite\n * (`no such column: bogus`, `table t has no column named label`)\n * phrases a sub-object failure so that a missing-table phrase falls out\n * of it, so there is nothing there to exclude. Adding their codes would\n * be surface with no defect behind it.\n */\n codes: new Set([\n '42703', // undefined_column\n '42704', // undefined_object — constraint, trigger, role, type, …\n '3D000', // invalid_catalog_name — `database \"x\" does not exist`\n ]),\n /**\n * `«sub-object» \"x\" of relation \"y\" …` — Postgres' phrasing for a\n * failure about something *inside* a relation, which therefore says the\n * relation itself is present. The two in-repo siblings that carry this\n * phrase are `mapDataError` (`packages/rest`, #5352) and\n * `service-analytics`'s missing-column subtraction (#6035/PR #6346).\n *\n * [#6615] All three now read one home — `@objectstack/types` — instead\n * of three hand-kept copies, so the phrase can no longer be taught to\n * the repo a fourth time or drift in one package only. The **width**\n * difference that used to justify the copy is preserved and is the\n * reason the home exports two functions rather than one: those two\n * *extract* the column name to phrase a better error, so a miss costs a\n * vaguer message; this one *excludes*, so a miss restores the\n * corruption. {@link isRelationSubObjectPhrase} is therefore the wider\n * question — it drops their `column`/`[a-z0-9_]+`/`does not exist`\n * anchors: any sub-object, any quoted identifier, any verdict.\n * Over-matching here only ever converts a benign verdict into a loud\n * one, which is the direction this whole module already errs in.\n */\n matchesMessage: isRelationSubObjectPhrase,\n },\n};\n\n/** How far to follow an `error.cause` chain — drivers wrap, but not deeply. */\nconst MAX_CAUSE_DEPTH = 4;\n\n/**\n * The single matcher both predicates run on: exclusions, then code, then errno,\n * then message, then one step down the `cause` chain.\n *\n * Unrecognised is always `false` — a benign verdict must be *earned*, never\n * defaulted to, because a false \"benign\" corrupts data while a false \"real\"\n * costs one error line.\n *\n * The exclusion runs at every node and, when it fires, returns `false` **without\n * descending into `cause`** (#6347). Two reasons, both the conservative\n * direction: an error that positively identifies as \"a column of an existing\n * relation\" *is* that error, whatever it wraps; and stopping can only ever\n * subtract benign verdicts, never add one.\n */\nfunction matchesDriverError(\n error: unknown,\n signature: DriverErrorSignature,\n depth: number,\n): boolean {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return false;\n\n if (typeof error === 'string') {\n if (signature.excludes?.matchesMessage(error)) return false;\n return signature.message.test(error);\n }\n if (typeof error !== 'object') return false;\n\n const err = error as {\n code?: unknown;\n errno?: unknown;\n message?: unknown;\n cause?: unknown;\n };\n\n const excludes = signature.excludes;\n if (excludes) {\n if (typeof err.code === 'string' && excludes.codes.has(err.code)) return false;\n if (typeof err.message === 'string' && excludes.matchesMessage(err.message)) return false;\n }\n\n if (typeof err.code === 'string' && signature.codes.has(err.code)) return true;\n if (typeof err.errno === 'number' && signature.errnos.has(err.errno)) return true;\n if (typeof err.message === 'string' && signature.message.test(err.message)) return true;\n\n // Drivers commonly re-throw with the original attached as `cause`.\n return matchesDriverError(err.cause, signature, depth + 1);\n}\n\n/**\n * Is this DDL error the benign \"already provisioned\" case?\n *\n * @param error - The value thrown by `syncSchema()` (or any DDL call).\n * @param depth - Internal `cause`-chain recursion counter; callers pass nothing.\n * @returns `true` only when the error positively identifies as\n * table/column/index-already-exists. Anything else — including an\n * unrecognised error, `undefined`, or a permission/connection failure —\n * returns `false` and MUST be reported loudly by the caller.\n */\nexport function isSchemaAlreadyExistsError(error: unknown, depth = 0): boolean {\n return matchesDriverError(error, ALREADY_EXISTS, depth);\n}\n\n/**\n * Is this READ error the benign \"table has not been provisioned yet\" case?\n *\n * The only failure that licenses a caller to treat an empty table as the truth\n * — there are no rows, so there is nothing to be inconsistent with. A\n * connection drop, a timeout, a permission denial or a query error all mean the\n * rows may well exist and simply were not seen; those return `false` and the\n * caller must report the consequence and give up rather than compute an answer\n * from data it never read (#4825).\n *\n * A failure about a **column** of a relation is never this case, in either of\n * Postgres' two phrasings — the relation is right there in the message because\n * it exists (#6347). See {@link MISSING_TABLE}'s `excludes`.\n *\n * @param error - The value thrown by a driver/engine read (`find`, `findOne`, …).\n * @param depth - Internal `cause`-chain recursion counter; callers pass nothing.\n * @returns `true` only when the error positively identifies as\n * table/relation-does-not-exist.\n */\nexport function isMissingTableError(error: unknown, depth = 0): boolean {\n return matchesDriverError(error, MISSING_TABLE, depth);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACwFA,mBAA0C;AA+F1C,IAAM,gBAAsC;AAAA,EACxC,OAAO,oBAAI,IAAI;AAAA,IACX;AAAA;AAAA,IACA;AAAA;AAAA,EACJ,CAAC;AAAA,EACD,QAAQ,oBAAI,IAAI,CAAC,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtB,SACI;AAAA,EACJ,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAeN,OAAO,oBAAI,IAAI;AAAA,MACX;AAAA;AAAA,MACA;AAAA;AAAA,MACA;AAAA;AAAA,IACJ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAqBD,gBAAgB;AAAA,EACpB;AACJ;AAGA,IAAM,kBAAkB;AAgBxB,SAAS,mBACL,OACA,WACA,OACO;AACP,MAAI,UAAU,QAAQ,UAAU,UAAa,QAAQ,gBAAiB,QAAO;AAE7E,MAAI,OAAO,UAAU,UAAU;AAC3B,QAAI,UAAU,UAAU,eAAe,KAAK,EAAG,QAAO;AACtD,WAAO,UAAU,QAAQ,KAAK,KAAK;AAAA,EACvC;AACA,MAAI,OAAO,UAAU,SAAU,QAAO;AAEtC,QAAM,MAAM;AAOZ,QAAM,WAAW,UAAU;AAC3B,MAAI,UAAU;AACV,QAAI,OAAO,IAAI,SAAS,YAAY,SAAS,MAAM,IAAI,IAAI,IAAI,EAAG,QAAO;AACzE,QAAI,OAAO,IAAI,YAAY,YAAY,SAAS,eAAe,IAAI,OAAO,EAAG,QAAO;AAAA,EACxF;AAEA,MAAI,OAAO,IAAI,SAAS,YAAY,UAAU,MAAM,IAAI,IAAI,IAAI,EAAG,QAAO;AAC1E,MAAI,OAAO,IAAI,UAAU,YAAY,UAAU,OAAO,IAAI,IAAI,KAAK,EAAG,QAAO;AAC7E,MAAI,OAAO,IAAI,YAAY,YAAY,UAAU,QAAQ,KAAK,IAAI,OAAO,EAAG,QAAO;AAGnF,SAAO,mBAAmB,IAAI,OAAO,WAAW,QAAQ,CAAC;AAC7D;AAmCO,SAAS,oBAAoB,OAAgB,QAAQ,GAAY;AACpE,SAAO,mBAAmB,OAAO,eAAe,KAAK;AACzD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `@objectstack/metadata/errors` — the shared driver-error discriminators for\n * the metadata storage seams (#4728 / #4825 / #4867 family).\n *\n * ## Why this subpath exists\n *\n * The \"which driver failures may be silenced?\" question is not local to one\n * package. It was answered first for DDL in `@objectstack/metadata`\n * (`ensureSchema`, #4728), then for reads on the legacy `DatabaseLoader` path\n * (`nextEventSeq`, #4825) — and the *canonical* transactional producer of the\n * very same numbers, `SysMetadataRepository`, lives in a different package\n * (`@objectstack/metadata-protocol`, #4867) and carried the identical defect.\n *\n * Three ways to serve that second package were considered; the third is the\n * one taken, and the first is the one this module exists to prevent:\n *\n * 1. **Copy the predicate.** Rejected. Two hand-rolled vocabularies of\n * \"benign driver error\" is precisely the dual-source debt #4825 killed:\n * a driver quirk taught to one copy and not the other produces two\n * packages that disagree about whether data may be silently invented.\n * 2. **Sink it into a common dependency** (`@objectstack/types`,\n * `@objectstack/spec/shared`). Architecturally attractive and explicitly\n * *not* precluded by this module — but out of scope on the round that\n * needed it (spec was frozen; types was under concurrent change).\n * ⇒ **TAKEN, by the maintainer's 2026-08-30 ruling on #13279.** The\n * predicate now lives in `@objectstack/types`\n * (`driver-error-classification.ts`); what forced it was a consumer this\n * file could never serve — `resolveAuthzContext` in `@objectstack/core`,\n * which metadata **depends on**, so the edge could not point that way.\n * 3. **Export it deliberately from its current home** — this file. One\n * declaration, one implementation, one place a new driver quirk is taught.\n *\n * ## What this file is now\n *\n * Option 2 is taken, so this is the compatibility seam it always said it would\n * become — \"a single, greppable seam to delete if the maintainer later takes\n * option 2\". It is NOT deleted: `@objectstack/metadata/errors` is a published\n * subpath with out-of-repo consumers, and #13279 is a fix, not a removal. It\n * re-exports the one symbol it always exported, from the new home. Everything\n * below still describes why the subpath exists and why it stays narrow.\n *\n * ## Why a subpath and not the package entry\n *\n * `@objectstack/metadata`'s root entry pulls the manager, every loader and the\n * YAML/filesystem machinery behind them. A consumer that wants a 40-line\n * predicate should not have to load any of that, and the weight is exactly\n * what would tempt the next author back to option 1. This entry re-exports\n * one leaf module and nothing else, so the cross-package edge stays a leaf\n * edge — and stays a single, greppable seam to delete if the maintainer later\n * takes option 2.\n *\n * ## Scope of the promise\n *\n * Only {@link isMissingTableError} is re-exported HERE. Its sibling\n * `isSchemaAlreadyExistsError` moved to `@objectstack/types` with it (they are\n * two signatures over one matcher and cannot be separated without re-rolling\n * it), but it does not need a second door: nothing imports it through\n * `@objectstack/metadata/errors`, and an exported symbol nobody imports is a\n * promise made for nothing (Prime Directive #10, pointed at our own API\n * surface). Anything that needs it reads `@objectstack/types` directly.\n */\n\nexport { isMissingTableError } from '@objectstack/types';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgEA,mBAAoC;","names":[]}
|
package/dist/errors.d.cts
CHANGED
|
@@ -1,22 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Is this READ error the benign "table has not been provisioned yet" case?
|
|
3
|
-
*
|
|
4
|
-
* The only failure that licenses a caller to treat an empty table as the truth
|
|
5
|
-
* — there are no rows, so there is nothing to be inconsistent with. A
|
|
6
|
-
* connection drop, a timeout, a permission denial or a query error all mean the
|
|
7
|
-
* rows may well exist and simply were not seen; those return `false` and the
|
|
8
|
-
* caller must report the consequence and give up rather than compute an answer
|
|
9
|
-
* from data it never read (#4825).
|
|
10
|
-
*
|
|
11
|
-
* A failure about a **column** of a relation is never this case, in either of
|
|
12
|
-
* Postgres' two phrasings — the relation is right there in the message because
|
|
13
|
-
* it exists (#6347). See {@link MISSING_TABLE}'s `excludes`.
|
|
14
|
-
*
|
|
15
|
-
* @param error - The value thrown by a driver/engine read (`find`, `findOne`, …).
|
|
16
|
-
* @param depth - Internal `cause`-chain recursion counter; callers pass nothing.
|
|
17
|
-
* @returns `true` only when the error positively identifies as
|
|
18
|
-
* table/relation-does-not-exist.
|
|
19
|
-
*/
|
|
20
|
-
declare function isMissingTableError(error: unknown, depth?: number): boolean;
|
|
21
|
-
|
|
22
|
-
export { isMissingTableError };
|
|
1
|
+
export { isMissingTableError } from '@objectstack/types';
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,22 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Is this READ error the benign "table has not been provisioned yet" case?
|
|
3
|
-
*
|
|
4
|
-
* The only failure that licenses a caller to treat an empty table as the truth
|
|
5
|
-
* — there are no rows, so there is nothing to be inconsistent with. A
|
|
6
|
-
* connection drop, a timeout, a permission denial or a query error all mean the
|
|
7
|
-
* rows may well exist and simply were not seen; those return `false` and the
|
|
8
|
-
* caller must report the consequence and give up rather than compute an answer
|
|
9
|
-
* from data it never read (#4825).
|
|
10
|
-
*
|
|
11
|
-
* A failure about a **column** of a relation is never this case, in either of
|
|
12
|
-
* Postgres' two phrasings — the relation is right there in the message because
|
|
13
|
-
* it exists (#6347). See {@link MISSING_TABLE}'s `excludes`.
|
|
14
|
-
*
|
|
15
|
-
* @param error - The value thrown by a driver/engine read (`find`, `findOne`, …).
|
|
16
|
-
* @param depth - Internal `cause`-chain recursion counter; callers pass nothing.
|
|
17
|
-
* @returns `true` only when the error positively identifies as
|
|
18
|
-
* table/relation-does-not-exist.
|
|
19
|
-
*/
|
|
20
|
-
declare function isMissingTableError(error: unknown, depth?: number): boolean;
|
|
21
|
-
|
|
22
|
-
export { isMissingTableError };
|
|
1
|
+
export { isMissingTableError } from '@objectstack/types';
|
package/dist/errors.js
CHANGED
|
@@ -1,87 +1,5 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
var MISSING_TABLE = {
|
|
4
|
-
codes: /* @__PURE__ */ new Set([
|
|
5
|
-
"42P01",
|
|
6
|
-
// PostgreSQL undefined_table
|
|
7
|
-
"ER_NO_SUCH_TABLE"
|
|
8
|
-
// MySQL / MariaDB 1146
|
|
9
|
-
]),
|
|
10
|
-
errnos: /* @__PURE__ */ new Set([1146]),
|
|
11
|
-
/**
|
|
12
|
-
* - SQLite / libsql: `no such table: sys_metadata_history`
|
|
13
|
-
* - PostgreSQL: `relation "sys_metadata_history" does not exist`
|
|
14
|
-
* - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
|
|
15
|
-
*/
|
|
16
|
-
message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i,
|
|
17
|
-
excludes: {
|
|
18
|
-
/**
|
|
19
|
-
* Exactly the three SQLSTATEs the docblock above already names as
|
|
20
|
-
* must-stay-loud neighbours of `does not exist`. They are listed here
|
|
21
|
-
* rather than merely trusted to miss the message test, because two of
|
|
22
|
-
* them (42703 columns, 42704 constraints/triggers) have a phrasing that
|
|
23
|
-
* *does* hit it, and because a code is a fact where prose is a guess.
|
|
24
|
-
*
|
|
25
|
-
* Postgres-shaped on purpose: measured, neither MySQL
|
|
26
|
-
* (`Unknown column 'label' in 'field list'`) nor SQLite
|
|
27
|
-
* (`no such column: bogus`, `table t has no column named label`)
|
|
28
|
-
* phrases a sub-object failure so that a missing-table phrase falls out
|
|
29
|
-
* of it, so there is nothing there to exclude. Adding their codes would
|
|
30
|
-
* be surface with no defect behind it.
|
|
31
|
-
*/
|
|
32
|
-
codes: /* @__PURE__ */ new Set([
|
|
33
|
-
"42703",
|
|
34
|
-
// undefined_column
|
|
35
|
-
"42704",
|
|
36
|
-
// undefined_object — constraint, trigger, role, type, …
|
|
37
|
-
"3D000"
|
|
38
|
-
// invalid_catalog_name — `database "x" does not exist`
|
|
39
|
-
]),
|
|
40
|
-
/**
|
|
41
|
-
* `«sub-object» "x" of relation "y" …` — Postgres' phrasing for a
|
|
42
|
-
* failure about something *inside* a relation, which therefore says the
|
|
43
|
-
* relation itself is present. The two in-repo siblings that carry this
|
|
44
|
-
* phrase are `mapDataError` (`packages/rest`, #5352) and
|
|
45
|
-
* `service-analytics`'s missing-column subtraction (#6035/PR #6346).
|
|
46
|
-
*
|
|
47
|
-
* [#6615] All three now read one home — `@objectstack/types` — instead
|
|
48
|
-
* of three hand-kept copies, so the phrase can no longer be taught to
|
|
49
|
-
* the repo a fourth time or drift in one package only. The **width**
|
|
50
|
-
* difference that used to justify the copy is preserved and is the
|
|
51
|
-
* reason the home exports two functions rather than one: those two
|
|
52
|
-
* *extract* the column name to phrase a better error, so a miss costs a
|
|
53
|
-
* vaguer message; this one *excludes*, so a miss restores the
|
|
54
|
-
* corruption. {@link isRelationSubObjectPhrase} is therefore the wider
|
|
55
|
-
* question — it drops their `column`/`[a-z0-9_]+`/`does not exist`
|
|
56
|
-
* anchors: any sub-object, any quoted identifier, any verdict.
|
|
57
|
-
* Over-matching here only ever converts a benign verdict into a loud
|
|
58
|
-
* one, which is the direction this whole module already errs in.
|
|
59
|
-
*/
|
|
60
|
-
matchesMessage: isRelationSubObjectPhrase
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
var MAX_CAUSE_DEPTH = 4;
|
|
64
|
-
function matchesDriverError(error, signature, depth) {
|
|
65
|
-
if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
|
|
66
|
-
if (typeof error === "string") {
|
|
67
|
-
if (signature.excludes?.matchesMessage(error)) return false;
|
|
68
|
-
return signature.message.test(error);
|
|
69
|
-
}
|
|
70
|
-
if (typeof error !== "object") return false;
|
|
71
|
-
const err = error;
|
|
72
|
-
const excludes = signature.excludes;
|
|
73
|
-
if (excludes) {
|
|
74
|
-
if (typeof err.code === "string" && excludes.codes.has(err.code)) return false;
|
|
75
|
-
if (typeof err.message === "string" && excludes.matchesMessage(err.message)) return false;
|
|
76
|
-
}
|
|
77
|
-
if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
|
|
78
|
-
if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
|
|
79
|
-
if (typeof err.message === "string" && signature.message.test(err.message)) return true;
|
|
80
|
-
return matchesDriverError(err.cause, signature, depth + 1);
|
|
81
|
-
}
|
|
82
|
-
function isMissingTableError(error, depth = 0) {
|
|
83
|
-
return matchesDriverError(error, MISSING_TABLE, depth);
|
|
84
|
-
}
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
import { isMissingTableError } from "@objectstack/types";
|
|
85
3
|
export {
|
|
86
4
|
isMissingTableError
|
|
87
5
|
};
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/schema-sync-errors.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Driver-error classification for the metadata storage seams (#4728, #4825;\n * rule from #4632).\n *\n * Two questions live here, and they share one mechanism on purpose. A second\n * hand-rolled `catch`-and-guess elsewhere in this package would be a second\n * de-facto vocabulary of \"which driver errors are benign\" — the exact debt this\n * module exists to retire. Both predicates below are thin wrappers over one\n * signature matcher, so a driver quirk is taught to the package once.\n *\n * 1. {@link isSchemaAlreadyExistsError} — \"was this DDL failure just the table\n * already being there?\" (#4728, `ensureSchema` / `ensureHistorySchema`).\n * 2. {@link isMissingTableError} — \"did this READ fail because the table has\n * not been provisioned yet?\" (#4825, `nextEventSeq`).\n *\n * They are deliberately **not** each other's negation. Each answers \"is this\n * the one benign reason?\" and defaults to *not benign*, so an error neither\n * recognises is loud under both.\n *\n * ---\n *\n * ## 1. DDL failure classification (#4728)\n *\n * `IDataDriver.syncSchema()` is contractually **idempotent** (\"creates tables if\n * missing, adds columns, updates indexes\"), so in principle a re-sync of an\n * existing table should not throw at all. In practice a driver may surface the\n * already-provisioned case as an error instead of a no-op — `CREATE TABLE`\n * without `IF NOT EXISTS`, an `ALTER TABLE ADD COLUMN` for a column that is\n * already there. That single failure reason is benign: the table and its columns\n * exist, so the bytes will land.\n *\n * **Every other** DDL failure is not benign, and the difference is the whole\n * point of this module. Insufficient privileges, a datasource that never\n * connected, an incompatible column type — after those, the table or column does\n * not exist, yet the process keeps looking healthy while everything it claims to\n * persist has nowhere to land. That is the #4420 shape, and AGENTS.md →\n * \"Degradation log levels\" requires it to be reported at `error`.\n *\n * The defect this replaces was a `catch` whose comment named the benign reason\n * (\"e.g. table already exists\") and used it to excuse **all** of them. Callers\n * must therefore ask the question by error *type*:\n *\n * ```ts\n * catch (error) {\n * if (!isSchemaAlreadyExistsError(error)) {\n * console.error('… consequence … fix …', error); // loud, and stay not-ready\n * return;\n * }\n * // benign only: the table is already provisioned, carry on\n * }\n * ```\n *\n * Classification is deliberately conservative — anything not positively\n * recognised as \"already exists\" is treated as a real failure, because the cost\n * of a false \"benign\" (silent data loss) is far higher than the cost of a false\n * \"real\" (one extra error line).\n *\n * ---\n *\n * ## 2. Missing-table classification for reads (#4825)\n *\n * `DatabaseLoader.nextEventSeq()` reads `sys_metadata_history` to decide what\n * `event_seq` the NEXT history row gets. Its `catch` named both reasons a read\n * can fail — \"table not provisioned yet\" (benign: 1 really is the next number)\n * and \"driver error\" (**not** benign) — and answered both with `return 1`.\n *\n * That is the #4728 shape one layer down, but the damage is the opposite kind\n * and worse. #4728 was *bytes that never landed*; this is **bytes that land\n * wrong**: with N rows already in the table, one flaky read hands the next row\n * `event_seq = 1`, colliding with an existing row. The insert **succeeds**, no\n * line is logged, and `event_seq` — the ordering key that history listing and\n * rollback targeting both stand on — is now silently untrustworthy.\n *\n * So the read seam gets the same treatment, with the same conservative default:\n *\n * ```ts\n * catch (error) {\n * if (isMissingTableError(error)) return 1; // benign: nothing to collide with\n * throw error; // caller reports the consequence\n * }\n * ```\n */\n\n// [#6615] The Postgres `\"x\" of relation \"y\"` phrase, owned once — see the\n// module docblock in `@objectstack/types` for the superstring hole it closes\n// and for why the exclusion's width deliberately differs from the extractor's.\nimport { isRelationSubObjectPhrase } from '@objectstack/types';\n\n/** One \"which errors mean X?\" vocabulary, in the three forms drivers use. */\ninterface DriverErrorSignature {\n /** `error.code` — Postgres SQLSTATE, or mysql2's symbolic name. */\n readonly codes: ReadonlySet<string>;\n /** `error.errno` — MySQL/MariaDB numeric equivalents. */\n readonly errnos: ReadonlySet<number>;\n /** `error.message` — the only signal SQLite-family drivers give. */\n readonly message: RegExp;\n /**\n * Optional **front-exclusion**, evaluated before any positive test (#6347).\n *\n * A message test can never exclude a *superstring*: once a legal phrase for\n * X appears inside a longer phrase that means NOT-X, no amount of widening\n * the X regex removes the match — the phrase really is in there. The only\n * repair is to recognise the not-X shape first and stop. So this is a\n * separate channel rather than another alternation in {@link message}.\n */\n readonly excludes?: {\n /** SQLSTATEs / driver codes that positively mean \"**not** this case\". */\n readonly codes: ReadonlySet<string>;\n /**\n * Message shapes that carry a legal match for this case as a substring.\n *\n * A predicate rather than a `RegExp` since #6615, so this channel can be\n * satisfied by a shared, named question from `@objectstack/types` instead\n * of a pattern this file owns alone. The phrase it tests is the same one\n * `@objectstack/rest` and `@objectstack/service-analytics` read.\n */\n readonly matchesMessage: (message: string) => boolean;\n };\n}\n\n/**\n * Driver/SQLSTATE codes that mean \"the thing you asked me to create is already\n * there\". Postgres reports SQLSTATE on `code`; mysql2 reports its symbolic name.\n */\nconst ALREADY_EXISTS: DriverErrorSignature = {\n codes: new Set([\n // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)\n '42P07', // duplicate_table\n '42701', // duplicate_column\n '42710', // duplicate_object — index / constraint already exists\n // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)\n 'ER_TABLE_EXISTS_ERROR', // 1050\n 'ER_DUP_FIELDNAME', // 1060\n 'ER_DUP_KEYNAME', // 1061\n ]),\n errnos: new Set([1050, 1060, 1061]),\n /**\n * Message fallback for drivers that carry no machine-readable code —\n * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for\n * every DDL failure, so the message is the only signal available:\n * - `table sys_metadata already exists`\n * - `duplicate column name: environment_id`\n * - `index idx_x already exists`\n * Postgres phrases its own as `relation \"x\" already exists` /\n * `column \"x\" of relation \"y\" already exists`, which matches the same test.\n */\n message: /already exists|duplicate column name|duplicate key name/i,\n};\n\n/**\n * Codes/messages that mean \"the table you tried to READ has not been created\".\n *\n * Narrower than it looks, on purpose. `does not exist` on its own also covers\n * `role \"x\" does not exist` (42704), `database \"x\" does not exist` (3D000) and\n * `column \"x\" does not exist` (42703) — every one of them a **real** failure\n * that must stay loud, and every one of them a case where \"start numbering at\n * 1\" would be the wrong answer against a table that may be full of rows. So the\n * message test demands the word table/relation next to the phrase rather than\n * the phrase alone, and the code set carries only the table-scoped SQLSTATEs.\n *\n * That was not enough on its own, and #6347 is why. Postgres has **two**\n * missing-column phrasings, one per direction:\n *\n * | path | phrase | SQLSTATE | matched the message test? |\n * |:---|:---|:---|:---|\n * | read (`SELECT`) | `column \"bogus\" does not exist` | 42703 | no |\n * | write (`INSERT`/`UPDATE`/`ALTER`) | `column \"label\" of relation \"sys_team\" does not exist` | 42703 | **yes** |\n *\n * The write-path phrase contains a complete, legal missing-table phrase —\n * `relation \"sys_team\" does not exist` — as a substring, so the table-scoped\n * test above matched it and answered *benign* about an error the docblock two\n * paragraphs up already named as one that must stay loud. The same holds for\n * every other sub-object of a relation Postgres phrases this way, e.g.\n * `constraint \"uq_x\" of relation \"sys_team\" does not exist` (42704). And\n * code-first does not rescue it: {@link matchesDriverError} is a sequential OR,\n * so a `code: '42703'` error simply falls past the two code lines and is\n * decided by the message.\n *\n * Hence {@link DriverErrorSignature.excludes}: the not-a-table shapes are\n * recognised FIRST, and recognition ends the question with `false`.\n */\nconst MISSING_TABLE: DriverErrorSignature = {\n codes: new Set([\n '42P01', // PostgreSQL undefined_table\n 'ER_NO_SUCH_TABLE', // MySQL / MariaDB 1146\n ]),\n errnos: new Set([1146]),\n /**\n * - SQLite / libsql: `no such table: sys_metadata_history`\n * - PostgreSQL: `relation \"sys_metadata_history\" does not exist`\n * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`\n */\n message:\n /no such table|relation [\"'`][^\"'`]+[\"'`] does not exist|table [\"'`][^\"'`]+[\"'`] doesn'?t exist|unknown table/i,\n excludes: {\n /**\n * Exactly the three SQLSTATEs the docblock above already names as\n * must-stay-loud neighbours of `does not exist`. They are listed here\n * rather than merely trusted to miss the message test, because two of\n * them (42703 columns, 42704 constraints/triggers) have a phrasing that\n * *does* hit it, and because a code is a fact where prose is a guess.\n *\n * Postgres-shaped on purpose: measured, neither MySQL\n * (`Unknown column 'label' in 'field list'`) nor SQLite\n * (`no such column: bogus`, `table t has no column named label`)\n * phrases a sub-object failure so that a missing-table phrase falls out\n * of it, so there is nothing there to exclude. Adding their codes would\n * be surface with no defect behind it.\n */\n codes: new Set([\n '42703', // undefined_column\n '42704', // undefined_object — constraint, trigger, role, type, …\n '3D000', // invalid_catalog_name — `database \"x\" does not exist`\n ]),\n /**\n * `«sub-object» \"x\" of relation \"y\" …` — Postgres' phrasing for a\n * failure about something *inside* a relation, which therefore says the\n * relation itself is present. The two in-repo siblings that carry this\n * phrase are `mapDataError` (`packages/rest`, #5352) and\n * `service-analytics`'s missing-column subtraction (#6035/PR #6346).\n *\n * [#6615] All three now read one home — `@objectstack/types` — instead\n * of three hand-kept copies, so the phrase can no longer be taught to\n * the repo a fourth time or drift in one package only. The **width**\n * difference that used to justify the copy is preserved and is the\n * reason the home exports two functions rather than one: those two\n * *extract* the column name to phrase a better error, so a miss costs a\n * vaguer message; this one *excludes*, so a miss restores the\n * corruption. {@link isRelationSubObjectPhrase} is therefore the wider\n * question — it drops their `column`/`[a-z0-9_]+`/`does not exist`\n * anchors: any sub-object, any quoted identifier, any verdict.\n * Over-matching here only ever converts a benign verdict into a loud\n * one, which is the direction this whole module already errs in.\n */\n matchesMessage: isRelationSubObjectPhrase,\n },\n};\n\n/** How far to follow an `error.cause` chain — drivers wrap, but not deeply. */\nconst MAX_CAUSE_DEPTH = 4;\n\n/**\n * The single matcher both predicates run on: exclusions, then code, then errno,\n * then message, then one step down the `cause` chain.\n *\n * Unrecognised is always `false` — a benign verdict must be *earned*, never\n * defaulted to, because a false \"benign\" corrupts data while a false \"real\"\n * costs one error line.\n *\n * The exclusion runs at every node and, when it fires, returns `false` **without\n * descending into `cause`** (#6347). Two reasons, both the conservative\n * direction: an error that positively identifies as \"a column of an existing\n * relation\" *is* that error, whatever it wraps; and stopping can only ever\n * subtract benign verdicts, never add one.\n */\nfunction matchesDriverError(\n error: unknown,\n signature: DriverErrorSignature,\n depth: number,\n): boolean {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return false;\n\n if (typeof error === 'string') {\n if (signature.excludes?.matchesMessage(error)) return false;\n return signature.message.test(error);\n }\n if (typeof error !== 'object') return false;\n\n const err = error as {\n code?: unknown;\n errno?: unknown;\n message?: unknown;\n cause?: unknown;\n };\n\n const excludes = signature.excludes;\n if (excludes) {\n if (typeof err.code === 'string' && excludes.codes.has(err.code)) return false;\n if (typeof err.message === 'string' && excludes.matchesMessage(err.message)) return false;\n }\n\n if (typeof err.code === 'string' && signature.codes.has(err.code)) return true;\n if (typeof err.errno === 'number' && signature.errnos.has(err.errno)) return true;\n if (typeof err.message === 'string' && signature.message.test(err.message)) return true;\n\n // Drivers commonly re-throw with the original attached as `cause`.\n return matchesDriverError(err.cause, signature, depth + 1);\n}\n\n/**\n * Is this DDL error the benign \"already provisioned\" case?\n *\n * @param error - The value thrown by `syncSchema()` (or any DDL call).\n * @param depth - Internal `cause`-chain recursion counter; callers pass nothing.\n * @returns `true` only when the error positively identifies as\n * table/column/index-already-exists. Anything else — including an\n * unrecognised error, `undefined`, or a permission/connection failure —\n * returns `false` and MUST be reported loudly by the caller.\n */\nexport function isSchemaAlreadyExistsError(error: unknown, depth = 0): boolean {\n return matchesDriverError(error, ALREADY_EXISTS, depth);\n}\n\n/**\n * Is this READ error the benign \"table has not been provisioned yet\" case?\n *\n * The only failure that licenses a caller to treat an empty table as the truth\n * — there are no rows, so there is nothing to be inconsistent with. A\n * connection drop, a timeout, a permission denial or a query error all mean the\n * rows may well exist and simply were not seen; those return `false` and the\n * caller must report the consequence and give up rather than compute an answer\n * from data it never read (#4825).\n *\n * A failure about a **column** of a relation is never this case, in either of\n * Postgres' two phrasings — the relation is right there in the message because\n * it exists (#6347). See {@link MISSING_TABLE}'s `excludes`.\n *\n * @param error - The value thrown by a driver/engine read (`find`, `findOne`, …).\n * @param depth - Internal `cause`-chain recursion counter; callers pass nothing.\n * @returns `true` only when the error positively identifies as\n * table/relation-does-not-exist.\n */\nexport function isMissingTableError(error: unknown, depth = 0): boolean {\n return matchesDriverError(error, MISSING_TABLE, depth);\n}\n"],"mappings":";AAwFA,SAAS,iCAAiC;AA+F1C,IAAM,gBAAsC;AAAA,EACxC,OAAO,oBAAI,IAAI;AAAA,IACX;AAAA;AAAA,IACA;AAAA;AAAA,EACJ,CAAC;AAAA,EACD,QAAQ,oBAAI,IAAI,CAAC,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtB,SACI;AAAA,EACJ,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAeN,OAAO,oBAAI,IAAI;AAAA,MACX;AAAA;AAAA,MACA;AAAA;AAAA,MACA;AAAA;AAAA,IACJ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAqBD,gBAAgB;AAAA,EACpB;AACJ;AAGA,IAAM,kBAAkB;AAgBxB,SAAS,mBACL,OACA,WACA,OACO;AACP,MAAI,UAAU,QAAQ,UAAU,UAAa,QAAQ,gBAAiB,QAAO;AAE7E,MAAI,OAAO,UAAU,UAAU;AAC3B,QAAI,UAAU,UAAU,eAAe,KAAK,EAAG,QAAO;AACtD,WAAO,UAAU,QAAQ,KAAK,KAAK;AAAA,EACvC;AACA,MAAI,OAAO,UAAU,SAAU,QAAO;AAEtC,QAAM,MAAM;AAOZ,QAAM,WAAW,UAAU;AAC3B,MAAI,UAAU;AACV,QAAI,OAAO,IAAI,SAAS,YAAY,SAAS,MAAM,IAAI,IAAI,IAAI,EAAG,QAAO;AACzE,QAAI,OAAO,IAAI,YAAY,YAAY,SAAS,eAAe,IAAI,OAAO,EAAG,QAAO;AAAA,EACxF;AAEA,MAAI,OAAO,IAAI,SAAS,YAAY,UAAU,MAAM,IAAI,IAAI,IAAI,EAAG,QAAO;AAC1E,MAAI,OAAO,IAAI,UAAU,YAAY,UAAU,OAAO,IAAI,IAAI,KAAK,EAAG,QAAO;AAC7E,MAAI,OAAO,IAAI,YAAY,YAAY,UAAU,QAAQ,KAAK,IAAI,OAAO,EAAG,QAAO;AAGnF,SAAO,mBAAmB,IAAI,OAAO,WAAW,QAAQ,CAAC;AAC7D;AAmCO,SAAS,oBAAoB,OAAgB,QAAQ,GAAY;AACpE,SAAO,mBAAmB,OAAO,eAAe,KAAK;AACzD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `@objectstack/metadata/errors` — the shared driver-error discriminators for\n * the metadata storage seams (#4728 / #4825 / #4867 family).\n *\n * ## Why this subpath exists\n *\n * The \"which driver failures may be silenced?\" question is not local to one\n * package. It was answered first for DDL in `@objectstack/metadata`\n * (`ensureSchema`, #4728), then for reads on the legacy `DatabaseLoader` path\n * (`nextEventSeq`, #4825) — and the *canonical* transactional producer of the\n * very same numbers, `SysMetadataRepository`, lives in a different package\n * (`@objectstack/metadata-protocol`, #4867) and carried the identical defect.\n *\n * Three ways to serve that second package were considered; the third is the\n * one taken, and the first is the one this module exists to prevent:\n *\n * 1. **Copy the predicate.** Rejected. Two hand-rolled vocabularies of\n * \"benign driver error\" is precisely the dual-source debt #4825 killed:\n * a driver quirk taught to one copy and not the other produces two\n * packages that disagree about whether data may be silently invented.\n * 2. **Sink it into a common dependency** (`@objectstack/types`,\n * `@objectstack/spec/shared`). Architecturally attractive and explicitly\n * *not* precluded by this module — but out of scope on the round that\n * needed it (spec was frozen; types was under concurrent change).\n * ⇒ **TAKEN, by the maintainer's 2026-08-30 ruling on #13279.** The\n * predicate now lives in `@objectstack/types`\n * (`driver-error-classification.ts`); what forced it was a consumer this\n * file could never serve — `resolveAuthzContext` in `@objectstack/core`,\n * which metadata **depends on**, so the edge could not point that way.\n * 3. **Export it deliberately from its current home** — this file. One\n * declaration, one implementation, one place a new driver quirk is taught.\n *\n * ## What this file is now\n *\n * Option 2 is taken, so this is the compatibility seam it always said it would\n * become — \"a single, greppable seam to delete if the maintainer later takes\n * option 2\". It is NOT deleted: `@objectstack/metadata/errors` is a published\n * subpath with out-of-repo consumers, and #13279 is a fix, not a removal. It\n * re-exports the one symbol it always exported, from the new home. Everything\n * below still describes why the subpath exists and why it stays narrow.\n *\n * ## Why a subpath and not the package entry\n *\n * `@objectstack/metadata`'s root entry pulls the manager, every loader and the\n * YAML/filesystem machinery behind them. A consumer that wants a 40-line\n * predicate should not have to load any of that, and the weight is exactly\n * what would tempt the next author back to option 1. This entry re-exports\n * one leaf module and nothing else, so the cross-package edge stays a leaf\n * edge — and stays a single, greppable seam to delete if the maintainer later\n * takes option 2.\n *\n * ## Scope of the promise\n *\n * Only {@link isMissingTableError} is re-exported HERE. Its sibling\n * `isSchemaAlreadyExistsError` moved to `@objectstack/types` with it (they are\n * two signatures over one matcher and cannot be separated without re-rolling\n * it), but it does not need a second door: nothing imports it through\n * `@objectstack/metadata/errors`, and an exported symbol nobody imports is a\n * promise made for nothing (Prime Directive #10, pointed at our own API\n * surface). Anything that needs it reads `@objectstack/types` directly.\n */\n\nexport { isMissingTableError } from '@objectstack/types';\n"],"mappings":";AAgEA,SAAS,2BAA2B;","names":[]}
|