@byline/db-postgres 3.0.1 → 3.1.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.
@@ -152,8 +152,43 @@ export declare class DocumentCommands implements IDocumentCommands {
152
152
  * version is assumed fresh (no rows), so no conflict handling is needed.
153
153
  * Used by `reAnchorDocument` to snapshot the current version into the new
154
154
  * re-anchored one without re-flattening (lossless, identity-preserving).
155
+ *
156
+ * When `excludeLocale` is provided, rows for that locale are skipped in the
157
+ * seven value-store tables — the carry-forward that powers `deleteDocumentLocale`
158
+ * (drop one locale's content, keep `'all'` + every other locale). The
159
+ * locale-agnostic `byline_store_meta` rows (block / array-item identities) are
160
+ * always copied wholesale, since a block's identity is shared across locales.
155
161
  */
156
162
  private copyAllVersionStoreRows;
163
+ /**
164
+ * deleteDocumentLocale
165
+ *
166
+ * Remove one content locale's data from a document by writing a **new
167
+ * immutable version** that carries forward every store row except the
168
+ * target locale's (the `'all'` rows and all other locales are kept). The
169
+ * prior version still holds the deleted locale, so the operation is
170
+ * recoverable via version restore, and a previously-published version keeps
171
+ * serving until the new version is published.
172
+ *
173
+ * The new version's status is supplied by the caller (the lifecycle service
174
+ * passes the workflow's default — a fresh draft, matching `copyToLocale`).
175
+ * The derived availability ledger is recomputed from the carried-forward
176
+ * rows, so the deleted locale drops out automatically. The default content
177
+ * locale (the document's anchor) must never be passed here — the lifecycle
178
+ * service enforces that.
179
+ *
180
+ * Mirrors `reAnchorDocument`'s new-version mechanics; defensively returns
181
+ * `null` when the document has no current version (the service validates
182
+ * existence first, so this is a guard).
183
+ */
184
+ deleteDocumentLocale(params: {
185
+ documentId: string;
186
+ locale: string;
187
+ status?: string;
188
+ }): Promise<{
189
+ newVersionId: string;
190
+ previousVersionId: string;
191
+ } | null>;
157
192
  /**
158
193
  * reAnchorDocument
159
194
  *
@@ -332,51 +332,61 @@ export class DocumentCommands {
332
332
  * version is assumed fresh (no rows), so no conflict handling is needed.
333
333
  * Used by `reAnchorDocument` to snapshot the current version into the new
334
334
  * re-anchored one without re-flattening (lossless, identity-preserving).
335
+ *
336
+ * When `excludeLocale` is provided, rows for that locale are skipped in the
337
+ * seven value-store tables — the carry-forward that powers `deleteDocumentLocale`
338
+ * (drop one locale's content, keep `'all'` + every other locale). The
339
+ * locale-agnostic `byline_store_meta` rows (block / array-item identities) are
340
+ * always copied wholesale, since a block's identity is shared across locales.
335
341
  */
336
- async copyAllVersionStoreRows(tx, fromVersionId, toVersionId) {
342
+ async copyAllVersionStoreRows(tx, fromVersionId, toVersionId, excludeLocale) {
337
343
  const from = sql `${fromVersionId}::uuid`;
338
344
  const to = sql `${toVersionId}::uuid`;
345
+ // Appended to each value-store WHERE so the dropped locale's rows are not
346
+ // carried into the new version. Empty fragment (copy everything) when no
347
+ // exclusion is requested — the `reAnchorDocument` snapshot path.
348
+ const localeFilter = excludeLocale ? sql ` AND locale <> ${excludeLocale}` : sql ``;
339
349
  await tx.execute(sql `
340
350
  INSERT INTO byline_store_text
341
351
  (id, document_version_id, collection_id, field_path, field_name, locale, parent_path, value, word_count, created_at, updated_at)
342
352
  SELECT gen_random_uuid(), ${to}, collection_id, field_path, field_name, locale, parent_path, value, word_count, NOW(), NOW()
343
- FROM byline_store_text WHERE document_version_id = ${from}
353
+ FROM byline_store_text WHERE document_version_id = ${from}${localeFilter}
344
354
  `);
345
355
  await tx.execute(sql `
346
356
  INSERT INTO byline_store_numeric
347
357
  (id, document_version_id, collection_id, field_path, field_name, locale, parent_path, number_type, value_integer, value_decimal, value_float, created_at, updated_at)
348
358
  SELECT gen_random_uuid(), ${to}, collection_id, field_path, field_name, locale, parent_path, number_type, value_integer, value_decimal, value_float, NOW(), NOW()
349
- FROM byline_store_numeric WHERE document_version_id = ${from}
359
+ FROM byline_store_numeric WHERE document_version_id = ${from}${localeFilter}
350
360
  `);
351
361
  await tx.execute(sql `
352
362
  INSERT INTO byline_store_boolean
353
363
  (id, document_version_id, collection_id, field_path, field_name, locale, parent_path, value, created_at, updated_at)
354
364
  SELECT gen_random_uuid(), ${to}, collection_id, field_path, field_name, locale, parent_path, value, NOW(), NOW()
355
- FROM byline_store_boolean WHERE document_version_id = ${from}
365
+ FROM byline_store_boolean WHERE document_version_id = ${from}${localeFilter}
356
366
  `);
357
367
  await tx.execute(sql `
358
368
  INSERT INTO byline_store_datetime
359
369
  (id, document_version_id, collection_id, field_path, field_name, locale, parent_path, date_type, value_date, value_time, value_timestamp_tz, created_at, updated_at)
360
370
  SELECT gen_random_uuid(), ${to}, collection_id, field_path, field_name, locale, parent_path, date_type, value_date, value_time, value_timestamp_tz, NOW(), NOW()
361
- FROM byline_store_datetime WHERE document_version_id = ${from}
371
+ FROM byline_store_datetime WHERE document_version_id = ${from}${localeFilter}
362
372
  `);
363
373
  await tx.execute(sql `
364
374
  INSERT INTO byline_store_json
365
375
  (id, document_version_id, collection_id, field_path, field_name, locale, parent_path, value, json_schema, object_keys, created_at, updated_at)
366
376
  SELECT gen_random_uuid(), ${to}, collection_id, field_path, field_name, locale, parent_path, value, json_schema, object_keys, NOW(), NOW()
367
- FROM byline_store_json WHERE document_version_id = ${from}
377
+ FROM byline_store_json WHERE document_version_id = ${from}${localeFilter}
368
378
  `);
369
379
  await tx.execute(sql `
370
380
  INSERT INTO byline_store_relation
371
381
  (id, document_version_id, collection_id, field_path, field_name, locale, parent_path, target_document_id, target_collection_id, relationship_type, cascade_delete, created_at, updated_at)
372
382
  SELECT gen_random_uuid(), ${to}, collection_id, field_path, field_name, locale, parent_path, target_document_id, target_collection_id, relationship_type, cascade_delete, NOW(), NOW()
373
- FROM byline_store_relation WHERE document_version_id = ${from}
383
+ FROM byline_store_relation WHERE document_version_id = ${from}${localeFilter}
374
384
  `);
375
385
  await tx.execute(sql `
376
386
  INSERT INTO byline_store_file
377
387
  (id, document_version_id, collection_id, field_path, field_name, locale, parent_path, file_id, filename, original_filename, mime_type, file_size, file_hash, storage_provider, storage_path, storage_url, image_width, image_height, image_format, processing_status, thumbnail_generated, created_at, updated_at)
378
388
  SELECT gen_random_uuid(), ${to}, collection_id, field_path, field_name, locale, parent_path, file_id, filename, original_filename, mime_type, file_size, file_hash, storage_provider, storage_path, storage_url, image_width, image_height, image_format, processing_status, thumbnail_generated, NOW(), NOW()
379
- FROM byline_store_file WHERE document_version_id = ${from}
389
+ FROM byline_store_file WHERE document_version_id = ${from}${localeFilter}
380
390
  `);
381
391
  await tx.execute(sql `
382
392
  INSERT INTO byline_store_meta
@@ -385,6 +395,67 @@ export class DocumentCommands {
385
395
  FROM byline_store_meta WHERE document_version_id = ${from}
386
396
  `);
387
397
  }
398
+ /**
399
+ * deleteDocumentLocale
400
+ *
401
+ * Remove one content locale's data from a document by writing a **new
402
+ * immutable version** that carries forward every store row except the
403
+ * target locale's (the `'all'` rows and all other locales are kept). The
404
+ * prior version still holds the deleted locale, so the operation is
405
+ * recoverable via version restore, and a previously-published version keeps
406
+ * serving until the new version is published.
407
+ *
408
+ * The new version's status is supplied by the caller (the lifecycle service
409
+ * passes the workflow's default — a fresh draft, matching `copyToLocale`).
410
+ * The derived availability ledger is recomputed from the carried-forward
411
+ * rows, so the deleted locale drops out automatically. The default content
412
+ * locale (the document's anchor) must never be passed here — the lifecycle
413
+ * service enforces that.
414
+ *
415
+ * Mirrors `reAnchorDocument`'s new-version mechanics; defensively returns
416
+ * `null` when the document has no current version (the service validates
417
+ * existence first, so this is a guard).
418
+ */
419
+ async deleteDocumentLocale(params) {
420
+ const { documentId, locale, status } = params;
421
+ return this.db.transaction(async (tx) => {
422
+ // 1. Current (latest, non-deleted) version + the document's anchor.
423
+ const current = await tx
424
+ .select({
425
+ versionId: documentVersions.id,
426
+ collectionId: documentVersions.collection_id,
427
+ collectionVersion: documentVersions.collection_version,
428
+ sourceLocale: documents.source_locale,
429
+ })
430
+ .from(documentVersions)
431
+ .innerJoin(documents, eq(documents.id, documentVersions.document_id))
432
+ .where(and(eq(documentVersions.document_id, documentId), eq(documentVersions.is_deleted, false)))
433
+ .orderBy(desc(documentVersions.id))
434
+ .limit(1)
435
+ .then((rows) => rows[0]);
436
+ if (current == null)
437
+ return null;
438
+ const sourceLocale = current.sourceLocale ?? this.defaultContentLocale;
439
+ // 2. New immutable version: a snapshot of the current version with the
440
+ // target locale's value rows dropped (meta + 'all' + other locales
441
+ // carried forward).
442
+ const newVersionId = uuidv7();
443
+ await tx.insert(documentVersions).values({
444
+ id: newVersionId,
445
+ document_id: documentId,
446
+ collection_id: current.collectionId,
447
+ collection_version: current.collectionVersion,
448
+ event_type: 'delete_locale',
449
+ status: status ?? 'draft',
450
+ change_summary: `deleted content locale ${locale}`,
451
+ });
452
+ await this.copyAllVersionStoreRows(tx, current.versionId, newVersionId, locale);
453
+ // 3. Recompute the new version's availability ledger against the source
454
+ // locale — the dropped locale no longer covers it, so it falls out.
455
+ await this.writeVersionLocaleLedger(tx, newVersionId, sourceLocale);
456
+ return { newVersionId, previousVersionId: current.versionId };
457
+ });
458
+ }
388
459
  /**
389
460
  * reAnchorDocument
390
461
  *
@@ -216,6 +216,20 @@ export declare class DocumentQueries implements IDocumentQueries {
216
216
  created_at: Date;
217
217
  updated_at: Date;
218
218
  } | null>;
219
+ /**
220
+ * getCurrentPath — resolve a document's canonical (source-locale) path.
221
+ *
222
+ * Reuses `pathProjection` against `current_documents`, passing
223
+ * `requestedLocale: undefined` so the projection's fallback floor — the
224
+ * document's own `source_locale` (COALESCE-guarded to the default content
225
+ * locale for not-yet-anchored rows) — supplies the canonical path. Used by
226
+ * the lifecycle to populate `path` on the status-change / unpublish hook
227
+ * contexts. Returns `null` when no path row (or document) exists.
228
+ */
229
+ getCurrentPath({ collection_id, document_id, }: {
230
+ collection_id: string;
231
+ document_id: string;
232
+ }): Promise<string | null>;
219
233
  /**
220
234
  * getDocumentById — gets the current version of a document by its logical document ID.
221
235
  *
@@ -439,6 +439,26 @@ export class DocumentQueries {
439
439
  updated_at: row.updated_at ?? new Date(),
440
440
  };
441
441
  }
442
+ /**
443
+ * getCurrentPath — resolve a document's canonical (source-locale) path.
444
+ *
445
+ * Reuses `pathProjection` against `current_documents`, passing
446
+ * `requestedLocale: undefined` so the projection's fallback floor — the
447
+ * document's own `source_locale` (COALESCE-guarded to the default content
448
+ * locale for not-yet-anchored rows) — supplies the canonical path. Used by
449
+ * the lifecycle to populate `path` on the status-change / unpublish hook
450
+ * contexts. Returns `null` when no path row (or document) exists.
451
+ */
452
+ async getCurrentPath({ collection_id, document_id, }) {
453
+ const [row] = await this.db
454
+ .select({
455
+ path: this.pathProjection(sql `${currentDocumentsView.document_id}`, undefined, sql `${currentDocumentsView.source_locale}`),
456
+ })
457
+ .from(currentDocumentsView)
458
+ .where(and(eq(currentDocumentsView.collection_id, collection_id), eq(currentDocumentsView.document_id, document_id)))
459
+ .limit(1);
460
+ return row?.path ?? null;
461
+ }
442
462
  /**
443
463
  * getDocumentById — gets the current version of a document by its logical document ID.
444
464
  *
@@ -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,155 @@
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 { afterAll, beforeAll, describe, expect, it } from 'vitest';
9
+ import { setupTestDB, teardownTestDB } from '../../../lib/test-helper.js';
10
+ let commandBuilders;
11
+ let queryBuilders;
12
+ const timestamp = Date.now();
13
+ const DeleteLocaleCollectionConfig = {
14
+ path: `delete-locale-${timestamp}`,
15
+ labels: { singular: 'DeleteLocaleTest', plural: 'DeleteLocaleTests' },
16
+ fields: [
17
+ { name: 'title', type: 'text', localized: true },
18
+ // Non-localized → lives on `locale: 'all'` rows; must survive the delete.
19
+ { name: 'sku', type: 'text' },
20
+ ],
21
+ };
22
+ let testCollection = {};
23
+ function readById(documentId, locale) {
24
+ return queryBuilders.documents.getDocumentById({
25
+ collection_id: testCollection.id,
26
+ document_id: documentId,
27
+ locale,
28
+ });
29
+ }
30
+ /**
31
+ * Create a fresh document with three localized titles (en/fr/de) and one
32
+ * shared non-localized field, returning the document id and the current
33
+ * (latest) version id.
34
+ */
35
+ async function seedTrilingualDoc(titlePrefix) {
36
+ const v1 = await commandBuilders.documents.createDocumentVersion({
37
+ collectionId: testCollection.id,
38
+ collectionVersion: 1,
39
+ collectionConfig: DeleteLocaleCollectionConfig,
40
+ action: 'create',
41
+ documentData: { title: `${titlePrefix} EN`, sku: 'SKU-1' },
42
+ locale: 'en',
43
+ status: 'published',
44
+ });
45
+ const documentId = v1.document.document_id;
46
+ const v2 = await commandBuilders.documents.createDocumentVersion({
47
+ documentId,
48
+ collectionId: testCollection.id,
49
+ collectionVersion: 1,
50
+ collectionConfig: DeleteLocaleCollectionConfig,
51
+ action: 'update',
52
+ documentData: { title: `${titlePrefix} FR`, sku: 'SKU-1' },
53
+ locale: 'fr',
54
+ status: 'published',
55
+ previousVersionId: v1.document.id,
56
+ });
57
+ const v3 = await commandBuilders.documents.createDocumentVersion({
58
+ documentId,
59
+ collectionId: testCollection.id,
60
+ collectionVersion: 1,
61
+ collectionConfig: DeleteLocaleCollectionConfig,
62
+ action: 'update',
63
+ documentData: { title: `${titlePrefix} DE`, sku: 'SKU-1' },
64
+ locale: 'de',
65
+ status: 'published',
66
+ previousVersionId: v2.document.id,
67
+ });
68
+ return { documentId, currentVersionId: v3.document.id };
69
+ }
70
+ describe('deleteDocumentLocale integration', () => {
71
+ beforeAll(async () => {
72
+ const testDB = setupTestDB([DeleteLocaleCollectionConfig]);
73
+ commandBuilders = testDB.commandBuilders;
74
+ queryBuilders = testDB.queryBuilders;
75
+ const result = await commandBuilders.collections.create(DeleteLocaleCollectionConfig.path, DeleteLocaleCollectionConfig);
76
+ const collection = result[0];
77
+ if (collection == null) {
78
+ throw new Error('Failed to create test collection');
79
+ }
80
+ testCollection = { id: collection.id, name: collection.path };
81
+ });
82
+ afterAll(async () => {
83
+ try {
84
+ await commandBuilders.collections.delete(testCollection.id);
85
+ }
86
+ catch (error) {
87
+ console.error('Failed to cleanup test collection:', error);
88
+ }
89
+ await teardownTestDB();
90
+ });
91
+ it('drops the target locale from availability, keeps the others', async () => {
92
+ const { documentId } = await seedTrilingualDoc('Avail');
93
+ const before = await readById(documentId, 'en');
94
+ expect(before?._availableVersionLocales, 'all three locales present').toEqual([
95
+ 'de',
96
+ 'en',
97
+ 'fr',
98
+ ]);
99
+ await commandBuilders.documents.deleteDocumentLocale({
100
+ documentId,
101
+ locale: 'fr',
102
+ status: 'draft',
103
+ });
104
+ const after = await readById(documentId, 'en');
105
+ expect(after?._availableVersionLocales, 'fr removed, de/en remain').toEqual(['de', 'en']);
106
+ });
107
+ it("keeps other locales' localized content and the shared non-localized field", async () => {
108
+ const { documentId } = await seedTrilingualDoc('Content');
109
+ await commandBuilders.documents.deleteDocumentLocale({
110
+ documentId,
111
+ locale: 'fr',
112
+ status: 'draft',
113
+ });
114
+ const de = await readById(documentId, 'de');
115
+ expect(de?.fields.title, 'de content untouched').toBe('Content DE');
116
+ expect(de?.fields.sku, "non-localized 'all' field preserved").toBe('SKU-1');
117
+ const en = await readById(documentId, 'en');
118
+ expect(en?.fields.title, 'default-locale content untouched').toBe('Content EN');
119
+ });
120
+ it('lands the new version with the supplied status (a fresh draft)', async () => {
121
+ const { documentId } = await seedTrilingualDoc('Status');
122
+ await commandBuilders.documents.deleteDocumentLocale({
123
+ documentId,
124
+ locale: 'fr',
125
+ status: 'draft',
126
+ });
127
+ const meta = await queryBuilders.documents.getCurrentVersionMetadata({
128
+ collection_id: testCollection.id,
129
+ document_id: documentId,
130
+ });
131
+ expect(meta?.status, 'delete-locale version is a draft').toBe('draft');
132
+ });
133
+ it('is recoverable — the prior version still holds the deleted locale', async () => {
134
+ const { documentId, currentVersionId } = await seedTrilingualDoc('Recover');
135
+ await commandBuilders.documents.deleteDocumentLocale({
136
+ documentId,
137
+ locale: 'fr',
138
+ status: 'draft',
139
+ });
140
+ // The pre-delete version is immutable and still carries the fr content.
141
+ const prior = (await queryBuilders.documents.getDocumentByVersion({
142
+ document_version_id: currentVersionId,
143
+ locale: 'fr',
144
+ }));
145
+ expect(prior?.fields.title, 'fr content survives in the prior version').toBe('Recover FR');
146
+ });
147
+ it('returns null for an unknown document', async () => {
148
+ const result = await commandBuilders.documents.deleteDocumentLocale({
149
+ documentId: crypto.randomUUID(),
150
+ locale: 'fr',
151
+ status: 'draft',
152
+ });
153
+ expect(result).toBeNull();
154
+ });
155
+ });
@@ -181,4 +181,81 @@ describe('byline_document_paths integration', () => {
181
181
  });
182
182
  expect(result).toBe(null);
183
183
  });
184
+ describe('getCurrentPath', () => {
185
+ it('resolves a document’s canonical path under its default source locale', async () => {
186
+ const canonicalPath = `current-path-${Date.now()}`;
187
+ const created = await commandBuilders.documents.createDocumentVersion({
188
+ collectionId: testCollection.id,
189
+ collectionVersion: 1,
190
+ collectionConfig: PathsCollectionConfig,
191
+ action: 'create',
192
+ documentData: { title: 'Has Path' },
193
+ path: canonicalPath,
194
+ locale: 'all',
195
+ status: 'draft',
196
+ });
197
+ const documentId = created.document.document_id;
198
+ const path = await queryBuilders.documents.getCurrentPath({
199
+ collection_id: testCollection.id,
200
+ document_id: documentId,
201
+ });
202
+ expect(path).toBe(canonicalPath);
203
+ });
204
+ it('follows the source-locale anchor after a document is re-anchored', async () => {
205
+ const canonicalPath = `reanchor-path-${Date.now()}`;
206
+ // Create locale-agnostic content (ledger carries the 'all' sentinel) so
207
+ // the document is "complete" in any target and re-anchoring is eligible.
208
+ const created = await commandBuilders.documents.createDocumentVersion({
209
+ collectionId: testCollection.id,
210
+ collectionVersion: 1,
211
+ collectionConfig: PathsCollectionConfig,
212
+ action: 'create',
213
+ documentData: { title: 'Re-anchor me' },
214
+ path: canonicalPath,
215
+ locale: 'all',
216
+ status: 'draft',
217
+ });
218
+ const documentId = created.document.document_id;
219
+ // Flip the document's source locale from the default ('en') to 'fr'.
220
+ // reAnchorDocument moves the path row onto the new source locale,
221
+ // keeping the slug. getCurrentPath passes requestedLocale: undefined, so
222
+ // its fallback floor is COALESCE(source_locale, default) — it must now
223
+ // resolve via the 'fr' anchor, not the global default 'en'.
224
+ const result = await commandBuilders.documents.reAnchorDocument({
225
+ documentId,
226
+ targetLocale: 'fr',
227
+ });
228
+ expect(result.status).toBe('reanchored');
229
+ const path = await queryBuilders.documents.getCurrentPath({
230
+ collection_id: testCollection.id,
231
+ document_id: documentId,
232
+ });
233
+ expect(path).toBe(canonicalPath);
234
+ });
235
+ it('returns null when the document has no path row', async () => {
236
+ // Create a version without a `path` — no document_paths row is written.
237
+ const created = await commandBuilders.documents.createDocumentVersion({
238
+ collectionId: testCollection.id,
239
+ collectionVersion: 1,
240
+ collectionConfig: PathsCollectionConfig,
241
+ action: 'create',
242
+ documentData: { title: 'No Path' },
243
+ locale: 'all',
244
+ status: 'draft',
245
+ });
246
+ const documentId = created.document.document_id;
247
+ const path = await queryBuilders.documents.getCurrentPath({
248
+ collection_id: testCollection.id,
249
+ document_id: documentId,
250
+ });
251
+ expect(path).toBe(null);
252
+ });
253
+ it('returns null for a non-existent document', async () => {
254
+ const path = await queryBuilders.documents.getCurrentPath({
255
+ collection_id: testCollection.id,
256
+ document_id: crypto.randomUUID(),
257
+ });
258
+ expect(path).toBe(null);
259
+ });
260
+ });
184
261
  });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/db-postgres",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "3.0.1",
5
+ "version": "3.1.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -57,9 +57,9 @@
57
57
  "pg": "^8.21.0",
58
58
  "uuid": "^14.0.0",
59
59
  "zod": "^4.4.3",
60
- "@byline/core": "3.0.1",
61
- "@byline/auth": "3.0.1",
62
- "@byline/admin": "3.0.1"
60
+ "@byline/core": "3.1.0",
61
+ "@byline/auth": "3.1.0",
62
+ "@byline/admin": "3.1.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@biomejs/biome": "2.4.15",