@byline/core 3.0.1 → 3.0.2

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.
@@ -360,6 +360,10 @@ export interface BeforeUpdateContext {
360
360
  sourceLocale: string;
361
361
  targetLocale: string;
362
362
  };
363
+ /** Set only when the update originates from a Delete-Locale operation. */
364
+ deleteLocale?: {
365
+ locale: string;
366
+ };
363
367
  }
364
368
  /**
365
369
  * Context passed to `afterUpdate` hooks.
@@ -385,6 +389,10 @@ export interface AfterUpdateContext {
385
389
  sourceLocale: string;
386
390
  targetLocale: string;
387
391
  };
392
+ /** Mirrors `BeforeUpdateContext.deleteLocale`. */
393
+ deleteLocale?: {
394
+ locale: string;
395
+ };
388
396
  }
389
397
  /**
390
398
  * Context passed to `beforeStatusChange` / `afterStatusChange` hooks.
@@ -373,6 +373,24 @@ export interface IDocumentCommands {
373
373
  softDeleteDocument(params: {
374
374
  document_id: string;
375
375
  }): Promise<number>;
376
+ /**
377
+ * Remove one content locale's data from a document by writing a new
378
+ * immutable version that carries forward every store row except the target
379
+ * locale's (the `'all'` rows and all other locales are kept). The prior
380
+ * version still holds the deleted locale, so the operation is recoverable.
381
+ *
382
+ * `status` is the new version's status (the lifecycle service passes the
383
+ * workflow default — a fresh draft). Returns the new and previous version
384
+ * ids, or `null` when the document has no current version.
385
+ */
386
+ deleteDocumentLocale(params: {
387
+ documentId: string;
388
+ locale: string;
389
+ status?: string;
390
+ }): Promise<{
391
+ newVersionId: string;
392
+ previousVersionId: string;
393
+ } | null>;
376
394
  /**
377
395
  * Write the fractional-index `order_key` on a single `byline_documents`
378
396
  * row. Used by the reorder server fn for `orderable: true` collections.
@@ -42,6 +42,7 @@ function createMockDb(options) {
42
42
  setDocumentStatus: vi.fn(fail),
43
43
  archivePublishedVersions: vi.fn(fail),
44
44
  softDeleteDocument: vi.fn(fail),
45
+ deleteDocumentLocale: vi.fn(fail),
45
46
  setOrderKey: vi.fn(fail),
46
47
  },
47
48
  counters: {
@@ -183,6 +184,7 @@ describe('ensureCollections', () => {
183
184
  setDocumentStatus: vi.fn(),
184
185
  archivePublishedVersions: vi.fn(),
185
186
  softDeleteDocument: vi.fn(),
187
+ deleteDocumentLocale: vi.fn(),
186
188
  setOrderKey: vi.fn(),
187
189
  },
188
190
  counters: {
@@ -27,6 +27,7 @@ function makeAdapter(options) {
27
27
  setDocumentStatus: vi.fn(fail),
28
28
  archivePublishedVersions: vi.fn(fail),
29
29
  softDeleteDocument: vi.fn(fail),
30
+ deleteDocumentLocale: vi.fn(fail),
30
31
  setOrderKey: vi.fn(fail),
31
32
  },
32
33
  counters: {
@@ -135,6 +135,13 @@ export interface CopyToLocaleResult {
135
135
  */
136
136
  fieldsUpdated: number;
137
137
  }
138
+ export interface DeleteLocaleResult {
139
+ documentId: string;
140
+ /** The newly-created version that omits the deleted locale's content. */
141
+ documentVersionId: string;
142
+ /** The content locale that was removed. */
143
+ locale: string;
144
+ }
138
145
  export interface DuplicateDocumentResult {
139
146
  /** The newly-created document's id. */
140
147
  documentId: string;
@@ -421,3 +428,32 @@ export declare function copyToLocale(ctx: DocumentLifecycleContext, params: {
421
428
  targetLocale: string;
422
429
  overwrite: boolean;
423
430
  }): Promise<CopyToLocaleResult>;
431
+ /**
432
+ * Remove one content locale's data from a document, in place on the same
433
+ * document, by writing a new immutable version that omits that locale's
434
+ * store rows (every other locale and all non-localized `'all'` rows are
435
+ * carried forward by the storage primitive).
436
+ *
437
+ * The default content locale is the document's anchor (path + source_locale)
438
+ * and can never be removed — rejected up front. The new version lands as the
439
+ * workflow's default status (a fresh draft), exactly like `copyToLocale`: the
440
+ * previously-published version keeps serving — including the locale being
441
+ * removed — until the new version is reviewed and published. The deletion is
442
+ * recoverable: the prior version still holds the locale, so restoring it
443
+ * brings the content back.
444
+ *
445
+ * Flow:
446
+ * 1. `assertActorCanPerform('update')` — removing a translation is an edit.
447
+ * 2. Reject `locale === defaultLocale`.
448
+ * 3. Read the document in the target locale (validates existence; supplies
449
+ * `originalData` for hooks and the availability set for the presence
450
+ * check).
451
+ * 4. Reject when the locale has no content to delete.
452
+ * 5. `hooks.beforeUpdate({ …, deleteLocale: { locale } })`.
453
+ * 6. `db.commands.documents.deleteDocumentLocale({ …, status: default })`.
454
+ * 7. `hooks.afterUpdate({ …, deleteLocale: { locale } })`.
455
+ */
456
+ export declare function deleteLocale(ctx: DocumentLifecycleContext, params: {
457
+ documentId: string;
458
+ locale: string;
459
+ }): Promise<DeleteLocaleResult>;
@@ -1341,3 +1341,107 @@ export async function copyToLocale(ctx, params) {
1341
1341
  };
1342
1342
  });
1343
1343
  }
1344
+ // ---------------------------------------------------------------------------
1345
+ // deleteLocale
1346
+ // ---------------------------------------------------------------------------
1347
+ /**
1348
+ * Remove one content locale's data from a document, in place on the same
1349
+ * document, by writing a new immutable version that omits that locale's
1350
+ * store rows (every other locale and all non-localized `'all'` rows are
1351
+ * carried forward by the storage primitive).
1352
+ *
1353
+ * The default content locale is the document's anchor (path + source_locale)
1354
+ * and can never be removed — rejected up front. The new version lands as the
1355
+ * workflow's default status (a fresh draft), exactly like `copyToLocale`: the
1356
+ * previously-published version keeps serving — including the locale being
1357
+ * removed — until the new version is reviewed and published. The deletion is
1358
+ * recoverable: the prior version still holds the locale, so restoring it
1359
+ * brings the content back.
1360
+ *
1361
+ * Flow:
1362
+ * 1. `assertActorCanPerform('update')` — removing a translation is an edit.
1363
+ * 2. Reject `locale === defaultLocale`.
1364
+ * 3. Read the document in the target locale (validates existence; supplies
1365
+ * `originalData` for hooks and the availability set for the presence
1366
+ * check).
1367
+ * 4. Reject when the locale has no content to delete.
1368
+ * 5. `hooks.beforeUpdate({ …, deleteLocale: { locale } })`.
1369
+ * 6. `db.commands.documents.deleteDocumentLocale({ …, status: default })`.
1370
+ * 7. `hooks.afterUpdate({ …, deleteLocale: { locale } })`.
1371
+ */
1372
+ export async function deleteLocale(ctx, params) {
1373
+ return withLogContext({ domain: 'services', module: 'lifecycle', function: 'deleteLocale' }, async () => {
1374
+ const { db, definition, collectionId, collectionPath, defaultLocale } = ctx;
1375
+ assertActorCanPerform(ctx.requestContext, collectionPath, 'update');
1376
+ // The default locale anchors the document's path and source_locale —
1377
+ // it cannot be deleted (the other locales fall back to it).
1378
+ if (params.locale === defaultLocale) {
1379
+ throw ERR_VALIDATION({
1380
+ message: `cannot delete the default content locale ('${defaultLocale}')`,
1381
+ details: { documentId: params.documentId, locale: params.locale, collectionPath },
1382
+ }).log(ctx.logger);
1383
+ }
1384
+ // Read the document in the locale being removed — validates existence,
1385
+ // supplies originalData for hooks, and the availability set for the
1386
+ // content-presence check below.
1387
+ const target = await db.queries.documents.getDocumentById({
1388
+ collection_id: collectionId,
1389
+ document_id: params.documentId,
1390
+ locale: params.locale,
1391
+ reconstruct: true,
1392
+ lenient: true,
1393
+ requestContext: ctx.requestContext,
1394
+ });
1395
+ if (target == null) {
1396
+ throw ERR_NOT_FOUND({
1397
+ message: 'document not found',
1398
+ details: { documentId: params.documentId, collectionPath },
1399
+ }).log(ctx.logger);
1400
+ }
1401
+ const targetRecord = target;
1402
+ // `_availableVersionLocales` is the derived (path-coverage) set; it is
1403
+ // the same source the editor's Delete-Locale picker is built from, so a
1404
+ // locale offered in the UI resolves here. A partially-translated locale
1405
+ // that never reached full coverage is not deletable through this path.
1406
+ const available = targetRecord._availableVersionLocales ?? [];
1407
+ if (!available.includes(params.locale)) {
1408
+ throw ERR_NOT_FOUND({
1409
+ message: `locale '${params.locale}' has no content to delete`,
1410
+ details: { documentId: params.documentId, locale: params.locale, collectionPath },
1411
+ }).log(ctx.logger);
1412
+ }
1413
+ const hooks = definition.hooks;
1414
+ const deleteLocaleMarker = { locale: params.locale };
1415
+ const originalData = targetRecord.fields ?? {};
1416
+ await invokeHook(hooks?.beforeUpdate, {
1417
+ data: originalData,
1418
+ originalData,
1419
+ collectionPath,
1420
+ deleteLocale: deleteLocaleMarker,
1421
+ });
1422
+ const result = await db.commands.documents.deleteDocumentLocale({
1423
+ documentId: params.documentId,
1424
+ locale: params.locale,
1425
+ status: getDefaultStatus(definition),
1426
+ });
1427
+ if (result == null) {
1428
+ throw ERR_NOT_FOUND({
1429
+ message: 'document not found',
1430
+ details: { documentId: params.documentId, collectionPath },
1431
+ }).log(ctx.logger);
1432
+ }
1433
+ await invokeHook(hooks?.afterUpdate, {
1434
+ data: originalData,
1435
+ originalData,
1436
+ collectionPath,
1437
+ documentId: params.documentId,
1438
+ documentVersionId: result.newVersionId,
1439
+ deleteLocale: deleteLocaleMarker,
1440
+ });
1441
+ return {
1442
+ documentId: params.documentId,
1443
+ documentVersionId: result.newVersionId,
1444
+ locale: params.locale,
1445
+ };
1446
+ });
1447
+ }
@@ -47,6 +47,7 @@ function createMockDb() {
47
47
  setDocumentStatus,
48
48
  archivePublishedVersions,
49
49
  softDeleteDocument,
50
+ deleteDocumentLocale: vi.fn(),
50
51
  setOrderKey: vi.fn(),
51
52
  },
52
53
  counters: {
@@ -56,6 +56,7 @@ function createMockDb() {
56
56
  setDocumentStatus: vi.fn(),
57
57
  archivePublishedVersions: vi.fn(),
58
58
  softDeleteDocument: vi.fn(),
59
+ deleteDocumentLocale: vi.fn(),
59
60
  setOrderKey: vi.fn(),
60
61
  },
61
62
  counters: {
@@ -144,6 +144,7 @@ function makeMockAdapter(store = {}, pathByCollectionId = {}) {
144
144
  setDocumentStatus: vi.fn(),
145
145
  archivePublishedVersions: vi.fn(),
146
146
  softDeleteDocument: vi.fn(),
147
+ deleteDocumentLocale: vi.fn(),
147
148
  setOrderKey: vi.fn(),
148
149
  },
149
150
  counters: {
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": "3.0.1",
5
+ "version": "3.0.2",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -79,7 +79,7 @@
79
79
  "sharp": "^0.34.5",
80
80
  "uuid": "^14.0.0",
81
81
  "zod": "^4.4.3",
82
- "@byline/auth": "3.0.1"
82
+ "@byline/auth": "3.0.2"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@biomejs/biome": "2.4.15",