@byline/core 4.7.0 → 4.8.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/dist/@types/db-types.d.ts +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/errors.d.ts +26 -0
- package/dist/lib/errors.js +19 -0
- package/dist/services/document-lifecycle/create.js +1 -1
- package/dist/services/document-lifecycle/duplicate.js +2 -2
- package/dist/services/document-lifecycle/internals.d.ts +9 -11
- package/dist/services/document-lifecycle/internals.js +16 -23
- package/dist/services/document-lifecycle/rethrow-path-conflict.test.node.d.ts +8 -0
- package/dist/services/document-lifecycle/rethrow-path-conflict.test.node.js +41 -0
- package/dist/services/document-lifecycle/system-fields.js +1 -1
- package/dist/services/document-lifecycle/update.js +2 -2
- package/dist/services/document-lifecycle.test.node.js +14 -1
- package/dist/storage/store-manifest.js +4 -0
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RequestContext } from '@byline/auth';
|
|
2
2
|
import type { CollectionDefinition } from '@byline/core';
|
|
3
|
+
import type { DbErrorClassification } from '../lib/errors.js';
|
|
3
4
|
import type { QueryPredicate } from './query-predicate.js';
|
|
4
5
|
/**
|
|
5
6
|
* Read mode for document queries.
|
|
@@ -245,6 +246,16 @@ export interface IDbAdapter {
|
|
|
245
246
|
backfillSourceLocales?: () => Promise<{
|
|
246
247
|
rowsUpdated: number;
|
|
247
248
|
}>;
|
|
249
|
+
/**
|
|
250
|
+
* Classify a raw driver error into an adapter-agnostic, code-based shape so
|
|
251
|
+
* core can map database failures to domain errors without knowing driver
|
|
252
|
+
* anatomy. The error-side analogue of the storage `normalizeRow` seam.
|
|
253
|
+
*
|
|
254
|
+
* Optional: when absent, core treats every error as `DB_UNKNOWN` and rethrows
|
|
255
|
+
* it unchanged — the current behaviour for adapters that do not implement it.
|
|
256
|
+
* Canonical adapters (db-postgres, db-mysql) implement it.
|
|
257
|
+
*/
|
|
258
|
+
classifyError?(err: unknown): DbErrorClassification;
|
|
248
259
|
}
|
|
249
260
|
/**
|
|
250
261
|
* The realm of the actor that performed an audited change. `'admin'` for
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
|
|
|
7
7
|
export { type BylineCore, getBylineCore, initBylineCore } from './core.js';
|
|
8
8
|
export * from './defaults/default-values.js';
|
|
9
9
|
export { getHostRequestBridge, type HostCookieSetOptions, type HostRequestBridge, registerHostRequestBridge, tryGetHostRequestBridge, } from './host/host-request-bridge.js';
|
|
10
|
-
export { BylineError, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, type ErrorReport, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
|
|
10
|
+
export { BylineError, type DbErrorClassification, type DbErrorCode, DbErrorCodes, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, type ErrorReport, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
|
|
11
11
|
export { generateKeyBetween, generateNKeysBetween, validateOrderKey, } from './lib/fractional-index.js';
|
|
12
12
|
export { type BylineLogger, getLogger } from './lib/logger.js';
|
|
13
13
|
export { AsyncRegistry, type RegisteredServices, Registry } from './lib/registry.js';
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
|
|
|
23
23
|
export { getBylineCore, initBylineCore } from './core.js';
|
|
24
24
|
export * from './defaults/default-values.js';
|
|
25
25
|
export { getHostRequestBridge, registerHostRequestBridge, tryGetHostRequestBridge, } from './host/host-request-bridge.js';
|
|
26
|
-
export { BylineError, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
|
|
26
|
+
export { BylineError, DbErrorCodes, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
|
|
27
27
|
export { generateKeyBetween, generateNKeysBetween, validateOrderKey, } from './lib/fractional-index.js';
|
|
28
28
|
export { getLogger } from './lib/logger.js';
|
|
29
29
|
export { AsyncRegistry, Registry } from './lib/registry.js';
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -127,3 +127,29 @@ export declare const ERR_AUDIT_UNSUPPORTED: (opts: BylineErrorOptions, errorCons
|
|
|
127
127
|
* reconcile, while transports can distinguish it from a rolled-back mutation.
|
|
128
128
|
*/
|
|
129
129
|
export declare const ERR_TREE_HOOK_COMMITTED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
130
|
+
/**
|
|
131
|
+
* Code-based classification of a raw database driver error, produced by
|
|
132
|
+
* `IDbAdapter.classifyError`. The error-side analogue of the storage
|
|
133
|
+
* `normalizeRow` seam: the adapter canonicalises driver anatomy into these
|
|
134
|
+
* codes so `@byline/core` can map DB failures to domain errors (e.g.
|
|
135
|
+
* `ERR_PATH_CONFLICT`) without knowing any driver's error shape.
|
|
136
|
+
*
|
|
137
|
+
* Distinct from `ErrorCodes` above: those are thrown `BylineError` codes;
|
|
138
|
+
* these are returned classification values. Extend with new codes (e.g. a
|
|
139
|
+
* future `STALE_RECORD` for optimistic-concurrency failures) as consumers
|
|
140
|
+
* need them.
|
|
141
|
+
*/
|
|
142
|
+
export declare const DbErrorCodes: {
|
|
143
|
+
readonly UNIQUE_VIOLATION: 'DB_UNIQUE_VIOLATION';
|
|
144
|
+
readonly UNKNOWN: 'DB_UNKNOWN';
|
|
145
|
+
};
|
|
146
|
+
export type DbErrorCode = (typeof DbErrorCodes)[keyof typeof DbErrorCodes];
|
|
147
|
+
export interface DbErrorClassification {
|
|
148
|
+
code: DbErrorCode;
|
|
149
|
+
/**
|
|
150
|
+
* For `DB_UNIQUE_VIOLATION`: the violated constraint / index name when the
|
|
151
|
+
* driver exposes it (Postgres carries it structurally; MySQL parses it from
|
|
152
|
+
* the error message). Absent when the driver surfaces no name.
|
|
153
|
+
*/
|
|
154
|
+
constraint?: string;
|
|
155
|
+
}
|
package/dist/lib/errors.js
CHANGED
|
@@ -164,3 +164,22 @@ export const ERR_AUDIT_UNSUPPORTED = createErrorType(ErrorCodes.AUDIT_UNSUPPORTE
|
|
|
164
164
|
* reconcile, while transports can distinguish it from a rolled-back mutation.
|
|
165
165
|
*/
|
|
166
166
|
export const ERR_TREE_HOOK_COMMITTED = createErrorType(ErrorCodes.TREE_HOOK_COMMITTED, 'warn');
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Database error classification (adapter seam)
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Code-based classification of a raw database driver error, produced by
|
|
172
|
+
* `IDbAdapter.classifyError`. The error-side analogue of the storage
|
|
173
|
+
* `normalizeRow` seam: the adapter canonicalises driver anatomy into these
|
|
174
|
+
* codes so `@byline/core` can map DB failures to domain errors (e.g.
|
|
175
|
+
* `ERR_PATH_CONFLICT`) without knowing any driver's error shape.
|
|
176
|
+
*
|
|
177
|
+
* Distinct from `ErrorCodes` above: those are thrown `BylineError` codes;
|
|
178
|
+
* these are returned classification values. Extend with new codes (e.g. a
|
|
179
|
+
* future `STALE_RECORD` for optimistic-concurrency failures) as consumers
|
|
180
|
+
* need them.
|
|
181
|
+
*/
|
|
182
|
+
export const DbErrorCodes = {
|
|
183
|
+
UNIQUE_VIOLATION: 'DB_UNIQUE_VIOLATION',
|
|
184
|
+
UNKNOWN: 'DB_UNKNOWN',
|
|
185
|
+
};
|
|
@@ -86,7 +86,7 @@ export async function createDocument(ctx, params) {
|
|
|
86
86
|
orderKey,
|
|
87
87
|
createdBy: actorId(ctx),
|
|
88
88
|
})
|
|
89
|
-
.catch((err) => rethrowPathConflict(err, resolvedPath, defaultLocale));
|
|
89
|
+
.catch((err) => rethrowPathConflict(db, err, resolvedPath, defaultLocale));
|
|
90
90
|
const documentId = extractDocumentId(result.document);
|
|
91
91
|
const documentVersionId = extractVersionId(result.document);
|
|
92
92
|
// `tree: true` collections place every document in the tree by default:
|
|
@@ -172,7 +172,7 @@ export async function duplicateDocument(ctx, params) {
|
|
|
172
172
|
orderKey,
|
|
173
173
|
createdBy: actorId(ctx),
|
|
174
174
|
})
|
|
175
|
-
.catch((err) => rethrowPathConflict(err, finalPath, defaultLocale));
|
|
175
|
+
.catch((err) => rethrowPathConflict(db, err, finalPath, defaultLocale));
|
|
176
176
|
}
|
|
177
177
|
catch (err) {
|
|
178
178
|
if (!isPathConflictError(err)) {
|
|
@@ -197,7 +197,7 @@ export async function duplicateDocument(ctx, params) {
|
|
|
197
197
|
orderKey,
|
|
198
198
|
createdBy: actorId(ctx),
|
|
199
199
|
})
|
|
200
|
-
.catch((retryErr) => rethrowPathConflict(retryErr, finalPath, defaultLocale));
|
|
200
|
+
.catch((retryErr) => rethrowPathConflict(db, retryErr, finalPath, defaultLocale));
|
|
201
201
|
}
|
|
202
202
|
const newDocumentId = extractDocumentId(result.document);
|
|
203
203
|
const newDocumentVersionId = extractVersionId(result.document);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* the barrel (`index.ts`), so the package's public surface is unchanged
|
|
12
12
|
* by the per-operation split.
|
|
13
13
|
*/
|
|
14
|
-
import { type CollectionDefinition, type CollectionHookSlot } from '../../@types/index.js';
|
|
14
|
+
import { type CollectionDefinition, type CollectionHookSlot, type IDbAdapter } from '../../@types/index.js';
|
|
15
15
|
import type { BylineLogger } from '../../lib/logger.js';
|
|
16
16
|
import type { SlugifierFn } from '../../utils/slugify.js';
|
|
17
17
|
import type { DocumentLifecycleContext } from './context.js';
|
|
@@ -64,20 +64,18 @@ export declare function extractVersionId(document: any): string;
|
|
|
64
64
|
/** Extract the logical document id from the document object returned by `createDocumentVersion`. */
|
|
65
65
|
export declare function extractDocumentId(document: any): string;
|
|
66
66
|
/**
|
|
67
|
-
* Detect a
|
|
67
|
+
* Detect a unique-constraint violation on
|
|
68
68
|
* `byline_document_paths(collection_id, locale, path)` and translate it
|
|
69
69
|
* to `ERR_PATH_CONFLICT`. Any other error is rethrown unchanged.
|
|
70
70
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* original attached as `cause`, so we walk a short cause chain to find
|
|
78
|
-
* the carried `code` / `constraint`.
|
|
71
|
+
* Driver anatomy (SQLSTATE codes, `cause`-chain walking, constraint-name
|
|
72
|
+
* carriage) is delegated to `db.classifyError` — the adapter seam that
|
|
73
|
+
* canonicalises a raw driver error into a `DbErrorClassification`. This
|
|
74
|
+
* function only knows the classification codes and the path constraint's
|
|
75
|
+
* name substring; it stays targeted to the path constraint so unrelated
|
|
76
|
+
* unique violations aren't spuriously rebranded as path conflicts.
|
|
79
77
|
*/
|
|
80
|
-
export declare function rethrowPathConflict(err: unknown, path: string, locale: string): never;
|
|
78
|
+
export declare function rethrowPathConflict(db: IDbAdapter, err: unknown, path: string, locale: string): never;
|
|
81
79
|
/**
|
|
82
80
|
* Detect whether an error is the `ERR_PATH_CONFLICT` raised by
|
|
83
81
|
* `rethrowPathConflict`. Used by `duplicateDocument`'s retry logic to
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { normalizeCollectionHook, } from '../../@types/index.js';
|
|
15
15
|
import { getCollectionDefinition, getServerConfig } from '../../config/config.js';
|
|
16
|
-
import { ERR_PATH_CONFLICT, ErrorCodes } from '../../lib/errors.js';
|
|
16
|
+
import { DbErrorCodes, ERR_PATH_CONFLICT, ErrorCodes } from '../../lib/errors.js';
|
|
17
17
|
import { generateKeyBetween } from '../../lib/fractional-index.js';
|
|
18
18
|
import { createReadContext } from '../populate.js';
|
|
19
19
|
import { embedRichTextFields } from '../richtext-embed.js';
|
|
@@ -131,32 +131,25 @@ export function extractDocumentId(document) {
|
|
|
131
131
|
return document?.document_id ?? '';
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
|
-
* Detect a
|
|
134
|
+
* Detect a unique-constraint violation on
|
|
135
135
|
* `byline_document_paths(collection_id, locale, path)` and translate it
|
|
136
136
|
* to `ERR_PATH_CONFLICT`. Any other error is rethrown unchanged.
|
|
137
137
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* original attached as `cause`, so we walk a short cause chain to find
|
|
145
|
-
* the carried `code` / `constraint`.
|
|
138
|
+
* Driver anatomy (SQLSTATE codes, `cause`-chain walking, constraint-name
|
|
139
|
+
* carriage) is delegated to `db.classifyError` — the adapter seam that
|
|
140
|
+
* canonicalises a raw driver error into a `DbErrorClassification`. This
|
|
141
|
+
* function only knows the classification codes and the path constraint's
|
|
142
|
+
* name substring; it stays targeted to the path constraint so unrelated
|
|
143
|
+
* unique violations aren't spuriously rebranded as path conflicts.
|
|
146
144
|
*/
|
|
147
|
-
export function rethrowPathConflict(err, path, locale) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
message: `path "${path}" is already in use in this collection (locale: ${locale})`,
|
|
156
|
-
details: { path, locale, constraint: e.constraint },
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
e = e.cause;
|
|
145
|
+
export function rethrowPathConflict(db, err, path, locale) {
|
|
146
|
+
const classification = db.classifyError?.(err);
|
|
147
|
+
if (classification?.code === DbErrorCodes.UNIQUE_VIOLATION &&
|
|
148
|
+
classification.constraint?.includes('document_paths_collection_locale_path')) {
|
|
149
|
+
throw ERR_PATH_CONFLICT({
|
|
150
|
+
message: `path "${path}" is already in use in this collection (locale: ${locale})`,
|
|
151
|
+
details: { path, locale, constraint: classification.constraint },
|
|
152
|
+
});
|
|
160
153
|
}
|
|
161
154
|
throw err;
|
|
162
155
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { ErrorCodes } from '../../lib/errors.js';
|
|
10
|
+
import { rethrowPathConflict } from './internals.js';
|
|
11
|
+
const adapterWith = (c) => ({ classifyError: c === undefined ? undefined : () => c });
|
|
12
|
+
describe('rethrowPathConflict', () => {
|
|
13
|
+
it('maps a unique violation on the path constraint to ERR_PATH_CONFLICT', () => {
|
|
14
|
+
const db = adapterWith({
|
|
15
|
+
code: 'DB_UNIQUE_VIOLATION',
|
|
16
|
+
constraint: 'byline_document_paths_document_paths_collection_locale_path',
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
rethrowPathConflict(db, new Error('raw'), 'news/hello', 'en');
|
|
20
|
+
throw new Error('should have thrown');
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
expect(e.code).toBe(ErrorCodes.PATH_CONFLICT);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
it('rethrows raw when the unique violation is on a different constraint', () => {
|
|
27
|
+
const raw = new Error('raw');
|
|
28
|
+
const db = adapterWith({ code: 'DB_UNIQUE_VIOLATION', constraint: 'some_other_unique' });
|
|
29
|
+
expect(() => rethrowPathConflict(db, raw, 'p', 'en')).toThrow(raw);
|
|
30
|
+
});
|
|
31
|
+
it('rethrows raw for DB_UNKNOWN', () => {
|
|
32
|
+
const raw = new Error('raw');
|
|
33
|
+
const db = adapterWith({ code: 'DB_UNKNOWN' });
|
|
34
|
+
expect(() => rethrowPathConflict(db, raw, 'p', 'en')).toThrow(raw);
|
|
35
|
+
});
|
|
36
|
+
it('rethrows raw when the adapter has no classifyError', () => {
|
|
37
|
+
const raw = new Error('raw');
|
|
38
|
+
const db = adapterWith(undefined);
|
|
39
|
+
expect(() => rethrowPathConflict(db, raw, 'p', 'en')).toThrow(raw);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -104,7 +104,7 @@ export async function updateDocumentSystemFields(ctx, params) {
|
|
|
104
104
|
locale: sourceLocale,
|
|
105
105
|
path: pathForCommand,
|
|
106
106
|
})
|
|
107
|
-
.catch((err) => rethrowPathConflict(err, pathForCommand, sourceLocale));
|
|
107
|
+
.catch((err) => rethrowPathConflict(db, err, pathForCommand, sourceLocale));
|
|
108
108
|
await audit.append({
|
|
109
109
|
documentId: params.documentId,
|
|
110
110
|
collectionId,
|
|
@@ -91,7 +91,7 @@ export async function updateDocument(ctx, params) {
|
|
|
91
91
|
previousVersionId: originalData.document_version_id,
|
|
92
92
|
createdBy: actorId(ctx),
|
|
93
93
|
})
|
|
94
|
-
.catch((err) => rethrowPathConflict(err, pathForCommand ?? '', defaultLocale));
|
|
94
|
+
.catch((err) => rethrowPathConflict(db, err, pathForCommand ?? '', defaultLocale));
|
|
95
95
|
const documentId = extractDocumentId(result.document) || params.documentId;
|
|
96
96
|
const documentVersionId = extractVersionId(result.document);
|
|
97
97
|
// Self-heal: re-root a genuinely-unplaced doc in a tree collection so any
|
|
@@ -210,7 +210,7 @@ export async function updateDocumentWithPatches(ctx, params) {
|
|
|
210
210
|
previousVersionId: originalData.document_version_id,
|
|
211
211
|
createdBy: actorId(ctx),
|
|
212
212
|
})
|
|
213
|
-
.catch((err) => rethrowPathConflict(err, pathForCommand ?? '', defaultLocale));
|
|
213
|
+
.catch((err) => rethrowPathConflict(db, err, pathForCommand ?? '', defaultLocale));
|
|
214
214
|
const documentId = extractDocumentId(result.document) || params.documentId;
|
|
215
215
|
const documentVersionId = extractVersionId(result.document);
|
|
216
216
|
// Self-heal: re-root a genuinely-unplaced doc in a tree collection so any
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { AdminAuth, AuthError, AuthErrorCodes, createRequestContext, createSuperAdminContext, } from '@byline/auth';
|
|
9
9
|
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
-
import { BylineError, ERR_PATH_CONFLICT, ErrorCodes } from '../lib/errors.js';
|
|
10
|
+
import { BylineError, DbErrorCodes, ERR_PATH_CONFLICT, ErrorCodes } from '../lib/errors.js';
|
|
11
11
|
import { changeDocumentStatus, copyToLocale, createDocument, deleteDocument, duplicateDocument, restoreDocumentVersion, unpublishDocument, updateDocument, updateDocumentSystemFields, updateDocumentWithPatches, } from './document-lifecycle/index.js';
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
// Fixtures / Helpers
|
|
@@ -50,7 +50,20 @@ function createMockDb() {
|
|
|
50
50
|
// records the calls so write-point tests can assert the audit rows emitted.
|
|
51
51
|
const auditAppend = vi.fn().mockResolvedValue({ id: 'audit-1' });
|
|
52
52
|
const withTransaction = vi.fn(async (fn) => fn());
|
|
53
|
+
// Fake classifier mirroring db-postgres's real one closely enough for
|
|
54
|
+
// rethrowPathConflict's consumption of the seam: a pg-shaped 23505 with a
|
|
55
|
+
// constraint name classifies as a unique violation; anything else is
|
|
56
|
+
// unknown. Individual tests inject pg-shaped errors on `createDocumentVersion`
|
|
57
|
+
// (see `translates a Postgres unique-constraint violation...` below).
|
|
58
|
+
const classifyError = vi.fn((err) => {
|
|
59
|
+
const e = err;
|
|
60
|
+
if (e?.code === '23505') {
|
|
61
|
+
return { code: DbErrorCodes.UNIQUE_VIOLATION, constraint: e.constraint };
|
|
62
|
+
}
|
|
63
|
+
return { code: DbErrorCodes.UNKNOWN };
|
|
64
|
+
});
|
|
53
65
|
const db = {
|
|
66
|
+
classifyError,
|
|
54
67
|
commands: {
|
|
55
68
|
collections: {
|
|
56
69
|
create: vi.fn(),
|
|
@@ -186,6 +186,10 @@ export const storeColumnManifest = [
|
|
|
186
186
|
nullCast: 'varchar',
|
|
187
187
|
sources: { json: 'json_schema' },
|
|
188
188
|
},
|
|
189
|
+
// nullCast 'text[]' is the one abstract null-cast type with no MySQL array
|
|
190
|
+
// equivalent; the MySQL adapter maps it to CAST(NULL AS JSON) in its UNION
|
|
191
|
+
// null casts (Postgres uses NULL::text[]). See
|
|
192
|
+
// specs/2026-07-24-db-error-classification-seam-design.md §6.
|
|
189
193
|
{
|
|
190
194
|
name: 'object_keys',
|
|
191
195
|
nullCast: 'text[]',
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/core",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.8.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"sharp": "^0.35.3",
|
|
83
83
|
"uuid": "^14.0.1",
|
|
84
84
|
"zod": "^4.4.3",
|
|
85
|
-
"@byline/auth": "4.
|
|
85
|
+
"@byline/auth": "4.8.0"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@biomejs/biome": "2.5.4",
|