@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
@@ -10,7 +10,7 @@ import { createFileRoute, notFound } from '@tanstack/react-router'
10
10
  import { createServerFn } from '@tanstack/react-start'
11
11
 
12
12
  import type { DocumentPublishScheduleInfo } from '@byline/client'
13
- import { getAdminBylineClient, getAdminRequestContext } from '@byline/client/server'
13
+ import { getAdminRequestContext } from '@byline/client/server'
14
14
  import {
15
15
  buildInitialDataFromFields,
16
16
  documentAbilityKey,
@@ -51,6 +51,7 @@ export const getSingletonRouteDocument = createServerFn({ method: 'GET' })
51
51
  if (document == null) {
52
52
  return {
53
53
  document: null,
54
+ expectedEmpty: true,
54
55
  initialData: serialise(
55
56
  await buildInitialDataFromFields(definition.fields, {
56
57
  locale: data.locale ?? getServerConfig().i18n.content.defaultLocale,
@@ -93,9 +94,9 @@ export const getSingletonRouteDocument = createServerFn({ method: 'GET' })
93
94
  actor?.hasAbility(documentAbilityKey(definition, 'changeStatus')) === true &&
94
95
  actor.hasAbility(documentAbilityKey(definition, 'publish'))
95
96
  if (canSchedulePublication) {
96
- scheduledPublish = await getAdminBylineClient()
97
- .singleton(data.singleton)
98
- .getScheduledPublish()
97
+ scheduledPublish = (
98
+ document as unknown as { scheduledPublication: DocumentPublishScheduleInfo | null }
99
+ ).scheduledPublication
99
100
  }
100
101
  }
101
102
  const serializedSchedule = scheduledPublish == null ? null : serialise(scheduledPublish)
@@ -159,6 +160,7 @@ export function createSingletonRoute(path: string) {
159
160
  ]}
160
161
  />
161
162
  <SingletonView
163
+ key={`${singleton}/${locale}`}
162
164
  singletonDefinition={definition}
163
165
  adminConfig={getSingletonAdminConfig(singleton) ?? undefined}
164
166
  document={routeState.document}
@@ -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
@@ -35,6 +36,7 @@ import {
35
36
  export const copyDocumentToLocale = createServerFn({ method: 'POST' })
36
37
  .validator(
37
38
  (input: {
39
+ expectedRevision: number
38
40
  collection: string
39
41
  id: string
40
42
  sourceLocale: string
@@ -43,43 +45,46 @@ export const copyDocumentToLocale = createServerFn({ method: 'POST' })
43
45
  }) => input
44
46
  )
45
47
  .handler(
46
- async ({
47
- data: input,
48
- }): Promise<CopyToLocaleResult | CollectionDocumentCommittedHookFailureResponse> => {
49
- const { collection: path, id, sourceLocale, targetLocale, overwrite } = input
50
- const logger = getLogger()
51
- const config = await ensureCollection(path)
52
- if (!config) {
53
- throw ERR_NOT_FOUND({
54
- message: 'Collection not found',
55
- details: { collectionPath: path },
56
- }).log(logger)
57
- }
48
+ withDocumentMutationErrors(
49
+ async ({
50
+ data: input,
51
+ }): Promise<CopyToLocaleResult | CollectionDocumentCommittedHookFailureResponse> => {
52
+ const { collection: path, id, sourceLocale, targetLocale, overwrite } = input
53
+ const logger = getLogger()
54
+ const config = await ensureCollection(path)
55
+ if (!config) {
56
+ throw ERR_NOT_FOUND({
57
+ message: 'Collection not found',
58
+ details: { collectionPath: path },
59
+ }).log(logger)
60
+ }
58
61
 
59
- const serverConfig = getServerConfig()
60
- const ctx: DocumentLifecycleContext = {
61
- db: serverConfig.db,
62
- definition: config.definition,
63
- collectionId: config.collection.id,
64
- collectionVersion: config.collection.version,
65
- collectionPath: path,
66
- logger,
67
- defaultLocale: serverConfig.i18n.content.defaultLocale,
68
- slugifier: serverConfig.slugifier,
69
- requestContext: await getAdminRequestContext(),
70
- }
62
+ const serverConfig = getServerConfig()
63
+ const ctx: DocumentLifecycleContext = {
64
+ db: serverConfig.db,
65
+ definition: config.definition,
66
+ collectionId: config.collection.id,
67
+ collectionVersion: config.collection.version,
68
+ collectionPath: path,
69
+ logger,
70
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
71
+ slugifier: serverConfig.slugifier,
72
+ requestContext: await getAdminRequestContext(),
73
+ }
71
74
 
72
- try {
73
- return await copyToLocale(ctx, {
74
- documentId: id,
75
- sourceLocale,
76
- targetLocale,
77
- overwrite,
78
- })
79
- } catch (error) {
80
- const committedFailure = toCommittedDocumentHookFailureResponse(error)
81
- if (committedFailure != null) return committedFailure
82
- throw error
75
+ try {
76
+ return await copyToLocale(ctx, {
77
+ documentId: id,
78
+ expectedRevision: input.expectedRevision,
79
+ sourceLocale,
80
+ targetLocale,
81
+ overwrite,
82
+ })
83
+ } catch (error) {
84
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
85
+ if (committedFailure != null) return committedFailure
86
+ throw error
87
+ }
83
88
  }
84
- }
89
+ )
85
90
  )
@@ -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
@@ -30,56 +31,59 @@ export const createCollectionDocument = createServerFn({ method: 'POST' })
30
31
  availableLocales?: string[]
31
32
  }) => input
32
33
  )
33
- .handler(async ({ data: input }) => {
34
- const {
35
- collection: path,
36
- data: documentData,
37
- locale,
38
- path: explicitPath,
39
- availableLocales,
40
- } = input
41
- const logger = getLogger()
42
- const config = await ensureCollection(path)
43
- if (!config) {
44
- throw ERR_NOT_FOUND({
45
- message: 'Collection not found',
46
- details: { collectionPath: path },
47
- }).log(logger)
48
- }
49
-
50
- const serverConfig = getServerConfig()
51
- const ctx: DocumentLifecycleContext = {
52
- db: serverConfig.db,
53
- definition: config.definition,
54
- collectionId: config.collection.id,
55
- collectionVersion: config.collection.version,
56
- collectionPath: path,
57
- logger,
58
- defaultLocale: serverConfig.i18n.content.defaultLocale,
59
- slugifier: serverConfig.slugifier,
60
- requestContext: await getAdminRequestContext(),
61
- }
62
-
63
- let result: CreateDocumentResult
64
- try {
65
- result = await createDocument(ctx, {
66
- data: structuredClone(documentData),
67
- status: documentData.status,
68
- locale: locale ?? serverConfig.i18n.content.defaultLocale,
34
+ .handler(
35
+ withDocumentMutationErrors(async ({ data: input }) => {
36
+ const {
37
+ collection: path,
38
+ data: documentData,
39
+ locale,
69
40
  path: explicitPath,
70
41
  availableLocales,
71
- })
72
- } catch (error) {
73
- const committedFailure = toCommittedDocumentHookFailureResponse(error)
74
- if (committedFailure != null) return committedFailure
75
- throw error
76
- }
42
+ } = input
43
+ const logger = getLogger()
44
+ const config = await ensureCollection(path)
45
+ if (!config) {
46
+ throw ERR_NOT_FOUND({
47
+ message: 'Collection not found',
48
+ details: { collectionPath: path },
49
+ }).log(logger)
50
+ }
51
+
52
+ const serverConfig = getServerConfig()
53
+ const ctx: DocumentLifecycleContext = {
54
+ db: serverConfig.db,
55
+ definition: config.definition,
56
+ collectionId: config.collection.id,
57
+ collectionVersion: config.collection.version,
58
+ collectionPath: path,
59
+ logger,
60
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
61
+ slugifier: serverConfig.slugifier,
62
+ requestContext: await getAdminRequestContext(),
63
+ }
77
64
 
78
- // The new document's id is returned so the create view can navigate
79
- // straight to the edit view (create → edit).
80
- return {
81
- status: 'ok' as const,
82
- documentId: result.documentId,
83
- documentVersionId: result.documentVersionId,
84
- }
85
- })
65
+ let result: CreateDocumentResult
66
+ try {
67
+ result = await createDocument(ctx, {
68
+ data: structuredClone(documentData),
69
+ status: documentData.status,
70
+ locale: locale ?? serverConfig.i18n.content.defaultLocale,
71
+ path: explicitPath,
72
+ availableLocales,
73
+ })
74
+ } catch (error) {
75
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
76
+ if (committedFailure != null) return committedFailure
77
+ throw error
78
+ }
79
+
80
+ // The new document's id is returned so the create view can navigate
81
+ // straight to the edit view (create → edit).
82
+ return {
83
+ status: 'ok' as const,
84
+ documentId: result.documentId,
85
+ documentVersionId: result.documentVersionId,
86
+ revision: result.revision,
87
+ }
88
+ })
89
+ )
@@ -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
@@ -34,40 +35,48 @@ import {
34
35
  * transport layer.
35
36
  */
36
37
  export const deleteDocumentLocale = createServerFn({ method: 'POST' })
37
- .validator((input: { collection: string; id: string; locale: string }) => input)
38
+ .validator(
39
+ (input: { expectedRevision: number; collection: string; id: string; locale: string }) => input
40
+ )
38
41
  .handler(
39
- async ({
40
- data: input,
41
- }): Promise<DeleteLocaleResult | CollectionDocumentCommittedHookFailureResponse> => {
42
- const { collection: path, id, locale } = input
43
- const logger = getLogger()
44
- const config = await ensureCollection(path)
45
- if (!config) {
46
- throw ERR_NOT_FOUND({
47
- message: 'Collection not found',
48
- details: { collectionPath: path },
49
- }).log(logger)
50
- }
42
+ withDocumentMutationErrors(
43
+ async ({
44
+ data: input,
45
+ }): Promise<DeleteLocaleResult | CollectionDocumentCommittedHookFailureResponse> => {
46
+ const { collection: path, id, locale } = input
47
+ const logger = getLogger()
48
+ const config = await ensureCollection(path)
49
+ if (!config) {
50
+ throw ERR_NOT_FOUND({
51
+ message: 'Collection not found',
52
+ details: { collectionPath: path },
53
+ }).log(logger)
54
+ }
51
55
 
52
- const serverConfig = getServerConfig()
53
- const ctx: DocumentLifecycleContext = {
54
- db: serverConfig.db,
55
- definition: config.definition,
56
- collectionId: config.collection.id,
57
- collectionVersion: config.collection.version,
58
- collectionPath: path,
59
- logger,
60
- defaultLocale: serverConfig.i18n.content.defaultLocale,
61
- slugifier: serverConfig.slugifier,
62
- requestContext: await getAdminRequestContext(),
63
- }
56
+ const serverConfig = getServerConfig()
57
+ const ctx: DocumentLifecycleContext = {
58
+ db: serverConfig.db,
59
+ definition: config.definition,
60
+ collectionId: config.collection.id,
61
+ collectionVersion: config.collection.version,
62
+ collectionPath: path,
63
+ logger,
64
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
65
+ slugifier: serverConfig.slugifier,
66
+ requestContext: await getAdminRequestContext(),
67
+ }
64
68
 
65
- try {
66
- return await deleteLocale(ctx, { documentId: id, locale })
67
- } catch (error) {
68
- const committedFailure = toCommittedDocumentHookFailureResponse(error)
69
- if (committedFailure != null) return committedFailure
70
- throw error
69
+ try {
70
+ return await deleteLocale(ctx, {
71
+ documentId: id,
72
+ expectedRevision: input.expectedRevision,
73
+ locale,
74
+ })
75
+ } catch (error) {
76
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
77
+ if (committedFailure != null) return committedFailure
78
+ throw error
79
+ }
71
80
  }
72
- }
81
+ )
73
82
  )
@@ -16,6 +16,10 @@ describe('delete outcome transport and admin interpretation', () => {
16
16
  // Simulate malformed runtime data from an older or compromised source. The
17
17
  // core type rejects this shape, but the transport still sanitizes it.
18
18
  const unsafeResult = {
19
+ documentId: 'doc-1',
20
+ revision: 2,
21
+ affectedDocuments: [{ documentId: 'child', revision: 3 }],
22
+ scheduledPublicationsNeedReconfirmation: true,
19
23
  deletedVersionCount: 2,
20
24
  outcome: 'committed-with-side-effect-failures',
21
25
  sideEffectFailures: [
@@ -36,6 +40,10 @@ describe('delete outcome transport and admin interpretation', () => {
36
40
 
37
41
  expect(response).toEqual({
38
42
  status: 'ok',
43
+ documentId: 'doc-1',
44
+ revision: 2,
45
+ affectedDocuments: [{ documentId: 'child', revision: 3 }],
46
+ scheduledPublicationsNeedReconfirmation: true,
39
47
  deletedVersionCount: 2,
40
48
  outcome: 'committed-with-side-effect-failures',
41
49
  sideEffectFailures: [
@@ -53,6 +61,10 @@ describe('delete outcome transport and admin interpretation', () => {
53
61
 
54
62
  it('preserves the committed success discriminant and empty failures', () => {
55
63
  const response = toDeleteDocumentResponse({
64
+ documentId: 'doc-1',
65
+ revision: 2,
66
+ affectedDocuments: [{ documentId: 'child', revision: 3 }],
67
+ scheduledPublicationsNeedReconfirmation: true,
56
68
  deletedVersionCount: 2,
57
69
  outcome: 'committed',
58
70
  sideEffectFailures: [],
@@ -60,6 +72,10 @@ describe('delete outcome transport and admin interpretation', () => {
60
72
 
61
73
  expect(response).toEqual({
62
74
  status: 'ok',
75
+ documentId: 'doc-1',
76
+ revision: 2,
77
+ affectedDocuments: [{ documentId: 'child', revision: 3 }],
78
+ scheduledPublicationsNeedReconfirmation: true,
63
79
  deletedVersionCount: 2,
64
80
  outcome: 'committed',
65
81
  sideEffectFailures: [],
@@ -10,6 +10,7 @@ import type {
10
10
  DeleteDocumentResult,
11
11
  DeleteDocumentSideEffectCode,
12
12
  DeleteDocumentSideEffectPhase,
13
+ StructuralMutationReceipt,
13
14
  } from '@byline/core'
14
15
  import { ErrorCodes } from '@byline/core'
15
16
 
@@ -21,22 +22,28 @@ export interface DeleteDocumentPublicSideEffectFailure {
21
22
  code: DeleteDocumentPublicSideEffectCode
22
23
  }
23
24
 
24
- export type DeleteDocumentResponse =
25
- | {
26
- status: 'ok'
27
- deletedVersionCount: number
28
- outcome: 'committed'
29
- sideEffectFailures: []
30
- }
31
- | {
32
- status: 'ok'
33
- deletedVersionCount: number
34
- outcome: 'committed-with-side-effect-failures'
35
- sideEffectFailures: [
36
- DeleteDocumentPublicSideEffectFailure,
37
- ...DeleteDocumentPublicSideEffectFailure[],
38
- ]
39
- }
25
+ export type DeleteDocumentResponse = StructuralMutationReceipt &
26
+ (
27
+ | {
28
+ status: 'ok'
29
+ documentId: string
30
+ revision: number
31
+ deletedVersionCount: number
32
+ outcome: 'committed'
33
+ sideEffectFailures: []
34
+ }
35
+ | {
36
+ status: 'ok'
37
+ documentId: string
38
+ revision: number
39
+ deletedVersionCount: number
40
+ outcome: 'committed-with-side-effect-failures'
41
+ sideEffectFailures: [
42
+ DeleteDocumentPublicSideEffectFailure,
43
+ ...DeleteDocumentPublicSideEffectFailure[],
44
+ ]
45
+ }
46
+ )
40
47
 
41
48
  function sanitizePhase(phase: DeleteDocumentSideEffectPhase): DeleteDocumentPublicSideEffectPhase {
42
49
  if (phase === 'afterTreeChange' || phase === 'afterDelete') {
@@ -67,6 +74,10 @@ export function toDeleteDocumentResponse(result: DeleteDocumentResult): DeleteDo
67
74
  const [firstFailure, ...remainingFailures] = result.sideEffectFailures
68
75
  return {
69
76
  status: 'ok',
77
+ documentId: result.documentId,
78
+ revision: result.revision,
79
+ affectedDocuments: result.affectedDocuments,
80
+ scheduledPublicationsNeedReconfirmation: result.scheduledPublicationsNeedReconfirmation,
70
81
  deletedVersionCount: result.deletedVersionCount,
71
82
  outcome: result.outcome,
72
83
  sideEffectFailures: [sanitizeFailure(firstFailure), ...remainingFailures.map(sanitizeFailure)],
@@ -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
@@ -21,43 +22,48 @@ import { toDeleteDocumentResponse } from './delete-outcome.js'
21
22
  // ---------------------------------------------------------------------------
22
23
 
23
24
  export const deleteDocument = createServerFn({ method: 'POST' })
24
- .validator((input: { collection: string; id: string }) => input)
25
- .handler(async ({ data: input }) => {
26
- const { collection: path, id } = input
27
- const logger = getLogger()
28
- const config = await ensureCollection(path)
29
- if (!config) {
30
- throw ERR_NOT_FOUND({
31
- message: 'Collection not found',
32
- details: { collectionPath: path },
33
- }).log(logger)
34
- }
25
+ .validator((input: { expectedRevision: number; collection: string; id: string }) => input)
26
+ .handler(
27
+ withDocumentMutationErrors(async ({ data: input }) => {
28
+ const { collection: path, id } = input
29
+ const logger = getLogger()
30
+ const config = await ensureCollection(path)
31
+ if (!config) {
32
+ throw ERR_NOT_FOUND({
33
+ message: 'Collection not found',
34
+ details: { collectionPath: path },
35
+ }).log(logger)
36
+ }
35
37
 
36
- const serverConfig = getServerConfig()
37
- // Resolve the storage provider so the lifecycle service can clean up
38
- // uploaded files and variants on deletion. With per-field upload
39
- // config, a collection may have one (or more) image/file fields with
40
- // their own storage. The delete path needs *a* provider; we pick the
41
- // first upload-capable field's, falling back to the site-wide
42
- // default. Multi-storage collections are out of scope today —
43
- // deletion routes everything through one provider, which is fine
44
- // when all upload fields target the same backend (the common case).
45
- const firstUploadField = getUploadFields(config.definition)[0]
46
- const storage = firstUploadField?.upload?.storage ?? serverConfig.storage
47
- const db = serverConfig.db
48
- const ctx: DocumentLifecycleContext = {
49
- db,
50
- definition: config.definition,
51
- collectionId: config.collection.id,
52
- collectionVersion: config.collection.version,
53
- collectionPath: path,
54
- ...(storage ? { storage } : {}),
55
- logger,
56
- defaultLocale: serverConfig.i18n.content.defaultLocale,
57
- slugifier: serverConfig.slugifier,
58
- requestContext: await getAdminRequestContext(),
59
- }
38
+ const serverConfig = getServerConfig()
39
+ // Resolve the storage provider so the lifecycle service can clean up
40
+ // uploaded files and variants on deletion. With per-field upload
41
+ // config, a collection may have one (or more) image/file fields with
42
+ // their own storage. The delete path needs *a* provider; we pick the
43
+ // first upload-capable field's, falling back to the site-wide
44
+ // default. Multi-storage collections are out of scope today —
45
+ // deletion routes everything through one provider, which is fine
46
+ // when all upload fields target the same backend (the common case).
47
+ const firstUploadField = getUploadFields(config.definition)[0]
48
+ const storage = firstUploadField?.upload?.storage ?? serverConfig.storage
49
+ const db = serverConfig.db
50
+ const ctx: DocumentLifecycleContext = {
51
+ db,
52
+ definition: config.definition,
53
+ collectionId: config.collection.id,
54
+ collectionVersion: config.collection.version,
55
+ collectionPath: path,
56
+ ...(storage ? { storage } : {}),
57
+ logger,
58
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
59
+ slugifier: serverConfig.slugifier,
60
+ requestContext: await getAdminRequestContext(),
61
+ }
60
62
 
61
- const result = await deleteDocumentService(ctx, { documentId: id })
62
- return toDeleteDocumentResponse(result)
63
- })
63
+ const result = await deleteDocumentService(ctx, {
64
+ documentId: id,
65
+ expectedRevision: input.expectedRevision,
66
+ })
67
+ return toDeleteDocumentResponse(result)
68
+ })
69
+ )
@@ -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
@@ -35,40 +36,45 @@ import {
35
36
  * TanStack Start's transport layer for the client to branch on.
36
37
  */
37
38
  export const duplicateCollectionDocument = createServerFn({ method: 'POST' })
38
- .validator((input: { collection: string; id: string }) => input)
39
+ .validator((input: { expectedRevision: number; collection: string; id: string }) => input)
39
40
  .handler(
40
- async ({
41
- data: input,
42
- }): Promise<DuplicateDocumentResult | CollectionDocumentCommittedHookFailureResponse> => {
43
- const { collection: path, id: sourceDocumentId } = 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
- }
41
+ withDocumentMutationErrors(
42
+ async ({
43
+ data: input,
44
+ }): Promise<DuplicateDocumentResult | CollectionDocumentCommittedHookFailureResponse> => {
45
+ const { collection: path, id: sourceDocumentId } = input
46
+ const logger = getLogger()
47
+ const config = await ensureCollection(path)
48
+ if (!config) {
49
+ throw ERR_NOT_FOUND({
50
+ message: 'Collection not found',
51
+ details: { collectionPath: path },
52
+ }).log(logger)
53
+ }
52
54
 
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
- }
55
+ const serverConfig = getServerConfig()
56
+ const ctx: DocumentLifecycleContext = {
57
+ db: serverConfig.db,
58
+ definition: config.definition,
59
+ collectionId: config.collection.id,
60
+ collectionVersion: config.collection.version,
61
+ collectionPath: path,
62
+ logger,
63
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
64
+ slugifier: serverConfig.slugifier,
65
+ requestContext: await getAdminRequestContext(),
66
+ }
65
67
 
66
- try {
67
- return await duplicateDocument(ctx, { sourceDocumentId })
68
- } catch (error) {
69
- const committedFailure = toCommittedDocumentHookFailureResponse(error)
70
- if (committedFailure != null) return committedFailure
71
- throw error
68
+ try {
69
+ return await duplicateDocument(ctx, {
70
+ sourceDocumentId,
71
+ expectedRevision: input.expectedRevision,
72
+ })
73
+ } catch (error) {
74
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
75
+ if (committedFailure != null) return committedFailure
76
+ throw error
77
+ }
72
78
  }
73
- }
79
+ )
74
80
  )
@@ -1,3 +1,4 @@
1
+ import { withEditableRevision } from '../editable-revision.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
@@ -51,13 +52,16 @@ const getDocumentFn = createServerFn({ method: 'GET' })
51
52
  const client = getAdminBylineClient()
52
53
  const handle = client.collection(path)
53
54
 
54
- const { options: readOptions, populatedTree } = resolveAdminDocumentRead(config.definition, {
55
+ const {
56
+ options: { status: _status, ...readOptions },
57
+ populatedTree,
58
+ } = resolveAdminDocumentRead(config.definition, {
55
59
  locale,
56
60
  depth,
57
61
  populateRelations,
58
62
  })
59
63
 
60
- const document = await handle.findById(id, {
64
+ const document = await handle.findByIdForEdit(id, {
61
65
  ...readOptions,
62
66
  })
63
67
 
@@ -141,13 +145,13 @@ const getDocumentFn = createServerFn({ method: 'GET' })
141
145
  actor?.hasAbility(documentAbilityKey(config.definition, 'changeStatus')) === true &&
142
146
  actor.hasAbility(documentAbilityKey(config.definition, 'publish'))
143
147
  if (canSchedulePublication) {
144
- scheduledPublish = await handle.getScheduledPublish(id)
148
+ scheduledPublish = document.scheduledPublication
145
149
  }
146
150
  }
147
151
  const serializedSchedule = scheduledPublish == null ? null : serialise(scheduledPublish)
148
152
 
149
153
  return {
150
- ...(parsed as Record<string, any>),
154
+ ...withEditableRevision(parsed, document),
151
155
  _publishedVersion: publishedVersion,
152
156
  ...(restoreWarnings && restoreWarnings.length > 0
153
157
  ? { _restoreWarnings: restoreWarnings }