@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
  /**
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
@@ -11,22 +12,15 @@ import { createServerFn } from '@tanstack/react-start'
11
12
  import { getAdminRequestContext } from '@byline/client/server'
12
13
  import { ERR_NOT_FOUND, getLogger, getServerConfig } from '@byline/core'
13
14
  import type { DocumentPatch } from '@byline/core/patches'
14
- import type {
15
- DocumentLifecycleContext,
16
- UpdateDocumentWithPatchesResult,
17
- } from '@byline/core/services'
18
- import { updateDocumentSystemFields, updateDocumentWithPatches } from '@byline/core/services'
15
+ import type { DocumentLifecycleContext } from '@byline/core/services'
16
+ import { saveDocument, updateDocumentSystemFields } from '@byline/core/services'
19
17
 
20
18
  import { ensureCollection } from '../../integrations/api-utils.js'
21
19
  import { toCommittedDocumentHookFailureResponse } from './save-outcome.js'
22
20
 
23
21
  // ---------------------------------------------------------------------------
24
- // Apply patches (patch-based update — creates a new immutable version)
25
- //
26
- // Document-grain system fields (`path`, `availableLocales`) are deliberately
27
- // NOT handled here: they are written through their own non-versioned path
28
- // (`updateCollectionDocumentSystemFields` below) so that editing them does not
29
- // mint a new version or reset workflow status. See docs/08-internationalization/index.md.
22
+ // Save content patches and optional metadata in one guarded transaction.
23
+ // Empty patches retain the non-versioned metadata-only behavior.
30
24
  // ---------------------------------------------------------------------------
31
25
 
32
26
  export const updateCollectionDocumentWithPatches = createServerFn({ method: 'POST' })
@@ -35,54 +29,69 @@ export const updateCollectionDocumentWithPatches = createServerFn({ method: 'POS
35
29
  collection: string
36
30
  id: string
37
31
  patches: DocumentPatch[]
38
- versionId?: string
32
+ expectedRevision: number
33
+ path?: string | null
34
+ availableLocales?: string[]
39
35
  locale?: string
40
36
  }) => input
41
37
  )
42
- .handler(async ({ data: input }) => {
43
- const { collection: path, id, patches, versionId, locale } = input
44
- const logger = getLogger()
45
- const config = await ensureCollection(path)
46
- if (!config) {
47
- throw ERR_NOT_FOUND({
48
- message: 'Collection not found',
49
- details: { collectionPath: path },
50
- }).log(logger)
51
- }
38
+ .handler(
39
+ withDocumentMutationErrors(async ({ data: input }) => {
40
+ const {
41
+ collection: path,
42
+ id,
43
+ patches,
44
+ expectedRevision,
45
+ locale,
46
+ path: explicitPath,
47
+ availableLocales,
48
+ } = input
49
+ const logger = getLogger()
50
+ const config = await ensureCollection(path)
51
+ if (!config) {
52
+ throw ERR_NOT_FOUND({
53
+ message: 'Collection not found',
54
+ details: { collectionPath: path },
55
+ }).log(logger)
56
+ }
52
57
 
53
- const serverConfig = getServerConfig()
54
- const ctx: DocumentLifecycleContext = {
55
- db: serverConfig.db,
56
- definition: config.definition,
57
- collectionId: config.collection.id,
58
- collectionVersion: config.collection.version,
59
- collectionPath: path,
60
- logger,
61
- defaultLocale: serverConfig.i18n.content.defaultLocale,
62
- slugifier: serverConfig.slugifier,
63
- requestContext: await getAdminRequestContext(),
64
- }
58
+ const serverConfig = getServerConfig()
59
+ const ctx: DocumentLifecycleContext = {
60
+ db: serverConfig.db,
61
+ definition: config.definition,
62
+ collectionId: config.collection.id,
63
+ collectionVersion: config.collection.version,
64
+ collectionPath: path,
65
+ logger,
66
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
67
+ slugifier: serverConfig.slugifier,
68
+ requestContext: await getAdminRequestContext(),
69
+ }
65
70
 
66
- let result: UpdateDocumentWithPatchesResult
67
- try {
68
- result = await updateDocumentWithPatches(ctx, {
69
- documentId: id,
70
- patches,
71
- documentVersionId: versionId,
72
- locale: locale ?? serverConfig.i18n.content.defaultLocale,
73
- })
74
- } catch (error) {
75
- const committedFailure = toCommittedDocumentHookFailureResponse(error)
76
- if (committedFailure != null) return committedFailure
77
- throw error
78
- }
71
+ let result: Awaited<ReturnType<typeof saveDocument>>
72
+ try {
73
+ result = await saveDocument(ctx, {
74
+ documentId: id,
75
+ patches,
76
+ expectedRevision,
77
+ path: explicitPath ?? undefined,
78
+ availableLocales,
79
+ locale: locale ?? serverConfig.i18n.content.defaultLocale,
80
+ })
81
+ } catch (error) {
82
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
83
+ if (committedFailure != null) return committedFailure
84
+ throw error
85
+ }
79
86
 
80
- return {
81
- status: 'ok' as const,
82
- documentId: result.documentId,
83
- documentVersionId: result.documentVersionId,
84
- }
85
- })
87
+ return {
88
+ status: 'ok' as const,
89
+ documentId: result.documentId,
90
+ ...('documentVersionId' in result ? { documentVersionId: result.documentVersionId } : {}),
91
+ revision: result.revision,
92
+ }
93
+ })
94
+ )
86
95
 
87
96
  // ---------------------------------------------------------------------------
88
97
  // System-managed, document-grain fields (path + advertised locales)
@@ -97,6 +106,7 @@ export const updateCollectionDocumentSystemFields = createServerFn({ method: 'PO
97
106
  (input: {
98
107
  collection: string
99
108
  id: string
109
+ expectedRevision: number
100
110
  locale?: string
101
111
  /** Path override; `null`/omitted means no path write. */
102
112
  path?: string | null
@@ -104,39 +114,53 @@ export const updateCollectionDocumentSystemFields = createServerFn({ method: 'PO
104
114
  availableLocales?: string[]
105
115
  }) => input
106
116
  )
107
- .handler(async ({ data: input }) => {
108
- const { collection: path, id, locale, path: explicitPath, availableLocales } = input
109
- const logger = getLogger()
110
- const config = await ensureCollection(path)
111
- if (!config) {
112
- throw ERR_NOT_FOUND({
113
- message: 'Collection not found',
114
- details: { collectionPath: path },
115
- }).log(logger)
116
- }
117
+ .handler(
118
+ withDocumentMutationErrors(async ({ data: input }) => {
119
+ const {
120
+ collection: path,
121
+ id,
122
+ expectedRevision,
123
+ locale,
124
+ path: explicitPath,
125
+ availableLocales,
126
+ } = input
127
+ const logger = getLogger()
128
+ const config = await ensureCollection(path)
129
+ if (!config) {
130
+ throw ERR_NOT_FOUND({
131
+ message: 'Collection not found',
132
+ details: { collectionPath: path },
133
+ }).log(logger)
134
+ }
117
135
 
118
- const serverConfig = getServerConfig()
119
- const ctx: DocumentLifecycleContext = {
120
- db: serverConfig.db,
121
- definition: config.definition,
122
- collectionId: config.collection.id,
123
- collectionVersion: config.collection.version,
124
- collectionPath: path,
125
- logger,
126
- defaultLocale: serverConfig.i18n.content.defaultLocale,
127
- slugifier: serverConfig.slugifier,
128
- requestContext: await getAdminRequestContext(),
129
- }
136
+ const serverConfig = getServerConfig()
137
+ const ctx: DocumentLifecycleContext = {
138
+ db: serverConfig.db,
139
+ definition: config.definition,
140
+ collectionId: config.collection.id,
141
+ collectionVersion: config.collection.version,
142
+ collectionPath: path,
143
+ logger,
144
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
145
+ slugifier: serverConfig.slugifier,
146
+ requestContext: await getAdminRequestContext(),
147
+ }
130
148
 
131
- await updateDocumentSystemFields(ctx, {
132
- documentId: id,
133
- locale: locale ?? serverConfig.i18n.content.defaultLocale,
134
- path: explicitPath,
135
- availableLocales,
136
- // A failed after-hook happens after commit. Retrying this same admin save
137
- // must re-run cache/search reconciliation when DB values are now current.
138
- reconcile: true,
149
+ try {
150
+ const result = await updateDocumentSystemFields(ctx, {
151
+ expectedRevision,
152
+ documentId: id,
153
+ locale: locale ?? serverConfig.i18n.content.defaultLocale,
154
+ path: explicitPath,
155
+ availableLocales,
156
+ // Reconciliation still requires a current observation.
157
+ reconcile: true,
158
+ })
159
+ return { status: 'ok' as const, revision: result.revision }
160
+ } catch (error) {
161
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
162
+ if (committedFailure != null) return committedFailure
163
+ throw error
164
+ }
139
165
  })
140
-
141
- return { status: 'ok' as const }
142
- })
166
+ )
@@ -1,3 +1,4 @@
1
+ import { testAdapter } from '../../../../core/src/storage/db-adapter.test-helper.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
@@ -7,12 +8,7 @@
7
8
  */
8
9
 
9
10
  import { AdminAuth, createRequestContext } from '@byline/auth'
10
- import {
11
- defineServerConfig,
12
- defineSingleton,
13
- type IDbAdapter,
14
- type IStorageProvider,
15
- } from '@byline/core'
11
+ import { defineServerConfig, defineSingleton, type IStorageProvider } from '@byline/core'
16
12
  import { defineLogger } from '@byline/core/logger'
17
13
  import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
18
14
 
@@ -112,7 +108,7 @@ beforeAll(() => {
112
108
  )
113
109
  defineServerConfig({
114
110
  collections: [singleton],
115
- db: {} as IDbAdapter,
111
+ db: testAdapter(),
116
112
  storage,
117
113
  i18n: {
118
114
  admin: { defaultLocale: 'en', locales: ['en'] },
@@ -14,6 +14,7 @@ import { getLogger, withLogContext } from '@byline/core/logger'
14
14
  import { uploadField as coreUploadField } from '@byline/core/services'
15
15
 
16
16
  import { ensureDocumentResource } from '../../integrations/api-utils.js'
17
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
17
18
 
18
19
  /**
19
20
  * Result of an upload through the host transport. The legacy top-level
@@ -25,6 +26,7 @@ export interface UploadFieldResult {
25
26
  /** Present when the upload endpoint created a document (createDocument=true, the default). */
26
27
  documentId?: string
27
28
  documentVersionId?: string
29
+ revision?: number
28
30
  storedFile: StoredFileValue
29
31
  }
30
32
 
@@ -149,88 +151,90 @@ function resolveUploadField(
149
151
  */
150
152
  export const uploadCollectionField = createServerFn({ method: 'POST' })
151
153
  .validator(parseUploadFormData)
152
- .handler(async ({ data }) => {
153
- const { collectionPath, shouldCreateDocument, fieldName, file, fields } = data
154
- const logger = getLogger()
154
+ .handler(
155
+ withDocumentMutationErrors(async ({ data }) => {
156
+ const { collectionPath, shouldCreateDocument, fieldName, file, fields } = data
157
+ const logger = getLogger()
155
158
 
156
- return withLogContext(
157
- { domain: 'api', module: 'upload', function: 'uploadCollectionField' },
158
- async () => {
159
- // Upload is the one field transport shared by both document-resource
160
- // kinds. The core service selects the kind-aware ability and rejects
161
- // singleton document creation before hooks or storage; the admin form
162
- // always requests the field-only branch (`createDocument=false`).
163
- const config = await ensureDocumentResource(collectionPath)
164
- if (config == null) {
165
- throw ERR_NOT_FOUND(
166
- {
167
- message: 'Document resource not found.',
168
- details: { collectionPath },
169
- },
170
- uploadCollectionField
171
- ).log(logger)
172
- }
159
+ return withLogContext(
160
+ { domain: 'api', module: 'upload', function: 'uploadCollectionField' },
161
+ async () => {
162
+ // Upload is the one field transport shared by both document-resource
163
+ // kinds. The core service selects the kind-aware ability and rejects
164
+ // singleton document creation before hooks or storage; the admin form
165
+ // always requests the field-only branch (`createDocument=false`).
166
+ const config = await ensureDocumentResource(collectionPath)
167
+ if (config == null) {
168
+ throw ERR_NOT_FOUND(
169
+ {
170
+ message: 'Document resource not found.',
171
+ details: { collectionPath },
172
+ },
173
+ uploadCollectionField
174
+ ).log(logger)
175
+ }
173
176
 
174
- const serverConfig = getServerConfig()
175
- const targetField = resolveUploadField(config.definition, collectionPath, fieldName)
176
- // Per-field storage routing falls through to the site-wide default.
177
- const storage = targetField.upload?.storage ?? serverConfig.storage
178
- if (!storage) {
179
- throw new Error(
180
- `No storage provider configured for field '${targetField.name}' on collection ` +
181
- `'${collectionPath}'. Set either field.upload.storage or the site-wide ` +
182
- 'ServerConfig.storage.'
183
- )
184
- }
177
+ const serverConfig = getServerConfig()
178
+ const targetField = resolveUploadField(config.definition, collectionPath, fieldName)
179
+ // Per-field storage routing falls through to the site-wide default.
180
+ const storage = targetField.upload?.storage ?? serverConfig.storage
181
+ if (!storage) {
182
+ throw new Error(
183
+ `No storage provider configured for field '${targetField.name}' on collection ` +
184
+ `'${collectionPath}'. Set either field.upload.storage or the site-wide ` +
185
+ 'ServerConfig.storage.'
186
+ )
187
+ }
185
188
 
186
- let buffer: Buffer
187
- try {
188
- buffer = Buffer.from(await file.arrayBuffer())
189
- } catch (err: unknown) {
190
- logger.error({ err, collectionPath }, 'failed to read file buffer')
191
- throw new Error('Failed to read uploaded file.')
192
- }
189
+ let buffer: Buffer
190
+ try {
191
+ buffer = Buffer.from(await file.arrayBuffer())
192
+ } catch (err: unknown) {
193
+ logger.error({ err, collectionPath }, 'failed to read file buffer')
194
+ throw new Error('Failed to read uploaded file.')
195
+ }
193
196
 
194
- const ctx: FieldUploadContext = {
195
- db: serverConfig.db,
196
- definition: config.definition,
197
- collectionId: config.collection.id,
198
- collectionVersion: config.collection.version,
199
- collectionPath,
200
- fieldName: targetField.name,
201
- storage,
202
- logger,
203
- defaultLocale: serverConfig.i18n.content.defaultLocale,
204
- slugifier: serverConfig.slugifier,
205
- filenameSlugifier: serverConfig.uploads?.filenameSlugifier,
206
- requestContext: await getAdminRequestContext(),
207
- imageProcessor: {
208
- extractMeta: extractImageMeta,
209
- isBypassMimeType,
210
- generateVariants: ({ buffer, mimeType, storedFile, storage, upload, logger }) =>
211
- generateImageVariants(
212
- buffer,
213
- mimeType,
214
- storedFile,
215
- storage,
216
- upload.sizes ?? [],
217
- logger
218
- ),
219
- },
220
- }
197
+ const ctx: FieldUploadContext = {
198
+ db: serverConfig.db,
199
+ definition: config.definition,
200
+ collectionId: config.collection.id,
201
+ collectionVersion: config.collection.version,
202
+ collectionPath,
203
+ fieldName: targetField.name,
204
+ storage,
205
+ logger,
206
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
207
+ slugifier: serverConfig.slugifier,
208
+ filenameSlugifier: serverConfig.uploads?.filenameSlugifier,
209
+ requestContext: await getAdminRequestContext(),
210
+ imageProcessor: {
211
+ extractMeta: extractImageMeta,
212
+ isBypassMimeType,
213
+ generateVariants: ({ buffer, mimeType, storedFile, storage, upload, logger }) =>
214
+ generateImageVariants(
215
+ buffer,
216
+ mimeType,
217
+ storedFile,
218
+ storage,
219
+ upload.sizes ?? [],
220
+ logger
221
+ ),
222
+ },
223
+ }
221
224
 
222
- return coreUploadField(ctx, {
223
- buffer,
224
- originalFilename: file.name || 'upload',
225
- mimeType: file.type || 'application/octet-stream',
226
- fileSize: file.size,
227
- fields,
228
- shouldCreateDocument,
229
- locale: serverConfig.i18n.content.defaultLocale,
230
- })
231
- }
232
- )
233
- })
225
+ return coreUploadField(ctx, {
226
+ buffer,
227
+ originalFilename: file.name || 'upload',
228
+ mimeType: file.type || 'application/octet-stream',
229
+ fileSize: file.size,
230
+ fields,
231
+ shouldCreateDocument,
232
+ locale: serverConfig.i18n.content.defaultLocale,
233
+ })
234
+ }
235
+ )
236
+ })
237
+ )
234
238
 
235
239
  /**
236
240
  * Upload a file to an upload-enabled collection.
@@ -0,0 +1,67 @@
1
+ import {
2
+ BylineError,
3
+ ErrorCodes,
4
+ getDocumentRevisionValidationDetails,
5
+ getDocumentStaleDetails,
6
+ getLockConflictDetails,
7
+ } from '@byline/core'
8
+ import { getDocumentHookCommittedDetails } from '@byline/core/services'
9
+
10
+ import { committedTreeReceipt } from './structural-receipt.js'
11
+
12
+ /** Error subclasses lose custom properties in the server-function serializer.
13
+ * Throw only validated plain data for document failures; never transport causes,
14
+ * SQL, hook messages or an unverified current revision. */
15
+ export function documentMutationError(error: unknown): unknown {
16
+ const stale = getDocumentStaleDetails(error)
17
+ if (stale)
18
+ return {
19
+ code: ErrorCodes.DOCUMENT_STALE,
20
+ message: 'This document has changed. Reload before making changes.',
21
+ details: stale,
22
+ }
23
+ const validation = getDocumentRevisionValidationDetails(error)
24
+ if (validation)
25
+ return {
26
+ code: ErrorCodes.VALIDATION,
27
+ message: 'Reload this document before making changes.',
28
+ details: validation,
29
+ }
30
+ const lock = getLockConflictDetails(error)
31
+ if (lock)
32
+ return {
33
+ code: ErrorCodes.LOCK_CONFLICT,
34
+ message:
35
+ 'This change could not be saved because another operation was using the document. Reload before trying again.',
36
+ details: lock,
37
+ }
38
+ const tree = committedTreeReceipt(error)
39
+ if (tree)
40
+ return {
41
+ code: ErrorCodes.TREE_HOOK_COMMITTED,
42
+ message: 'The structure was saved, but a follow-up action failed.',
43
+ details: tree,
44
+ }
45
+ const committed = getDocumentHookCommittedDetails(error)
46
+ if (committed)
47
+ return {
48
+ code: ErrorCodes.DOCUMENT_HOOK_COMMITTED,
49
+ message: 'The document was saved, but a follow-up action failed.',
50
+ details: committed,
51
+ }
52
+ if (error instanceof BylineError && error.code === ErrorCodes.NOT_FOUND)
53
+ return { code: ErrorCodes.NOT_FOUND, message: 'This document is no longer available.' }
54
+ return error
55
+ }
56
+
57
+ export function withDocumentMutationErrors<T, R>(
58
+ handler: (input: T) => Promise<R>
59
+ ): (input: T) => Promise<R> {
60
+ return async (input) => {
61
+ try {
62
+ return await handler(input)
63
+ } catch (error) {
64
+ throw documentMutationError(error)
65
+ }
66
+ }
67
+ }