@byline/host-tanstack-start 4.19.0 → 5.0.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.
Files changed (142) hide show
  1. package/dist/admin-shell/collections/edit.d.ts +1 -1
  2. package/dist/admin-shell/collections/edit.js +151 -31
  3. package/dist/admin-shell/collections/history.js +4 -2
  4. package/dist/admin-shell/collections/list.js +30 -6
  5. package/dist/admin-shell/collections/restore-version-modal.d.ts +3 -1
  6. package/dist/admin-shell/collections/restore-version-modal.js +9 -4
  7. package/dist/admin-shell/collections/tree-list.js +16 -6
  8. package/dist/admin-shell/collections/version-history.d.ts +2 -1
  9. package/dist/admin-shell/collections/version-history.js +11 -2
  10. package/dist/admin-shell/document-mutation-notice.d.ts +6 -0
  11. package/dist/admin-shell/document-mutation-notice.js +53 -0
  12. package/dist/admin-shell/document-mutation-state.d.ts +22 -0
  13. package/dist/admin-shell/document-mutation-state.js +71 -0
  14. package/dist/admin-shell/document-mutation-state.test.d.ts +1 -0
  15. package/dist/admin-shell/scheduled-publications/list.js +15 -5
  16. package/dist/admin-shell/singletons/history.js +4 -2
  17. package/dist/admin-shell/singletons/view.d.ts +1 -1
  18. package/dist/admin-shell/singletons/view.js +98 -19
  19. package/dist/integrations/byline-field-services.js +2 -9
  20. package/dist/routes/create-collection-edit-route.js +1 -1
  21. package/dist/routes/create-collection-list-route.d.ts +0 -7
  22. package/dist/routes/create-collection-list-route.js +7 -9
  23. package/dist/routes/create-singleton-route.d.ts +2 -0
  24. package/dist/routes/create-singleton-route.js +4 -3
  25. package/dist/server-fns/collections/copy-to-locale.d.ts +2 -7
  26. package/dist/server-fns/collections/copy-to-locale.js +4 -2
  27. package/dist/server-fns/collections/create.d.ts +1 -7
  28. package/dist/server-fns/collections/create.js +5 -3
  29. package/dist/server-fns/collections/delete-locale.d.ts +2 -7
  30. package/dist/server-fns/collections/delete-locale.js +4 -2
  31. package/dist/server-fns/collections/delete-outcome.d.ts +7 -3
  32. package/dist/server-fns/collections/delete-outcome.js +4 -0
  33. package/dist/server-fns/collections/delete.d.ts +2 -7
  34. package/dist/server-fns/collections/delete.js +5 -3
  35. package/dist/server-fns/collections/duplicate.d.ts +2 -7
  36. package/dist/server-fns/collections/duplicate.js +5 -3
  37. package/dist/server-fns/collections/get.d.ts +1 -7
  38. package/dist/server-fns/collections/get.js +5 -4
  39. package/dist/server-fns/collections/list.d.ts +0 -7
  40. package/dist/server-fns/collections/list.js +11 -3
  41. package/dist/server-fns/collections/reorder.d.ts +7 -8
  42. package/dist/server-fns/collections/reorder.js +20 -76
  43. package/dist/server-fns/collections/restore-version.d.ts +3 -7
  44. package/dist/server-fns/collections/restore-version.js +5 -2
  45. package/dist/server-fns/collections/save-outcome.d.ts +1 -0
  46. package/dist/server-fns/collections/save-outcome.js +1 -0
  47. package/dist/server-fns/collections/scheduled-publication.d.ts +49 -9
  48. package/dist/server-fns/collections/scheduled-publication.js +31 -21
  49. package/dist/server-fns/collections/status.d.ts +8 -7
  50. package/dist/server-fns/collections/status.js +13 -5
  51. package/dist/server-fns/collections/tree-mutation.d.ts +7 -6
  52. package/dist/server-fns/collections/tree-mutation.js +17 -5
  53. package/dist/server-fns/collections/tree.d.ts +14 -8
  54. package/dist/server-fns/collections/tree.js +21 -17
  55. package/dist/server-fns/collections/update.d.ts +12 -11
  56. package/dist/server-fns/collections/update.js +34 -20
  57. package/dist/server-fns/collections/upload.d.ts +1 -0
  58. package/dist/server-fns/collections/upload.js +3 -2
  59. package/dist/server-fns/document-mutation-errors.d.ts +5 -0
  60. package/dist/server-fns/document-mutation-errors.js +50 -0
  61. package/dist/server-fns/editable-revision.d.ts +9 -0
  62. package/dist/server-fns/editable-revision.js +11 -0
  63. package/dist/server-fns/singleton-document-read.d.ts +1 -1
  64. package/dist/server-fns/singleton-document-read.js +7 -3
  65. package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +6 -8
  66. package/dist/server-fns/singletons/cancel-scheduled-publish.js +4 -1
  67. package/dist/server-fns/singletons/change-status.d.ts +2 -7
  68. package/dist/server-fns/singletons/change-status.js +4 -1
  69. package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +5 -8
  70. package/dist/server-fns/singletons/confirm-scheduled-publish.js +4 -2
  71. package/dist/server-fns/singletons/copy-to-locale.d.ts +2 -7
  72. package/dist/server-fns/singletons/copy-to-locale.js +4 -2
  73. package/dist/server-fns/singletons/get.d.ts +4 -4
  74. package/dist/server-fns/singletons/restore-version.d.ts +2 -7
  75. package/dist/server-fns/singletons/restore-version.js +6 -3
  76. package/dist/server-fns/singletons/schedule-publish.d.ts +5 -8
  77. package/dist/server-fns/singletons/schedule-publish.js +4 -2
  78. package/dist/server-fns/singletons/unpublish.d.ts +2 -7
  79. package/dist/server-fns/singletons/unpublish.js +4 -1
  80. package/dist/server-fns/singletons/update.d.ts +4 -11
  81. package/dist/server-fns/singletons/update.js +5 -4
  82. package/dist/server-fns/structural-receipt.d.ts +4 -0
  83. package/dist/server-fns/structural-receipt.js +31 -0
  84. package/package.json +11 -11
  85. package/src/admin-shell/collections/edit.tsx +173 -54
  86. package/src/admin-shell/collections/history.tsx +11 -2
  87. package/src/admin-shell/collections/list.tsx +22 -6
  88. package/src/admin-shell/collections/restore-version-modal.tsx +12 -3
  89. package/src/admin-shell/collections/save-navigation.test.tsx +156 -8
  90. package/src/admin-shell/collections/tree-list.tsx +16 -6
  91. package/src/admin-shell/collections/version-history.tsx +13 -3
  92. package/src/admin-shell/document-mutation-notice.tsx +45 -0
  93. package/src/admin-shell/document-mutation-state.test.tsx +146 -0
  94. package/src/admin-shell/document-mutation-state.ts +95 -0
  95. package/src/admin-shell/scheduled-publications/list.tsx +16 -6
  96. package/src/admin-shell/singletons/history.test.tsx +3 -1
  97. package/src/admin-shell/singletons/history.tsx +10 -2
  98. package/src/admin-shell/singletons/view.test.tsx +59 -18
  99. package/src/admin-shell/singletons/view.tsx +110 -29
  100. package/src/integrations/byline-field-services.ts +2 -3
  101. package/src/routes/create-collection-edit-route.tsx +1 -0
  102. package/src/routes/create-collection-list-route.tsx +13 -2
  103. package/src/routes/create-singleton-route.test.node.ts +10 -2
  104. package/src/routes/create-singleton-route.tsx +6 -4
  105. package/src/server-fns/collections/copy-to-locale.ts +41 -36
  106. package/src/server-fns/collections/create.ts +54 -50
  107. package/src/server-fns/collections/delete-locale.ts +41 -32
  108. package/src/server-fns/collections/delete-outcome.test.node.ts +16 -0
  109. package/src/server-fns/collections/delete-outcome.ts +27 -16
  110. package/src/server-fns/collections/delete.ts +44 -38
  111. package/src/server-fns/collections/duplicate.ts +38 -32
  112. package/src/server-fns/collections/get.ts +8 -4
  113. package/src/server-fns/collections/list.ts +8 -3
  114. package/src/server-fns/collections/reorder.ts +32 -124
  115. package/src/server-fns/collections/restore-version.ts +47 -39
  116. package/src/server-fns/collections/revision-input.test.node.ts +108 -0
  117. package/src/server-fns/collections/save-outcome.test.node.ts +3 -0
  118. package/src/server-fns/collections/save-outcome.ts +2 -0
  119. package/src/server-fns/collections/scheduled-publication.ts +138 -111
  120. package/src/server-fns/collections/status.ts +86 -64
  121. package/src/server-fns/collections/tree-mutation.test.node.ts +87 -29
  122. package/src/server-fns/collections/tree-mutation.ts +28 -11
  123. package/src/server-fns/collections/tree.ts +150 -122
  124. package/src/server-fns/collections/update.ts +110 -86
  125. package/src/server-fns/collections/upload-singleton.test.node.ts +3 -7
  126. package/src/server-fns/collections/upload.ts +81 -77
  127. package/src/server-fns/document-mutation-errors.ts +67 -0
  128. package/src/server-fns/document-mutation-transport.test.node.ts +211 -0
  129. package/src/server-fns/editable-revision.test.node.ts +23 -0
  130. package/src/server-fns/editable-revision.ts +18 -0
  131. package/src/server-fns/singleton-document-read.ts +7 -3
  132. package/src/server-fns/singletons/cancel-scheduled-publish.ts +10 -3
  133. package/src/server-fns/singletons/change-status.ts +10 -3
  134. package/src/server-fns/singletons/confirm-scheduled-publish.ts +12 -6
  135. package/src/server-fns/singletons/copy-to-locale.ts +20 -15
  136. package/src/server-fns/singletons/get.ts +3 -3
  137. package/src/server-fns/singletons/restore-version.ts +17 -12
  138. package/src/server-fns/singletons/schedule-publish.ts +18 -7
  139. package/src/server-fns/singletons/singletons.test.node.ts +63 -25
  140. package/src/server-fns/singletons/unpublish.ts +10 -3
  141. package/src/server-fns/singletons/update.ts +21 -16
  142. package/src/server-fns/structural-receipt.ts +41 -0
@@ -1,3 +1,4 @@
1
+ import { withDocumentMutationErrors } from "../document-mutation-errors.js";
1
2
  import { createServerFn } from "@tanstack/react-start";
2
3
  import { getAdminRequestContext } from "@byline/client/server";
3
4
  import { ERR_NOT_FOUND, getLogger, getServerConfig } from "@byline/core";
@@ -5,7 +6,7 @@ import { changeDocumentStatus, unpublishDocument } from "@byline/core/services";
5
6
  import { ensureCollection } from "../../integrations/api-utils.js";
6
7
  const updateDocumentStatus = createServerFn({
7
8
  method: 'POST'
8
- }).validator((input)=>input).handler(async ({ data: input })=>{
9
+ }).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data: input })=>{
9
10
  const { collection: path, id, status: nextStatus } = input;
10
11
  const logger = getLogger();
11
12
  const config = await ensureCollection(path);
@@ -29,17 +30,20 @@ const updateDocumentStatus = createServerFn({
29
30
  };
30
31
  const result = await changeDocumentStatus(ctx, {
31
32
  documentId: id,
33
+ expectedRevision: input.expectedRevision,
32
34
  nextStatus
33
35
  });
34
36
  return {
35
37
  status: 'ok',
38
+ documentId: result.documentId,
39
+ revision: result.revision,
36
40
  previousStatus: result.previousStatus,
37
41
  newStatus: result.newStatus
38
42
  };
39
- });
43
+ }));
40
44
  const status_unpublishDocument = createServerFn({
41
45
  method: 'POST'
42
- }).validator((input)=>input).handler(async ({ data: input })=>{
46
+ }).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data: input })=>{
43
47
  const { collection: path, id } = input;
44
48
  const logger = getLogger();
45
49
  const config = await ensureCollection(path);
@@ -62,18 +66,22 @@ const status_unpublishDocument = createServerFn({
62
66
  requestContext: await getAdminRequestContext()
63
67
  };
64
68
  const result = await unpublishDocument(ctx, {
65
- documentId: id
69
+ documentId: id,
70
+ expectedRevision: input.expectedRevision
66
71
  });
67
72
  if (0 === result.archivedCount) throw ERR_NOT_FOUND({
68
73
  message: 'No published version found for this document',
69
74
  details: {
70
75
  documentId: id,
76
+ expectedRevision: input.expectedRevision,
71
77
  collectionPath: path
72
78
  }
73
79
  }).log(logger);
74
80
  return {
75
81
  status: 'ok',
82
+ documentId: result.documentId,
83
+ revision: result.revision,
76
84
  archivedCount: result.archivedCount
77
85
  };
78
- });
86
+ }));
79
87
  export { status_unpublishDocument as unpublishDocument, updateDocumentStatus };
@@ -1,10 +1,12 @@
1
1
  import type { CollectionHandle, PlaceTreeNodeOptions, RemoveFromTreeOptions } from '@byline/client';
2
+ import { structuralReceipt } from '../structural-receipt.js';
2
3
  /** Prefer the code; the marker survives transports that reconstruct a code-less Error. */
3
4
  export declare function isTreeMutationConflict(error: unknown): boolean;
4
5
  /** Detect when tree/audit committed even if transport reconstruction dropped the code. */
5
6
  export declare function isCommittedTreeHookFailure(error: unknown): boolean;
6
7
  export type AdminTreeMoveOutcome = {
7
8
  status: 'ok';
9
+ receipt?: ReturnType<typeof structuralReceipt>;
8
10
  } | {
9
11
  status: 'suppressed';
10
12
  } | {
@@ -18,6 +20,7 @@ export type AdminTreeMoveOutcome = {
18
20
  } | {
19
21
  status: 'refresh-failed';
20
22
  error: unknown;
23
+ receipt?: ReturnType<typeof structuralReceipt>;
21
24
  };
22
25
  export interface AdminTreeMoveUiEffects {
23
26
  rollback: boolean;
@@ -38,9 +41,7 @@ export interface AdminTreeMoveController {
38
41
  }
39
42
  /** Single-flight controller shared by pointer and keyboard tree moves. */
40
43
  export declare function createAdminTreeMoveController(): AdminTreeMoveController;
41
- /** Admin retries reconcile post-commit tree hooks unless explicitly disabled. */
42
- export declare function placeAdminTreeNode(handle: Pick<CollectionHandle, 'placeTreeNode'>, documentId: string, options: PlaceTreeNodeOptions): Promise<{
43
- orderKey: string;
44
- }>;
45
- /** Admin retries reconcile post-commit tree hooks unless explicitly disabled. */
46
- export declare function removeAdminTreeNode(handle: Pick<CollectionHandle, 'removeFromTree'>, documentId: string, options?: RemoveFromTreeOptions): Promise<void>;
44
+ /** Request reconciliation for explicit caller actions; never retry or replace their revision. */
45
+ export declare function placeAdminTreeNode(handle: Pick<CollectionHandle, 'placeTreeNode'>, documentId: string, options: PlaceTreeNodeOptions): ReturnType<CollectionHandle['placeTreeNode']>;
46
+ /** Request reconciliation for explicit caller actions; never retry or replace their revision. */
47
+ export declare function removeAdminTreeNode(handle: Pick<CollectionHandle, 'removeFromTree'>, documentId: string, options: RemoveFromTreeOptions): ReturnType<CollectionHandle['removeFromTree']>;
@@ -1,4 +1,5 @@
1
- import { ErrorCodes, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER } from "@byline/core";
1
+ import { ErrorCodes, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, getDocumentRevisionValidationDetails, getDocumentStaleDetails, getLockConflictDetails } from "@byline/core";
2
+ import { structuralReceipt } from "../structural-receipt.js";
2
3
  function hasErrorCode(error, code) {
3
4
  return 'object' == typeof error && null != error && 'code' in error && error.code === code;
4
5
  }
@@ -47,9 +48,14 @@ function shouldSyncAdminTreeRows(rows, preservedRows, isMoving) {
47
48
  return !isMoving && rows !== preservedRows;
48
49
  }
49
50
  async function executeAdminTreeMove(mutate, refresh) {
51
+ let receipt;
50
52
  try {
51
- await mutate();
53
+ receipt = structuralReceipt(await mutate());
52
54
  } catch (error) {
55
+ if (getDocumentStaleDetails(error) || getDocumentRevisionValidationDetails(error) || getLockConflictDetails(error) || hasErrorCode(error, ErrorCodes.NOT_FOUND)) return {
56
+ status: 'mutation-failed',
57
+ error
58
+ };
53
59
  const status = isCommittedTreeHookFailure(error) ? 'committed-hook-failed' : 'mutation-failed';
54
60
  try {
55
61
  await refresh();
@@ -68,12 +74,18 @@ async function executeAdminTreeMove(mutate, refresh) {
68
74
  try {
69
75
  await refresh();
70
76
  return {
71
- status: 'ok'
77
+ status: 'ok',
78
+ ...receipt ? {
79
+ receipt
80
+ } : {}
72
81
  };
73
82
  } catch (error) {
74
83
  return {
75
84
  status: 'refresh-failed',
76
- error
85
+ error,
86
+ ...receipt ? {
87
+ receipt
88
+ } : {}
77
89
  };
78
90
  }
79
91
  }
@@ -100,7 +112,7 @@ function placeAdminTreeNode(handle, documentId, options) {
100
112
  reconcile: options.reconcile ?? true
101
113
  });
102
114
  }
103
- function removeAdminTreeNode(handle, documentId, options = {}) {
115
+ function removeAdminTreeNode(handle, documentId, options) {
104
116
  return handle.removeFromTree(documentId, {
105
117
  ...options,
106
118
  reconcile: options.reconcile ?? true
@@ -1,11 +1,5 @@
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
1
  export declare const placeTreeNode: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
2
+ expectedRevision: number;
9
3
  collection: string;
10
4
  documentId: string;
11
5
  parentDocumentId: string | null;
@@ -13,6 +7,7 @@ export declare const placeTreeNode: import("@tanstack/react-start").RequiredFetc
13
7
  afterDocumentId?: string | null;
14
8
  reconcile?: boolean;
15
9
  }) => {
10
+ expectedRevision: number;
16
11
  collection: string;
17
12
  documentId: string;
18
13
  parentDocumentId: string | null;
@@ -20,18 +15,28 @@ export declare const placeTreeNode: import("@tanstack/react-start").RequiredFetc
20
15
  afterDocumentId?: string | null;
21
16
  reconcile?: boolean;
22
17
  }, Promise<{
23
- status: 'ok';
18
+ documentId: string;
19
+ revision: number;
20
+ affectedDocuments: import("@byline/core").DocumentRevisionReceipt[];
21
+ scheduledPublicationsNeedReconfirmation: boolean;
24
22
  orderKey: string;
23
+ status: 'ok';
25
24
  }>>;
26
25
  export declare const removeFromTree: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
26
+ expectedRevision: number;
27
27
  collection: string;
28
28
  documentId: string;
29
29
  reconcile?: boolean;
30
30
  }) => {
31
+ expectedRevision: number;
31
32
  collection: string;
32
33
  documentId: string;
33
34
  reconcile?: boolean;
34
35
  }, Promise<{
36
+ documentId: string;
37
+ revision: number;
38
+ affectedDocuments: import("@byline/core").DocumentRevisionReceipt[];
39
+ scheduledPublicationsNeedReconfirmation: boolean;
35
40
  status: 'ok';
36
41
  }>>;
37
42
  export declare const getTreeAncestors: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
@@ -53,6 +58,7 @@ export declare const getTreeParent: import("@tanstack/react-start").RequiredFetc
53
58
  documentId: string;
54
59
  }, Promise<import("@byline/client").TreeParentResult>>;
55
60
  export interface CollectionTreeRow {
61
+ revision: number;
56
62
  id: string;
57
63
  /** Parent document id, or `null` for a root or unplaced node. Drives the
58
64
  * client-side tree reconstruction for drag-to-reorder / re-parent. */
@@ -1,12 +1,13 @@
1
+ import { withDocumentMutationErrors } from "../document-mutation-errors.js";
1
2
  import { createServerFn } from "@tanstack/react-start";
2
3
  import { getAdminBylineClient } from "@byline/client/server";
3
4
  import { ERR_NOT_FOUND, createReadContext, getLogger } from "@byline/core";
4
5
  import { ensureCollection } from "../../integrations/api-utils.js";
5
6
  import { placeAdminTreeNode, removeAdminTreeNode } from "./tree-mutation.js";
6
- import { getAdminTreeParent, getAdminUnplacedTreeDocuments } from "./tree-read.js";
7
+ import { getAdminTreeParent } from "./tree-read.js";
7
8
  const placeTreeNode = createServerFn({
8
9
  method: 'POST'
9
- }).validator((input)=>input).handler(async ({ data })=>{
10
+ }).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data })=>{
10
11
  const { collection: path, documentId } = data;
11
12
  const config = await ensureCollection(path);
12
13
  if (!config) throw ERR_NOT_FOUND({
@@ -17,6 +18,7 @@ const placeTreeNode = createServerFn({
17
18
  }).log(getLogger());
18
19
  const handle = getAdminBylineClient().collection(path);
19
20
  const result = await placeAdminTreeNode(handle, documentId, {
21
+ expectedRevision: data.expectedRevision,
20
22
  parentDocumentId: data.parentDocumentId,
21
23
  beforeDocumentId: data.beforeDocumentId ?? null,
22
24
  afterDocumentId: data.afterDocumentId ?? null,
@@ -24,12 +26,12 @@ const placeTreeNode = createServerFn({
24
26
  });
25
27
  return {
26
28
  status: 'ok',
27
- orderKey: result.orderKey
29
+ ...result
28
30
  };
29
- });
31
+ }));
30
32
  const removeFromTree = createServerFn({
31
33
  method: 'POST'
32
- }).validator((input)=>input).handler(async ({ data })=>{
34
+ }).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data })=>{
33
35
  const { collection: path, documentId } = data;
34
36
  const config = await ensureCollection(path);
35
37
  if (!config) throw ERR_NOT_FOUND({
@@ -38,16 +40,18 @@ const removeFromTree = createServerFn({
38
40
  collectionPath: path
39
41
  }
40
42
  }).log(getLogger());
41
- await removeAdminTreeNode(getAdminBylineClient().collection(path), documentId, {
43
+ const result = await removeAdminTreeNode(getAdminBylineClient().collection(path), documentId, {
44
+ expectedRevision: data.expectedRevision,
42
45
  reconcile: data.reconcile
43
46
  });
44
47
  return {
45
- status: 'ok'
48
+ status: 'ok',
49
+ ...result
46
50
  };
47
- });
51
+ }));
48
52
  const getTreeAncestors = createServerFn({
49
53
  method: 'GET'
50
- }).validator((input)=>input).handler(async ({ data })=>{
54
+ }).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data })=>{
51
55
  const { collection: path, documentId } = data;
52
56
  const config = await ensureCollection(path);
53
57
  if (!config) throw ERR_NOT_FOUND({
@@ -69,10 +73,10 @@ const getTreeAncestors = createServerFn({
69
73
  path: doc.path
70
74
  };
71
75
  });
72
- });
76
+ }));
73
77
  const getTreeParent = createServerFn({
74
78
  method: 'GET'
75
- }).validator((input)=>input).handler(async ({ data })=>{
79
+ }).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data })=>{
76
80
  const { collection: path, documentId } = data;
77
81
  const config = await ensureCollection(path);
78
82
  if (!config) throw ERR_NOT_FOUND({
@@ -82,10 +86,10 @@ const getTreeParent = createServerFn({
82
86
  }
83
87
  }).log(getLogger());
84
88
  return getAdminTreeParent(getAdminBylineClient().collection(path), documentId);
85
- });
89
+ }));
86
90
  const getCollectionTree = createServerFn({
87
91
  method: 'GET'
88
- }).validator((input)=>input).handler(async ({ data })=>{
92
+ }).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data })=>{
89
93
  const { collection: path } = data;
90
94
  const config = await ensureCollection(path);
91
95
  if (!config) throw ERR_NOT_FOUND({
@@ -96,8 +100,7 @@ const getCollectionTree = createServerFn({
96
100
  }).log(getLogger());
97
101
  const handle = getAdminBylineClient().collection(path);
98
102
  const readContext = createReadContext();
99
- const forest = await handle.getSubtree({
100
- status: 'any',
103
+ const { forest, unplaced } = await handle.getTreeForEdit({
101
104
  locale: data.locale,
102
105
  _readContext: readContext
103
106
  });
@@ -108,6 +111,7 @@ const getCollectionTree = createServerFn({
108
111
  const doc = node.document;
109
112
  rows.push({
110
113
  id: doc.id,
114
+ revision: doc.revision,
111
115
  parentId,
112
116
  depth,
113
117
  unplaced: false,
@@ -122,9 +126,9 @@ const getCollectionTree = createServerFn({
122
126
  }
123
127
  };
124
128
  walk(forest, 0, null);
125
- const unplaced = await getAdminUnplacedTreeDocuments(handle, placed, readContext, data.locale);
126
129
  for (const doc of unplaced)rows.push({
127
130
  id: doc.id,
131
+ revision: doc.revision,
128
132
  parentId: null,
129
133
  depth: 0,
130
134
  unplaced: true,
@@ -143,5 +147,5 @@ const getCollectionTree = createServerFn({
143
147
  }
144
148
  }
145
149
  };
146
- });
150
+ }));
147
151
  export { getCollectionTree, getTreeAncestors, getTreeParent, placeTreeNode, removeFromTree };
@@ -1,31 +1,30 @@
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
1
  import type { DocumentPatch } from '@byline/core/patches';
9
2
  export declare const updateCollectionDocumentWithPatches: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
10
3
  collection: string;
11
4
  id: string;
12
5
  patches: DocumentPatch[];
13
- versionId?: string;
6
+ expectedRevision: number;
7
+ path?: string | null;
8
+ availableLocales?: string[];
14
9
  locale?: string;
15
10
  }) => {
16
11
  collection: string;
17
12
  id: string;
18
13
  patches: DocumentPatch[];
19
- versionId?: string;
14
+ expectedRevision: number;
15
+ path?: string | null;
16
+ availableLocales?: string[];
20
17
  locale?: string;
21
18
  }, Promise<import("./save-outcome.js").CollectionDocumentCommittedHookFailureResponse | {
22
19
  status: 'ok';
23
20
  documentId: string;
24
- documentVersionId: string;
21
+ documentVersionId?: string | undefined;
22
+ revision: number;
25
23
  }>>;
26
24
  export declare const updateCollectionDocumentSystemFields: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
27
25
  collection: string;
28
26
  id: string;
27
+ expectedRevision: number;
29
28
  locale?: string;
30
29
  /** Path override; `null`/omitted means no path write. */
31
30
  path?: string | null;
@@ -34,11 +33,13 @@ export declare const updateCollectionDocumentSystemFields: import("@tanstack/rea
34
33
  }) => {
35
34
  collection: string;
36
35
  id: string;
36
+ expectedRevision: number;
37
37
  locale?: string;
38
38
  /** Path override; `null`/omitted means no path write. */
39
39
  path?: string | null;
40
40
  /** Editorial advertised-locale set; omitted means no advertised-locale write. */
41
41
  availableLocales?: string[];
42
- }, Promise<{
42
+ }, Promise<import("./save-outcome.js").CollectionDocumentCommittedHookFailureResponse | {
43
43
  status: 'ok';
44
+ revision: number;
44
45
  }>>;
@@ -1,13 +1,14 @@
1
+ import { withDocumentMutationErrors } from "../document-mutation-errors.js";
1
2
  import { createServerFn } from "@tanstack/react-start";
2
3
  import { getAdminRequestContext } from "@byline/client/server";
3
4
  import { ERR_NOT_FOUND, getLogger, getServerConfig } from "@byline/core";
4
- import { updateDocumentSystemFields, updateDocumentWithPatches } from "@byline/core/services";
5
+ import { saveDocument, updateDocumentSystemFields } from "@byline/core/services";
5
6
  import { ensureCollection } from "../../integrations/api-utils.js";
6
7
  import { toCommittedDocumentHookFailureResponse } from "./save-outcome.js";
7
8
  const updateCollectionDocumentWithPatches = createServerFn({
8
9
  method: 'POST'
9
- }).validator((input)=>input).handler(async ({ data: input })=>{
10
- const { collection: path, id, patches, versionId, locale } = input;
10
+ }).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data: input })=>{
11
+ const { collection: path, id, patches, expectedRevision, locale, path: explicitPath, availableLocales } = input;
11
12
  const logger = getLogger();
12
13
  const config = await ensureCollection(path);
13
14
  if (!config) throw ERR_NOT_FOUND({
@@ -30,10 +31,12 @@ const updateCollectionDocumentWithPatches = createServerFn({
30
31
  };
31
32
  let result;
32
33
  try {
33
- result = await updateDocumentWithPatches(ctx, {
34
+ result = await saveDocument(ctx, {
34
35
  documentId: id,
35
36
  patches,
36
- documentVersionId: versionId,
37
+ expectedRevision,
38
+ path: explicitPath ?? void 0,
39
+ availableLocales,
37
40
  locale: locale ?? serverConfig.i18n.content.defaultLocale
38
41
  });
39
42
  } catch (error) {
@@ -44,13 +47,16 @@ const updateCollectionDocumentWithPatches = createServerFn({
44
47
  return {
45
48
  status: 'ok',
46
49
  documentId: result.documentId,
47
- documentVersionId: result.documentVersionId
50
+ ...'documentVersionId' in result ? {
51
+ documentVersionId: result.documentVersionId
52
+ } : {},
53
+ revision: result.revision
48
54
  };
49
- });
55
+ }));
50
56
  const updateCollectionDocumentSystemFields = createServerFn({
51
57
  method: 'POST'
52
- }).validator((input)=>input).handler(async ({ data: input })=>{
53
- const { collection: path, id, locale, path: explicitPath, availableLocales } = input;
58
+ }).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data: input })=>{
59
+ const { collection: path, id, expectedRevision, locale, path: explicitPath, availableLocales } = input;
54
60
  const logger = getLogger();
55
61
  const config = await ensureCollection(path);
56
62
  if (!config) throw ERR_NOT_FOUND({
@@ -71,15 +77,23 @@ const updateCollectionDocumentSystemFields = createServerFn({
71
77
  slugifier: serverConfig.slugifier,
72
78
  requestContext: await getAdminRequestContext()
73
79
  };
74
- await updateDocumentSystemFields(ctx, {
75
- documentId: id,
76
- locale: locale ?? serverConfig.i18n.content.defaultLocale,
77
- path: explicitPath,
78
- availableLocales,
79
- reconcile: true
80
- });
81
- return {
82
- status: 'ok'
83
- };
84
- });
80
+ try {
81
+ const result = await updateDocumentSystemFields(ctx, {
82
+ expectedRevision,
83
+ documentId: id,
84
+ locale: locale ?? serverConfig.i18n.content.defaultLocale,
85
+ path: explicitPath,
86
+ availableLocales,
87
+ reconcile: true
88
+ });
89
+ return {
90
+ status: 'ok',
91
+ revision: result.revision
92
+ };
93
+ } catch (error) {
94
+ const committedFailure = toCommittedDocumentHookFailureResponse(error);
95
+ if (null != committedFailure) return committedFailure;
96
+ throw error;
97
+ }
98
+ }));
85
99
  export { updateCollectionDocumentSystemFields, updateCollectionDocumentWithPatches };
@@ -9,6 +9,7 @@ export interface UploadFieldResult {
9
9
  /** Present when the upload endpoint created a document (createDocument=true, the default). */
10
10
  documentId?: string;
11
11
  documentVersionId?: string;
12
+ revision?: number;
12
13
  storedFile: StoredFileValue;
13
14
  }
14
15
  interface UploadDocumentInput {
@@ -5,6 +5,7 @@ import { extractImageMeta, generateImageVariants, isBypassMimeType } from "@byli
5
5
  import { getLogger, withLogContext } from "@byline/core/logger";
6
6
  import { uploadField } from "@byline/core/services";
7
7
  import { ensureDocumentResource } from "../../integrations/api-utils.js";
8
+ import { withDocumentMutationErrors } from "../document-mutation-errors.js";
8
9
  function parseUploadFormData(data) {
9
10
  if (!(data instanceof FormData)) throw new Error('Expected FormData upload payload.');
10
11
  const collectionEntry = data.get('collection');
@@ -56,7 +57,7 @@ function resolveUploadField(definition, collectionPath, requested) {
56
57
  }
57
58
  const uploadCollectionField = createServerFn({
58
59
  method: 'POST'
59
- }).validator(parseUploadFormData).handler(async ({ data })=>{
60
+ }).validator(parseUploadFormData).handler(withDocumentMutationErrors(async ({ data })=>{
60
61
  const { collectionPath, shouldCreateDocument, fieldName, file, fields } = data;
61
62
  const logger = getLogger();
62
63
  return withLogContext({
@@ -114,7 +115,7 @@ const uploadCollectionField = createServerFn({
114
115
  locale: serverConfig.i18n.content.defaultLocale
115
116
  });
116
117
  });
117
- });
118
+ }));
118
119
  async function upload_uploadField(collection, formData, createDocument = true) {
119
120
  const payload = new FormData();
120
121
  for (const [key, value] of formData.entries())payload.append(key, value);
@@ -0,0 +1,5 @@
1
+ /** Error subclasses lose custom properties in the server-function serializer.
2
+ * Throw only validated plain data for document failures; never transport causes,
3
+ * SQL, hook messages or an unverified current revision. */
4
+ export declare function documentMutationError(error: unknown): unknown;
5
+ export declare function withDocumentMutationErrors<T, R>(handler: (input: T) => Promise<R>): (input: T) => Promise<R>;
@@ -0,0 +1,50 @@
1
+ import { BylineError, ErrorCodes, getDocumentRevisionValidationDetails, getDocumentStaleDetails, getLockConflictDetails } from "@byline/core";
2
+ import { getDocumentHookCommittedDetails } from "@byline/core/services";
3
+ import { committedTreeReceipt } from "./structural-receipt.js";
4
+ function documentMutationError(error) {
5
+ const stale = getDocumentStaleDetails(error);
6
+ if (stale) return {
7
+ code: ErrorCodes.DOCUMENT_STALE,
8
+ message: 'This document has changed. Reload before making changes.',
9
+ details: stale
10
+ };
11
+ const validation = getDocumentRevisionValidationDetails(error);
12
+ if (validation) return {
13
+ code: ErrorCodes.VALIDATION,
14
+ message: 'Reload this document before making changes.',
15
+ details: validation
16
+ };
17
+ const lock = getLockConflictDetails(error);
18
+ if (lock) return {
19
+ code: ErrorCodes.LOCK_CONFLICT,
20
+ message: 'This change could not be saved because another operation was using the document. Reload before trying again.',
21
+ details: lock
22
+ };
23
+ const tree = committedTreeReceipt(error);
24
+ if (tree) return {
25
+ code: ErrorCodes.TREE_HOOK_COMMITTED,
26
+ message: 'The structure was saved, but a follow-up action failed.',
27
+ details: tree
28
+ };
29
+ const committed = getDocumentHookCommittedDetails(error);
30
+ if (committed) return {
31
+ code: ErrorCodes.DOCUMENT_HOOK_COMMITTED,
32
+ message: 'The document was saved, but a follow-up action failed.',
33
+ details: committed
34
+ };
35
+ if (error instanceof BylineError && error.code === ErrorCodes.NOT_FOUND) return {
36
+ code: ErrorCodes.NOT_FOUND,
37
+ message: 'This document is no longer available.'
38
+ };
39
+ return error;
40
+ }
41
+ function withDocumentMutationErrors(handler) {
42
+ return async (input)=>{
43
+ try {
44
+ return await handler(input);
45
+ } catch (error) {
46
+ throw documentMutationError(error);
47
+ }
48
+ };
49
+ }
50
+ export { documentMutationError, withDocumentMutationErrors };
@@ -0,0 +1,9 @@
1
+ /** Restore metadata stripped by schema parsing only onto the exact source version.
2
+ * Never attach by array position: filtering/reordering must not transfer tokens. */
3
+ export declare function withEditableRevision<T extends object>(document: T, observation: {
4
+ id: string;
5
+ versionId: string;
6
+ revision: number;
7
+ } | undefined): T & {
8
+ revision: number;
9
+ };
@@ -0,0 +1,11 @@
1
+ import { ERR_DATABASE, isDocumentRevision } from "@byline/core";
2
+ function withEditableRevision(document, observation) {
3
+ if (!observation || !isDocumentRevision(observation.revision) || Reflect.get(document, 'id') !== observation.id || Reflect.get(document, 'versionId') !== observation.versionId) throw ERR_DATABASE({
4
+ message: 'Editable response no longer matches its source observation'
5
+ });
6
+ return {
7
+ ...document,
8
+ revision: observation.revision
9
+ };
10
+ }
11
+ export { withEditableRevision };
@@ -17,4 +17,4 @@ export interface SingletonDocumentReadInput {
17
17
  * is deliberately composed one layer up, where host database and actor
18
18
  * context are available without widening the singleton transport contract.
19
19
  */
20
- export declare function readSingletonDocument(data: SingletonDocumentReadInput): Promise<import("@byline/client").SingletonDocument<Record<string, any>> | null>;
20
+ export declare function readSingletonDocument(data: SingletonDocumentReadInput): Promise<Omit<import("@byline/client").EditableDocument<Record<string, any>>, "path"> | null>;
@@ -1,10 +1,14 @@
1
1
  import { getAdminBylineClient } from "@byline/client/server";
2
- import { getCollectionDefinition } from "@byline/core";
2
+ import { ERR_NOT_FOUND, getCollectionDefinition } from "@byline/core";
3
3
  import { resolveAdminDocumentRead } from "./admin-document-presentation.js";
4
4
  import { serialise } from "./serialise.js";
5
5
  async function readSingletonDocument(data) {
6
6
  const handle = getAdminBylineClient().singleton(data.singleton);
7
- const { options } = resolveAdminDocumentRead(getCollectionDefinition(data.singleton), data);
8
- return serialise(await handle.get(options));
7
+ const { options: { status: _status, ...options } } = resolveAdminDocumentRead(getCollectionDefinition(data.singleton), data);
8
+ const snapshot = await handle.getForEdit(options);
9
+ if (null == snapshot) throw ERR_NOT_FOUND({
10
+ message: 'Singleton document is unavailable'
11
+ });
12
+ return 'empty' === snapshot.state ? null : serialise(snapshot.document);
9
13
  }
10
14
  export { readSingletonDocument };
@@ -1,12 +1,10 @@
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
1
  export declare const cancelSingletonScheduledPublish: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
2
+ expectedRevision: number;
9
3
  singleton: string;
10
4
  }) => {
5
+ expectedRevision: number;
11
6
  singleton: string;
12
- }, Promise<import("@byline/client").DocumentPublishScheduleInfo | null>>;
7
+ }, Promise<{
8
+ schedule: import("@byline/client").DocumentPublishScheduleInfo | null;
9
+ revision: number;
10
+ }>>;
@@ -1,7 +1,10 @@
1
+ import { withDocumentMutationErrors } from "../document-mutation-errors.js";
1
2
  import { createServerFn } from "@tanstack/react-start";
2
3
  import { getAdminBylineClient } from "@byline/client/server";
3
4
  import { serialise } from "../serialise.js";
4
5
  const cancelSingletonScheduledPublish = createServerFn({
5
6
  method: 'POST'
6
- }).validator((input)=>input).handler(async ({ data })=>serialise(await getAdminBylineClient().singleton(data.singleton).cancelScheduledPublish()));
7
+ }).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data })=>serialise(await getAdminBylineClient().singleton(data.singleton).cancelScheduledPublish({
8
+ expectedRevision: data.expectedRevision
9
+ }))));
7
10
  export { cancelSingletonScheduledPublish };