@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
@@ -0,0 +1,211 @@
1
+ import { createRequire } from 'node:module'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ import {
5
+ ERR_CONFLICT,
6
+ ERR_DOCUMENT_HOOK_COMMITTED,
7
+ ERR_DOCUMENT_STALE,
8
+ ERR_LOCK_CONFLICT,
9
+ ERR_NOT_FOUND,
10
+ ERR_TREE_HOOK_COMMITTED,
11
+ ERR_VALIDATION,
12
+ getDocumentRevisionValidationDetails,
13
+ getDocumentStaleDetails,
14
+ getLockConflictDetails,
15
+ } from '@byline/core'
16
+ import { getDocumentHookCommittedDetails } from '@byline/core/services'
17
+ import { beforeAll, describe, expect, it, vi } from 'vitest'
18
+
19
+ import { documentMutationError, withDocumentMutationErrors } from './document-mutation-errors.js'
20
+
21
+ // Exercise the installed Start HTTP handler and its real Seroval protocol. Only
22
+ // manifest lookup and request-local response state are supplied by this harness;
23
+ // the production application's compiled actions are covered by browser tests.
24
+ const require = createRequire(import.meta.url)
25
+ const startRequire = createRequire(require.resolve('@tanstack/react-start/package.json'))
26
+ const serverPackage = startRequire.resolve('@tanstack/start-server-core/package.json')
27
+ const esm = join(dirname(serverPackage), 'src')
28
+ const serverRequire = createRequire(serverPackage)
29
+ let handler: (input: { request: Request; context: object; serverFnId: string }) => Promise<Response>
30
+ let seroval: {
31
+ toJSONAsync(value: unknown): Promise<unknown>
32
+ fromCrossJSON(value: unknown, options: object): unknown
33
+ }
34
+ let runWithStartContext: (context: object, fn: () => Promise<Response>) => Promise<Response>
35
+ let action: (input: { data: unknown }) => Promise<unknown>
36
+
37
+ beforeAll(async () => {
38
+ vi.doMock(join(esm, 'getServerFnById.ts'), () => ({ getServerFnById: async () => action }))
39
+ vi.doMock(join(esm, 'request-response.ts'), () => ({
40
+ getResponse: () => ({ status: 200, headers: new Headers() }),
41
+ }))
42
+ handler = (await import(join(esm, 'server-functions-handler.ts'))).handleServerAction
43
+ seroval = await import(serverRequire.resolve('seroval'))
44
+ runWithStartContext = (
45
+ await import(
46
+ join(
47
+ dirname(serverRequire.resolve('@tanstack/start-storage-context/package.json')),
48
+ 'dist/esm/index.js'
49
+ )
50
+ )
51
+ ).runWithStartContext
52
+ })
53
+
54
+ async function roundTrip(data: unknown, operation: (data: unknown) => Promise<unknown>) {
55
+ const guarded = withDocumentMutationErrors(operation)
56
+ action = async ({ data }) => {
57
+ try {
58
+ return { result: await guarded(data) }
59
+ } catch (error) {
60
+ return { error }
61
+ }
62
+ }
63
+ const request = new Request('http://localhost/_serverFn/document-mutation', {
64
+ method: 'POST',
65
+ headers: { 'content-type': 'application/json', 'x-tsr-serverFn': 'true' },
66
+ body: JSON.stringify(await seroval.toJSONAsync({ data })),
67
+ })
68
+ const response = await runWithStartContext(
69
+ {
70
+ request,
71
+ startOptions: {},
72
+ contextAfterGlobalMiddlewares: {},
73
+ executedRequestMiddlewares: new Set(),
74
+ handlerType: 'serverFn',
75
+ getRouter: () => {
76
+ throw new Error('Router not used')
77
+ },
78
+ },
79
+ () => handler({ request, context: {}, serverFnId: 'document-mutation' })
80
+ )
81
+ const wire = await response.text()
82
+ const decoded = seroval.fromCrossJSON(JSON.parse(wire), {})
83
+ if (typeof decoded !== 'object' || decoded === null) throw new Error('Expected wire envelope')
84
+ return {
85
+ value: { error: Reflect.get(decoded, 'error'), result: Reflect.get(decoded, 'result') },
86
+ wire,
87
+ }
88
+ }
89
+
90
+ const staleCases = [
91
+ { reason: 'revision_mismatch', documentId: 'doc', expectedRevision: 1, currentRevision: 2 },
92
+ {
93
+ reason: 'version_parent_mismatch',
94
+ documentId: 'doc',
95
+ previousVersionId: 'old',
96
+ currentVersionId: 'new',
97
+ },
98
+ {
99
+ reason: 'singleton_slot_changed',
100
+ singletonPath: 'settings',
101
+ expectedState: 'empty',
102
+ currentState: 'document',
103
+ },
104
+ ] as const
105
+
106
+ describe('document failures over Start HTTP serialization', () => {
107
+ for (const details of staleCases) {
108
+ it(`retains ${details.reason} and excludes diagnostics`, async () => {
109
+ const { value, wire } = await roundTrip({}, async () => {
110
+ throw ERR_DOCUMENT_STALE({
111
+ message: 'private driver detail',
112
+ details,
113
+ cause: new Error('SELECT secret'),
114
+ logExtra: { private: true },
115
+ })
116
+ })
117
+ expect(getDocumentStaleDetails(value.error)).toEqual(details)
118
+ expect(wire).not.toMatch(/private driver|SELECT secret|logExtra/)
119
+ })
120
+ }
121
+ for (const reason of ['missing_document_revision', 'invalid_document_revision'] as const) {
122
+ it(`retains reload-required validation: ${reason}`, async () => {
123
+ const { value } = await roundTrip({}, async () => {
124
+ throw ERR_VALIDATION({ message: 'internal', details: { reason } })
125
+ })
126
+ expect(getDocumentRevisionValidationDetails(value.error)).toEqual({ reason })
127
+ expect(getDocumentStaleDetails(value.error)).toBeNull()
128
+ })
129
+ }
130
+ it('retains a confirmed rollback without relabelling it stale', async () => {
131
+ const details = { reason: 'lock_conflict', rolledBack: true, retryable: true }
132
+ const { value, wire } = await roundTrip({}, async () => {
133
+ throw ERR_LOCK_CONFLICT({
134
+ message: 'driver message',
135
+ details,
136
+ cause: new Error('SQL private'),
137
+ })
138
+ })
139
+ expect(getLockConflictDetails(value.error)).toEqual(details)
140
+ expect(getDocumentStaleDetails(value.error)).toBeNull()
141
+ expect(wire).not.toMatch(/driver message|SQL private/)
142
+ })
143
+ it('keeps committed hook warnings separate and preserves their committed revision', async () => {
144
+ const details = {
145
+ phase: 'afterUpdate',
146
+ documentId: 'doc',
147
+ documentVersionId: 'version',
148
+ revision: 3,
149
+ sideEffectCode: 'ERR_UNHANDLED',
150
+ }
151
+ const { value, wire } = await roundTrip({}, async () => {
152
+ throw ERR_DOCUMENT_HOOK_COMMITTED({
153
+ message: 'private hook failure',
154
+ details,
155
+ cause: new Error('secret hook'),
156
+ })
157
+ })
158
+ expect(getDocumentHookCommittedDetails(value.error)).toEqual(details)
159
+ expect(getDocumentStaleDetails(value.error)).toBeNull()
160
+ expect(getLockConflictDetails(value.error)).toBeNull()
161
+ expect(wire).not.toMatch(/private hook failure|secret hook/)
162
+ })
163
+ it('round-trips safe integer observations and receipts as numbers', async () => {
164
+ const { value } = await roundTrip(
165
+ { expectedRevision: Number.MAX_SAFE_INTEGER - 1 },
166
+ async (data) => {
167
+ expect(data).toEqual({ expectedRevision: Number.MAX_SAFE_INTEGER - 1 })
168
+ return { documentId: 'doc', revision: Number.MAX_SAFE_INTEGER }
169
+ }
170
+ )
171
+ expect(value.result).toEqual({ documentId: 'doc', revision: Number.MAX_SAFE_INTEGER })
172
+ })
173
+ it('does not classify unrelated or malformed conflicts by message', () => {
174
+ const conflict = ERR_CONFLICT({ message: 'This document has changed.' })
175
+ expect(documentMutationError(conflict)).toBe(conflict)
176
+ const malformed = { code: 'ERR_DOCUMENT_STALE', details: { reason: 'revision_mismatch' } }
177
+ expect(documentMutationError(malformed)).toBe(malformed)
178
+ })
179
+ })
180
+
181
+ it('transports the authorized structural receipt on committed tree hook failure without diagnostics', async () => {
182
+ const receipt = {
183
+ documentId: 'doc',
184
+ revision: 3,
185
+ affectedDocuments: [{ documentId: 'allowed-sibling', revision: 4 }],
186
+ scheduledPublicationsNeedReconfirmation: true,
187
+ }
188
+ const { value, wire } = await roundTrip({}, async () => {
189
+ throw ERR_TREE_HOOK_COMMITTED({
190
+ message: 'private hook diagnostics',
191
+ cause: new Error('SQL secret'),
192
+ details: { ...receipt, change: 'reorder', affectedDocumentIds: ['private'], internal: true },
193
+ })
194
+ })
195
+ expect(value.error).toEqual({
196
+ code: 'ERR_TREE_HOOK_COMMITTED',
197
+ message: 'The structure was saved, but a follow-up action failed.',
198
+ details: receipt,
199
+ })
200
+ expect(wire).not.toMatch(/private|SQL secret|internal/)
201
+ })
202
+ it('transports unavailable-document errors without provider details', async () => {
203
+ const { value, wire } = await roundTrip({}, async () => {
204
+ throw ERR_NOT_FOUND({ message: 'private unavailable reason', details: { internal: true } })
205
+ })
206
+ expect(value.error).toEqual({
207
+ code: 'ERR_NOT_FOUND',
208
+ message: 'This document is no longer available.',
209
+ })
210
+ expect(wire).not.toMatch(/private|internal/)
211
+ })
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { withEditableRevision } from './editable-revision.js'
4
+
5
+ describe('editable response revision binding', () => {
6
+ const observed = { id: 'document', versionId: 'current', revision: 7 }
7
+ it('restores only the source revision without changing populated targets', () => {
8
+ const related = { id: 'related', versionId: 'published', fields: {} }
9
+ expect(
10
+ withEditableRevision({ id: 'document', versionId: 'current', fields: { related } }, observed)
11
+ ).toEqual({ id: 'document', versionId: 'current', revision: 7, fields: { related } })
12
+ expect(related).not.toHaveProperty('revision')
13
+ })
14
+ it.each([
15
+ { id: 'document', versionId: 'historical' },
16
+ { id: 'another-document', versionId: 'current' },
17
+ ])('rejects a transferred observation: %o', (document) => {
18
+ expect(() => withEditableRevision(document, observed)).toThrow('source observation')
19
+ })
20
+ it('rejects a row removed from the editable result', () => {
21
+ expect(() => withEditableRevision(observed, undefined)).toThrow('source observation')
22
+ })
23
+ })
@@ -0,0 +1,18 @@
1
+ import { ERR_DATABASE, isDocumentRevision } from '@byline/core'
2
+
3
+ /** Restore metadata stripped by schema parsing only onto the exact source version.
4
+ * Never attach by array position: filtering/reordering must not transfer tokens. */
5
+ export function withEditableRevision<T extends object>(
6
+ document: T,
7
+ observation: { id: string; versionId: string; revision: number } | undefined
8
+ ): T & { revision: number } {
9
+ if (
10
+ !observation ||
11
+ !isDocumentRevision(observation.revision) ||
12
+ Reflect.get(document, 'id') !== observation.id ||
13
+ Reflect.get(document, 'versionId') !== observation.versionId
14
+ ) {
15
+ throw ERR_DATABASE({ message: 'Editable response no longer matches its source observation' })
16
+ }
17
+ return { ...document, revision: observation.revision }
18
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { getAdminBylineClient } from '@byline/client/server'
10
- import { getCollectionDefinition } from '@byline/core'
10
+ import { ERR_NOT_FOUND, getCollectionDefinition } from '@byline/core'
11
11
 
12
12
  import { resolveAdminDocumentRead } from './admin-document-presentation.js'
13
13
  import { serialise } from './serialise.js'
@@ -27,6 +27,10 @@ export interface SingletonDocumentReadInput {
27
27
  */
28
28
  export async function readSingletonDocument(data: SingletonDocumentReadInput) {
29
29
  const handle = getAdminBylineClient().singleton(data.singleton)
30
- const { options } = resolveAdminDocumentRead(getCollectionDefinition(data.singleton), data)
31
- return serialise(await handle.get(options))
30
+ const {
31
+ options: { status: _status, ...options },
32
+ } = resolveAdminDocumentRead(getCollectionDefinition(data.singleton), data)
33
+ const snapshot = await handle.getForEdit(options)
34
+ if (snapshot == null) throw ERR_NOT_FOUND({ message: 'Singleton document is unavailable' })
35
+ return snapshot.state === 'empty' ? null : serialise(snapshot.document)
32
36
  }
@@ -1,3 +1,4 @@
1
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
1
2
  /**
2
3
  * This Source Code is subject to the terms of the Mozilla Public
3
4
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,7 +14,13 @@ import { getAdminBylineClient } from '@byline/client/server'
13
14
  import { serialise } from '../serialise.js'
14
15
 
15
16
  export const cancelSingletonScheduledPublish = createServerFn({ method: 'POST' })
16
- .validator((input: { singleton: string }) => input)
17
- .handler(async ({ data }) =>
18
- serialise(await getAdminBylineClient().singleton(data.singleton).cancelScheduledPublish())
17
+ .validator((input: { expectedRevision: number; singleton: string }) => input)
18
+ .handler(
19
+ withDocumentMutationErrors(async ({ data }) =>
20
+ serialise(
21
+ await getAdminBylineClient()
22
+ .singleton(data.singleton)
23
+ .cancelScheduledPublish({ expectedRevision: data.expectedRevision })
24
+ )
25
+ )
19
26
  )
@@ -1,3 +1,4 @@
1
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
1
2
  /**
2
3
  * This Source Code is subject to the terms of the Mozilla Public
3
4
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,7 +14,13 @@ import { getAdminBylineClient } from '@byline/client/server'
13
14
  import { serialise } from '../serialise.js'
14
15
 
15
16
  export const changeSingletonStatus = createServerFn({ method: 'POST' })
16
- .validator((input: { singleton: string; status: string }) => input)
17
- .handler(async ({ data }) =>
18
- serialise(await getAdminBylineClient().singleton(data.singleton).changeStatus(data.status))
17
+ .validator((input: { expectedRevision: number; singleton: string; status: string }) => input)
18
+ .handler(
19
+ withDocumentMutationErrors(async ({ data }) =>
20
+ serialise(
21
+ await getAdminBylineClient()
22
+ .singleton(data.singleton)
23
+ .changeStatus(data.status, { expectedRevision: data.expectedRevision })
24
+ )
25
+ )
19
26
  )
@@ -1,3 +1,4 @@
1
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
1
2
  /**
2
3
  * This Source Code is subject to the terms of the Mozilla Public
3
4
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,11 +14,16 @@ import { getAdminBylineClient } from '@byline/client/server'
13
14
  import { serialise } from '../serialise.js'
14
15
 
15
16
  export const confirmSingletonScheduledPublish = createServerFn({ method: 'POST' })
16
- .validator((input: { singleton: string; expectedVersionId: string }) => input)
17
- .handler(async ({ data }) =>
18
- serialise(
19
- await getAdminBylineClient()
20
- .singleton(data.singleton)
21
- .confirmScheduledPublish({ expectedVersionId: data.expectedVersionId })
17
+ .validator(
18
+ (input: { expectedRevision: number; singleton: string; expectedVersionId: string }) => input
19
+ )
20
+ .handler(
21
+ withDocumentMutationErrors(async ({ data }) =>
22
+ serialise(
23
+ await getAdminBylineClient().singleton(data.singleton).confirmScheduledPublish({
24
+ expectedRevision: data.expectedRevision,
25
+ expectedVersionId: data.expectedVersionId,
26
+ })
27
+ )
22
28
  )
23
29
  )
@@ -1,3 +1,4 @@
1
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
1
2
  /**
2
3
  * This Source Code is subject to the terms of the Mozilla Public
3
4
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -16,24 +17,28 @@ import { serialise } from '../serialise.js'
16
17
  export const copySingletonToLocale = createServerFn({ method: 'POST' })
17
18
  .validator(
18
19
  (input: {
20
+ expectedRevision: number
19
21
  singleton: string
20
22
  sourceLocale: string
21
23
  targetLocale: string
22
24
  overwrite?: boolean
23
25
  }) => input
24
26
  )
25
- .handler(async ({ data }) => {
26
- try {
27
- return serialise(
28
- await getAdminBylineClient().singleton(data.singleton).copyToLocale({
29
- sourceLocale: data.sourceLocale,
30
- targetLocale: data.targetLocale,
31
- overwrite: data.overwrite,
32
- })
33
- )
34
- } catch (error) {
35
- const committedFailure = toCommittedDocumentHookFailureResponse(error)
36
- if (committedFailure != null) return committedFailure
37
- throw error
38
- }
39
- })
27
+ .handler(
28
+ withDocumentMutationErrors(async ({ data }) => {
29
+ try {
30
+ return serialise(
31
+ await getAdminBylineClient().singleton(data.singleton).copyToLocale({
32
+ expectedRevision: data.expectedRevision,
33
+ sourceLocale: data.sourceLocale,
34
+ targetLocale: data.targetLocale,
35
+ overwrite: data.overwrite,
36
+ })
37
+ )
38
+ } catch (error) {
39
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
40
+ if (committedFailure != null) return committedFailure
41
+ throw error
42
+ }
43
+ })
44
+ )
@@ -23,9 +23,9 @@ export interface GetSingletonInput {
23
23
  * - this shared host layer owns lenient reads, raw missing-locale values,
24
24
  * relation-summary population, transport serialization, and preservation of
25
25
  * restore warnings and locale metadata;
26
- * - the route loader composes published-version metadata plus scheduled-
27
- * publication enablement, capability, and schedule aliases because those
28
- * require host-only database and actor context beyond the singleton handle.
26
+ * - the route loader composes the informational published-version badge, feature
27
+ * enablement, capability, and schedule aliases. Schedule state itself comes
28
+ * from the same editable snapshot as the source document.
29
29
  */
30
30
  export const getSingleton = createServerFn({ method: 'GET' })
31
31
  .validator((input: GetSingletonInput) => input)
@@ -1,3 +1,4 @@
1
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
1
2
  /**
2
3
  * This Source Code is subject to the terms of the Mozilla Public
3
4
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -14,15 +15,19 @@ import { toCommittedDocumentHookFailureResponse } from '../collections/save-outc
14
15
  import { serialise } from '../serialise.js'
15
16
 
16
17
  export const restoreSingletonVersion = createServerFn({ method: 'POST' })
17
- .validator((input: { singleton: string; versionId: string }) => input)
18
- .handler(async ({ data }) => {
19
- try {
20
- return serialise(
21
- await getAdminBylineClient().singleton(data.singleton).restoreVersion(data.versionId)
22
- )
23
- } catch (error) {
24
- const committedFailure = toCommittedDocumentHookFailureResponse(error)
25
- if (committedFailure != null) return committedFailure
26
- throw error
27
- }
28
- })
18
+ .validator((input: { expectedRevision: number; singleton: string; versionId: string }) => input)
19
+ .handler(
20
+ withDocumentMutationErrors(async ({ data }) => {
21
+ try {
22
+ return serialise(
23
+ await getAdminBylineClient()
24
+ .singleton(data.singleton)
25
+ .restoreVersion(data.versionId, { expectedRevision: data.expectedRevision })
26
+ )
27
+ } catch (error) {
28
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
29
+ if (committedFailure != null) return committedFailure
30
+ throw error
31
+ }
32
+ })
33
+ )
@@ -1,3 +1,4 @@
1
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
1
2
  /**
2
3
  * This Source Code is subject to the terms of the Mozilla Public
3
4
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,12 +14,22 @@ import { getAdminBylineClient } from '@byline/client/server'
13
14
  import { serialise } from '../serialise.js'
14
15
 
15
16
  export const scheduleSingletonPublish = createServerFn({ method: 'POST' })
16
- .validator((input: { singleton: string; publishAt: string; expectedVersionId: string }) => input)
17
- .handler(async ({ data }) =>
18
- serialise(
19
- await getAdminBylineClient().singleton(data.singleton).schedulePublish({
20
- publishAt: data.publishAt,
21
- expectedVersionId: data.expectedVersionId,
22
- })
17
+ .validator(
18
+ (input: {
19
+ expectedRevision: number
20
+ singleton: string
21
+ publishAt: string
22
+ expectedVersionId: string
23
+ }) => input
24
+ )
25
+ .handler(
26
+ withDocumentMutationErrors(async ({ data }) =>
27
+ serialise(
28
+ await getAdminBylineClient().singleton(data.singleton).schedulePublish({
29
+ publishAt: data.publishAt,
30
+ expectedRevision: data.expectedRevision,
31
+ expectedVersionId: data.expectedVersionId,
32
+ })
33
+ )
23
34
  )
24
35
  )