@byline/host-tanstack-start 4.18.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.
- package/dist/admin-shell/collections/create.js +22 -5
- package/dist/admin-shell/collections/edit.d.ts +1 -1
- package/dist/admin-shell/collections/edit.js +222 -34
- package/dist/admin-shell/collections/history.js +4 -2
- package/dist/admin-shell/collections/list.js +30 -6
- package/dist/admin-shell/collections/restore-version-modal.d.ts +3 -1
- package/dist/admin-shell/collections/restore-version-modal.js +23 -6
- package/dist/admin-shell/collections/save-navigation.test.d.ts +8 -0
- package/dist/admin-shell/collections/tree-list.js +16 -6
- package/dist/admin-shell/collections/version-history.d.ts +2 -1
- package/dist/admin-shell/collections/version-history.js +11 -2
- package/dist/admin-shell/document-mutation-notice.d.ts +6 -0
- package/dist/admin-shell/document-mutation-notice.js +53 -0
- package/dist/admin-shell/document-mutation-state.d.ts +22 -0
- package/dist/admin-shell/document-mutation-state.js +71 -0
- package/dist/admin-shell/document-mutation-state.test.d.ts +1 -0
- package/dist/admin-shell/scheduled-publications/list.js +15 -5
- package/dist/admin-shell/singletons/history.js +4 -2
- package/dist/admin-shell/singletons/view.d.ts +1 -1
- package/dist/admin-shell/singletons/view.js +108 -23
- package/dist/integrations/byline-field-services.js +2 -9
- package/dist/routes/create-collection-edit-route.js +1 -1
- package/dist/routes/create-collection-list-route.d.ts +0 -7
- package/dist/routes/create-collection-list-route.js +7 -9
- package/dist/routes/create-singleton-route.d.ts +2 -0
- package/dist/routes/create-singleton-route.js +4 -3
- package/dist/server-fns/collections/copy-to-locale.d.ts +4 -8
- package/dist/server-fns/collections/copy-to-locale.js +17 -8
- package/dist/server-fns/collections/create.d.ts +2 -8
- package/dist/server-fns/collections/create.js +20 -10
- package/dist/server-fns/collections/delete-locale.d.ts +4 -8
- package/dist/server-fns/collections/delete-locale.js +15 -6
- package/dist/server-fns/collections/delete-outcome.d.ts +7 -3
- package/dist/server-fns/collections/delete-outcome.js +4 -0
- package/dist/server-fns/collections/delete.d.ts +2 -7
- package/dist/server-fns/collections/delete.js +5 -3
- package/dist/server-fns/collections/duplicate.d.ts +4 -8
- package/dist/server-fns/collections/duplicate.js +14 -5
- package/dist/server-fns/collections/get.d.ts +1 -7
- package/dist/server-fns/collections/get.js +5 -4
- package/dist/server-fns/collections/index.d.ts +1 -0
- package/dist/server-fns/collections/index.js +1 -0
- package/dist/server-fns/collections/list.d.ts +0 -7
- package/dist/server-fns/collections/list.js +11 -3
- package/dist/server-fns/collections/reorder.d.ts +7 -8
- package/dist/server-fns/collections/reorder.js +20 -76
- package/dist/server-fns/collections/restore-version.d.ts +4 -8
- package/dist/server-fns/collections/restore-version.js +22 -12
- package/dist/server-fns/collections/save-outcome.d.ts +24 -0
- package/dist/server-fns/collections/save-outcome.js +19 -0
- package/dist/server-fns/collections/scheduled-publication.d.ts +49 -9
- package/dist/server-fns/collections/scheduled-publication.js +31 -21
- package/dist/server-fns/collections/status.d.ts +8 -7
- package/dist/server-fns/collections/status.js +13 -5
- package/dist/server-fns/collections/tree-mutation.d.ts +7 -6
- package/dist/server-fns/collections/tree-mutation.js +17 -5
- package/dist/server-fns/collections/tree.d.ts +14 -8
- package/dist/server-fns/collections/tree.js +21 -17
- package/dist/server-fns/collections/update.d.ts +14 -11
- package/dist/server-fns/collections/update.js +48 -24
- package/dist/server-fns/collections/upload.d.ts +1 -0
- package/dist/server-fns/collections/upload.js +3 -2
- package/dist/server-fns/document-mutation-errors.d.ts +5 -0
- package/dist/server-fns/document-mutation-errors.js +50 -0
- package/dist/server-fns/editable-revision.d.ts +9 -0
- package/dist/server-fns/editable-revision.js +11 -0
- package/dist/server-fns/singleton-document-read.d.ts +1 -1
- package/dist/server-fns/singleton-document-read.js +7 -3
- package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +6 -8
- package/dist/server-fns/singletons/cancel-scheduled-publish.js +4 -1
- package/dist/server-fns/singletons/change-status.d.ts +2 -7
- package/dist/server-fns/singletons/change-status.js +4 -1
- package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +5 -8
- package/dist/server-fns/singletons/confirm-scheduled-publish.js +4 -2
- package/dist/server-fns/singletons/copy-to-locale.d.ts +2 -7
- package/dist/server-fns/singletons/copy-to-locale.js +16 -5
- package/dist/server-fns/singletons/get.d.ts +4 -4
- package/dist/server-fns/singletons/restore-version.d.ts +2 -7
- package/dist/server-fns/singletons/restore-version.js +13 -1
- package/dist/server-fns/singletons/schedule-publish.d.ts +5 -8
- package/dist/server-fns/singletons/schedule-publish.js +4 -2
- package/dist/server-fns/singletons/unpublish.d.ts +2 -7
- package/dist/server-fns/singletons/unpublish.js +4 -1
- package/dist/server-fns/singletons/update.d.ts +4 -11
- package/dist/server-fns/singletons/update.js +16 -8
- package/dist/server-fns/structural-receipt.d.ts +4 -0
- package/dist/server-fns/structural-receipt.js +31 -0
- package/package.json +11 -11
- package/src/admin-shell/collections/create.tsx +22 -3
- package/src/admin-shell/collections/edit.tsx +233 -61
- package/src/admin-shell/collections/history.tsx +11 -2
- package/src/admin-shell/collections/list.tsx +22 -6
- package/src/admin-shell/collections/restore-version-modal.tsx +23 -5
- package/src/admin-shell/collections/save-navigation.test.tsx +532 -0
- package/src/admin-shell/collections/tree-list.tsx +16 -6
- package/src/admin-shell/collections/version-history.tsx +13 -3
- package/src/admin-shell/document-mutation-notice.tsx +45 -0
- package/src/admin-shell/document-mutation-state.test.tsx +146 -0
- package/src/admin-shell/document-mutation-state.ts +95 -0
- package/src/admin-shell/scheduled-publications/list.tsx +16 -6
- package/src/admin-shell/singletons/history.test.tsx +46 -1
- package/src/admin-shell/singletons/history.tsx +10 -2
- package/src/admin-shell/singletons/view.test.tsx +86 -18
- package/src/admin-shell/singletons/view.tsx +145 -45
- package/src/integrations/byline-field-services.ts +2 -3
- package/src/routes/create-collection-edit-route.tsx +1 -0
- package/src/routes/create-collection-list-route.tsx +13 -2
- package/src/routes/create-singleton-route.test.node.ts +10 -2
- package/src/routes/create-singleton-route.tsx +6 -4
- package/src/server-fns/collections/copy-to-locale.ts +48 -29
- package/src/server-fns/collections/create.ts +56 -44
- package/src/server-fns/collections/delete-locale.ts +48 -25
- package/src/server-fns/collections/delete-outcome.test.node.ts +16 -0
- package/src/server-fns/collections/delete-outcome.ts +27 -16
- package/src/server-fns/collections/delete.ts +44 -38
- package/src/server-fns/collections/duplicate.ts +45 -25
- package/src/server-fns/collections/get.ts +8 -4
- package/src/server-fns/collections/index.ts +1 -0
- package/src/server-fns/collections/list.ts +8 -3
- package/src/server-fns/collections/reorder.ts +32 -124
- package/src/server-fns/collections/restore-version.ts +51 -36
- package/src/server-fns/collections/revision-input.test.node.ts +108 -0
- package/src/server-fns/collections/save-outcome.test.node.ts +80 -0
- package/src/server-fns/collections/save-outcome.ts +57 -0
- package/src/server-fns/collections/scheduled-publication.ts +138 -111
- package/src/server-fns/collections/status.ts +86 -64
- package/src/server-fns/collections/tree-mutation.test.node.ts +87 -29
- package/src/server-fns/collections/tree-mutation.ts +28 -11
- package/src/server-fns/collections/tree.ts +150 -122
- package/src/server-fns/collections/update.ts +110 -71
- package/src/server-fns/collections/upload-singleton.test.node.ts +3 -7
- package/src/server-fns/collections/upload.ts +81 -77
- package/src/server-fns/document-mutation-errors.ts +67 -0
- package/src/server-fns/document-mutation-transport.test.node.ts +211 -0
- package/src/server-fns/editable-revision.test.node.ts +23 -0
- package/src/server-fns/editable-revision.ts +18 -0
- package/src/server-fns/singleton-document-read.ts +7 -3
- package/src/server-fns/singletons/cancel-scheduled-publish.ts +10 -3
- package/src/server-fns/singletons/change-status.ts +10 -3
- package/src/server-fns/singletons/confirm-scheduled-publish.ts +12 -6
- package/src/server-fns/singletons/copy-to-locale.ts +20 -8
- package/src/server-fns/singletons/get.ts +3 -3
- package/src/server-fns/singletons/restore-version.ts +17 -3
- package/src/server-fns/singletons/schedule-publish.ts +18 -7
- package/src/server-fns/singletons/singletons.test.node.ts +96 -24
- package/src/server-fns/singletons/unpublish.ts +10 -3
- package/src/server-fns/singletons/update.ts +21 -9
- package/src/server-fns/structural-receipt.ts +41 -0
|
@@ -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
|
|
@@ -131,7 +132,7 @@ export const getCollectionDocuments = createServerFn({ method: 'GET' })
|
|
|
131
132
|
const hasRelations = Object.keys(populateMap).length > 0
|
|
132
133
|
const populate: PopulateSpec | undefined = hasRelations ? populateMap : undefined
|
|
133
134
|
|
|
134
|
-
const result = await handle.
|
|
135
|
+
const result = await handle.findForEdit({
|
|
135
136
|
where: Object.keys(where).length > 0 ? where : undefined,
|
|
136
137
|
sort: viewState.sort,
|
|
137
138
|
locale: params.locale ?? 'en',
|
|
@@ -140,7 +141,6 @@ export const getCollectionDocuments = createServerFn({ method: 'GET' })
|
|
|
140
141
|
select: params.fields,
|
|
141
142
|
populate,
|
|
142
143
|
depth: hasRelations ? 1 : undefined,
|
|
143
|
-
status: 'any',
|
|
144
144
|
// Admin list: show the raw per-locale state (untranslated docs render
|
|
145
145
|
// empty in the active locale's columns) rather than falling back to the
|
|
146
146
|
// default locale. Consistent with the edit view; overrides the client's
|
|
@@ -205,5 +205,10 @@ export const getCollectionDocuments = createServerFn({ method: 'GET' })
|
|
|
205
205
|
return serialised as unknown as ReturnType<typeof list.parse>
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
const parsed = list.parse(serialised)
|
|
209
|
+
const observations = new Map(result.docs.map((doc) => [doc.id, doc]))
|
|
210
|
+
return {
|
|
211
|
+
...parsed,
|
|
212
|
+
docs: parsed.docs.map((doc) => withEditableRevision(doc, observations.get(doc.id))),
|
|
213
|
+
}
|
|
209
214
|
})
|
|
@@ -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
|
|
@@ -9,15 +10,7 @@
|
|
|
9
10
|
import { createServerFn } from '@tanstack/react-start'
|
|
10
11
|
|
|
11
12
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
12
|
-
import {
|
|
13
|
-
assertActorCanPerform,
|
|
14
|
-
ERR_NOT_FOUND,
|
|
15
|
-
ERR_VALIDATION,
|
|
16
|
-
generateKeyBetween,
|
|
17
|
-
generateNKeysBetween,
|
|
18
|
-
getLogger,
|
|
19
|
-
getServerConfig,
|
|
20
|
-
} from '@byline/core'
|
|
13
|
+
import { ERR_NOT_FOUND, getLogger, getServerConfig, reorderDocument } from '@byline/core'
|
|
21
14
|
|
|
22
15
|
import { ensureCollection } from '../../integrations/api-utils.js'
|
|
23
16
|
|
|
@@ -27,138 +20,53 @@ import { ensureCollection } from '../../integrations/api-utils.js'
|
|
|
27
20
|
// One of `beforeDocumentId` / `afterDocumentId` should be provided — they
|
|
28
21
|
// identify the neighbours the dragged row should land between. Either may
|
|
29
22
|
// be null:
|
|
30
|
-
// - both null →
|
|
31
|
-
// a fresh key)
|
|
23
|
+
// - both null → append to the end; retain the key if already there
|
|
32
24
|
// - beforeId set, afterId null → append after `beforeId`
|
|
33
25
|
// - beforeId null, afterId set → prepend before `afterId`
|
|
34
26
|
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
27
|
+
// Updates ordering and any necessary sibling-key repairs in one guarded
|
|
28
|
+
// transaction, without creating content versions. Uses `collections.<path>.update`;
|
|
37
29
|
// reordering is metadata-level update, not a new ability slug.
|
|
38
30
|
// ---------------------------------------------------------------------------
|
|
39
31
|
|
|
40
32
|
export const reorderCollectionDocument = createServerFn({ method: 'POST' })
|
|
41
33
|
.validator(
|
|
42
34
|
(input: {
|
|
35
|
+
expectedRevision: number
|
|
43
36
|
collection: string
|
|
44
37
|
documentId: string
|
|
45
38
|
beforeDocumentId?: string | null
|
|
46
39
|
afterDocumentId?: string | null
|
|
47
40
|
}) => input
|
|
48
41
|
)
|
|
49
|
-
.handler(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (config.definition.orderable !== true) {
|
|
62
|
-
throw ERR_VALIDATION({
|
|
63
|
-
message: `collection '${path}' is not orderable; set \`orderable: true\` on its collection definition to enable reordering`,
|
|
64
|
-
details: { collectionPath: path },
|
|
65
|
-
}).log(logger)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const requestContext = await getAdminRequestContext()
|
|
69
|
-
assertActorCanPerform(requestContext, config.definition, 'update')
|
|
70
|
-
|
|
71
|
-
const serverConfig = getServerConfig()
|
|
72
|
-
const collectionId = config.collection.id
|
|
73
|
-
|
|
74
|
-
// Read the whole collection in canonical display order. Small by design
|
|
75
|
-
// for orderable use cases (bios, FAQs, sections), so a full read is
|
|
76
|
-
// cheap — and it gives us a single, consistent snapshot to reason about
|
|
77
|
-
// both backfill and recovery in one pass.
|
|
78
|
-
const canonical = await serverConfig.db.queries.documents.getCanonicalDocumentOrder({
|
|
79
|
-
collection_id: collectionId,
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
// Detect pathological key state — duplicates or non-ascending runs of
|
|
83
|
-
// keyed rows. These can land in the DB from prior buggy code paths
|
|
84
|
-
// or interrupted writes; once present, `generateKeyBetween` throws
|
|
85
|
-
// with `a >= b`. When detected, re-key the entire collection in the
|
|
86
|
-
// editor's current visible order so subsequent operations work from
|
|
87
|
-
// a clean baseline.
|
|
88
|
-
let corrupted = false
|
|
89
|
-
{
|
|
90
|
-
const seen = new Set<string>()
|
|
91
|
-
let lastKey: string | null = null
|
|
92
|
-
for (const doc of canonical) {
|
|
93
|
-
if (doc.order_key == null) continue
|
|
94
|
-
if (seen.has(doc.order_key)) {
|
|
95
|
-
corrupted = true
|
|
96
|
-
break
|
|
97
|
-
}
|
|
98
|
-
if (lastKey != null && doc.order_key <= lastKey) {
|
|
99
|
-
corrupted = true
|
|
100
|
-
break
|
|
101
|
-
}
|
|
102
|
-
seen.add(doc.order_key)
|
|
103
|
-
lastKey = doc.order_key
|
|
42
|
+
.handler(
|
|
43
|
+
withDocumentMutationErrors(async ({ data: input }) => {
|
|
44
|
+
const { collection: path, documentId, beforeDocumentId, afterDocumentId } = input
|
|
45
|
+
const logger = getLogger()
|
|
46
|
+
|
|
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)
|
|
104
53
|
}
|
|
105
|
-
}
|
|
106
54
|
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
// Happy path — backfill trailing NULLs after the largest existing
|
|
117
|
-
// key. After canonical sort the NULL rows are always contiguous at
|
|
118
|
-
// the tail (`NULLS LAST`).
|
|
119
|
-
const firstNullIdx = canonical.findIndex((d) => d.order_key == null)
|
|
120
|
-
if (firstNullIdx !== -1) {
|
|
121
|
-
const nullDocs = canonical.slice(firstNullIdx)
|
|
122
|
-
const lastExistingKey = firstNullIdx === 0 ? null : canonical[firstNullIdx - 1]?.order_key
|
|
123
|
-
const newKeys = generateNKeysBetween(lastExistingKey, null, nullDocs.length)
|
|
124
|
-
for (let i = 0; i < nullDocs.length; i++) {
|
|
125
|
-
await serverConfig.db.commands.documents.setOrderKey({
|
|
126
|
-
document_id: nullDocs[i]?.id,
|
|
127
|
-
order_key: newKeys[i]!,
|
|
128
|
-
})
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Keys are now clean — resolve neighbours and place the moved row.
|
|
134
|
-
const { left, right } = await serverConfig.db.queries.documents.getNeighborOrderKeys({
|
|
135
|
-
collection_id: collectionId,
|
|
136
|
-
before_document_id: beforeDocumentId ?? null,
|
|
137
|
-
after_document_id: afterDocumentId ?? null,
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
let newKey: string
|
|
141
|
-
try {
|
|
142
|
-
newKey = generateKeyBetween(left, right)
|
|
143
|
-
} catch (err) {
|
|
144
|
-
throw ERR_VALIDATION({
|
|
145
|
-
message: 'cannot generate order_key between supplied neighbors',
|
|
146
|
-
details: {
|
|
55
|
+
const serverConfig = getServerConfig()
|
|
56
|
+
const result = await reorderDocument(
|
|
57
|
+
{
|
|
58
|
+
db: serverConfig.db,
|
|
59
|
+
definition: config.definition,
|
|
60
|
+
collectionId: config.collection.id,
|
|
61
|
+
collectionVersion: config.collection.version,
|
|
147
62
|
collectionPath: path,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
right,
|
|
153
|
-
cause: err instanceof Error ? err.message : String(err),
|
|
63
|
+
requestContext: await getAdminRequestContext(),
|
|
64
|
+
logger,
|
|
65
|
+
defaultLocale: serverConfig.i18n.content.defaultLocale,
|
|
66
|
+
slugifier: serverConfig.slugifier,
|
|
154
67
|
},
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
await serverConfig.db.commands.documents.setOrderKey({
|
|
159
|
-
document_id: documentId,
|
|
160
|
-
order_key: newKey,
|
|
68
|
+
{ documentId, expectedRevision: input.expectedRevision, beforeDocumentId, afterDocumentId }
|
|
69
|
+
)
|
|
70
|
+
return { status: 'ok' as const, ...result }
|
|
161
71
|
})
|
|
162
|
-
|
|
163
|
-
return { status: 'ok' as const, orderKey: newKey }
|
|
164
|
-
})
|
|
72
|
+
)
|
|
@@ -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,46 +15,60 @@ import type { DocumentLifecycleContext } from '@byline/core/services'
|
|
|
14
15
|
import { restoreDocumentVersion as restoreDocumentVersionService } from '@byline/core/services'
|
|
15
16
|
|
|
16
17
|
import { ensureCollection } from '../../integrations/api-utils.js'
|
|
18
|
+
import { toCommittedDocumentHookFailureResponse } from './save-outcome.js'
|
|
17
19
|
|
|
18
20
|
// ---------------------------------------------------------------------------
|
|
19
21
|
// Restore a historical document version as the new current version
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
21
23
|
|
|
22
24
|
export const restoreDocumentVersion = createServerFn({ method: 'POST' })
|
|
23
|
-
.validator(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
25
|
+
.validator(
|
|
26
|
+
(input: { expectedRevision: number; collection: string; id: string; versionId: string }) =>
|
|
27
|
+
input
|
|
28
|
+
)
|
|
29
|
+
.handler(
|
|
30
|
+
withDocumentMutationErrors(async ({ data: input }) => {
|
|
31
|
+
const { collection: path, id, versionId } = input
|
|
32
|
+
const logger = getLogger()
|
|
33
|
+
const config = await ensureCollection(path)
|
|
34
|
+
if (!config) {
|
|
35
|
+
throw ERR_NOT_FOUND({
|
|
36
|
+
message: 'Collection not found',
|
|
37
|
+
details: { collectionPath: path },
|
|
38
|
+
}).log(logger)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const serverConfig = getServerConfig()
|
|
42
|
+
const ctx: DocumentLifecycleContext = {
|
|
43
|
+
db: serverConfig.db,
|
|
44
|
+
definition: config.definition,
|
|
45
|
+
collectionId: config.collection.id,
|
|
46
|
+
collectionVersion: config.collection.version,
|
|
47
|
+
collectionPath: path,
|
|
48
|
+
logger,
|
|
49
|
+
defaultLocale: serverConfig.i18n.content.defaultLocale,
|
|
50
|
+
slugifier: serverConfig.slugifier,
|
|
51
|
+
requestContext: await getAdminRequestContext(),
|
|
52
|
+
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
try {
|
|
55
|
+
const result = await restoreDocumentVersionService(ctx, {
|
|
56
|
+
documentId: id,
|
|
57
|
+
expectedRevision: input.expectedRevision,
|
|
58
|
+
sourceVersionId: versionId,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
status: 'ok' as const,
|
|
63
|
+
documentId: result.documentId,
|
|
64
|
+
revision: result.revision,
|
|
65
|
+
documentVersionId: result.documentVersionId,
|
|
66
|
+
sourceVersionId: result.sourceVersionId,
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const committedFailure = toCommittedDocumentHookFailureResponse(error)
|
|
70
|
+
if (committedFailure != null) return committedFailure
|
|
71
|
+
throw error
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createSuperAdminContext } from '@byline/auth'
|
|
2
|
+
import {
|
|
3
|
+
defineCollection,
|
|
4
|
+
defineServerConfig,
|
|
5
|
+
getDocumentRevisionValidationDetails,
|
|
6
|
+
} from '@byline/core'
|
|
7
|
+
import { defineLogger } from '@byline/core/logger'
|
|
8
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
9
|
+
|
|
10
|
+
import { testAdapter } from '../../../../core/src/storage/db-adapter.test-helper.js'
|
|
11
|
+
|
|
12
|
+
const fixtures = vi.hoisted(() => ({ ensureCollection: vi.fn(), getAdminRequestContext: vi.fn() }))
|
|
13
|
+
// Simulate only the compiler's binding of a validator to a handler. These are
|
|
14
|
+
// the real exported host handlers and core services, not mocked lifecycle calls.
|
|
15
|
+
vi.mock('@tanstack/react-start', () => ({
|
|
16
|
+
createServerFn: () => {
|
|
17
|
+
let validate = (value: unknown) => value
|
|
18
|
+
const chain = {
|
|
19
|
+
validator(fn: (value: unknown) => unknown) {
|
|
20
|
+
validate = fn
|
|
21
|
+
return chain
|
|
22
|
+
},
|
|
23
|
+
handler(fn: (args: { data: unknown }) => Promise<unknown>) {
|
|
24
|
+
return (args: { data: unknown }) => fn({ data: validate(args.data) })
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
return chain
|
|
28
|
+
},
|
|
29
|
+
}))
|
|
30
|
+
vi.mock('../../integrations/api-utils.js', () => ({ ensureCollection: fixtures.ensureCollection }))
|
|
31
|
+
vi.mock('@byline/client/server', () => ({
|
|
32
|
+
getAdminRequestContext: fixtures.getAdminRequestContext,
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
import { deleteDocument } from './delete.js'
|
|
36
|
+
import { unpublishDocument, updateDocumentStatus } from './status.js'
|
|
37
|
+
import {
|
|
38
|
+
updateCollectionDocumentSystemFields,
|
|
39
|
+
updateCollectionDocumentWithPatches,
|
|
40
|
+
} from './update.js'
|
|
41
|
+
|
|
42
|
+
const write = vi.fn((): never => {
|
|
43
|
+
throw new Error('Unexpected document write')
|
|
44
|
+
})
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks()
|
|
47
|
+
const silent = () => {}
|
|
48
|
+
defineLogger({
|
|
49
|
+
log: silent,
|
|
50
|
+
fatal: silent,
|
|
51
|
+
error: silent,
|
|
52
|
+
warn: silent,
|
|
53
|
+
info: silent,
|
|
54
|
+
debug: silent,
|
|
55
|
+
trace: silent,
|
|
56
|
+
silent,
|
|
57
|
+
})
|
|
58
|
+
const definition = defineCollection({
|
|
59
|
+
path: 'pages',
|
|
60
|
+
labels: { singular: 'Page', plural: 'Pages' },
|
|
61
|
+
fields: [],
|
|
62
|
+
})
|
|
63
|
+
fixtures.ensureCollection.mockResolvedValue({
|
|
64
|
+
definition,
|
|
65
|
+
collection: { id: 'collection', version: 1 },
|
|
66
|
+
})
|
|
67
|
+
fixtures.getAdminRequestContext.mockResolvedValue(createSuperAdminContext({ id: 'editor' }))
|
|
68
|
+
defineServerConfig({
|
|
69
|
+
collections: [definition],
|
|
70
|
+
db: testAdapter({ withTransaction: write }),
|
|
71
|
+
i18n: {
|
|
72
|
+
admin: { defaultLocale: 'en', locales: [] },
|
|
73
|
+
content: { defaultLocale: 'en', locales: ['en'] },
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const operations = [
|
|
79
|
+
['save', updateCollectionDocumentWithPatches, { patches: [] }],
|
|
80
|
+
['metadata', updateCollectionDocumentSystemFields, { path: 'changed' }],
|
|
81
|
+
['status', updateDocumentStatus, { status: 'published' }],
|
|
82
|
+
['unpublish', unpublishDocument, {}],
|
|
83
|
+
['delete', deleteDocument, {}],
|
|
84
|
+
] as const
|
|
85
|
+
|
|
86
|
+
describe('raw host revision inputs', () => {
|
|
87
|
+
for (const [name, operation, extra] of operations) {
|
|
88
|
+
for (const revision of [undefined, null, '1', 0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1]) {
|
|
89
|
+
it(`${name} rejects ${String(revision)} without writing`, async () => {
|
|
90
|
+
const data = {
|
|
91
|
+
collection: 'pages',
|
|
92
|
+
id: 'document',
|
|
93
|
+
...extra,
|
|
94
|
+
...(revision === undefined ? {} : { expectedRevision: revision }),
|
|
95
|
+
}
|
|
96
|
+
const error = await Reflect.apply(operation, undefined, [{ data }]).catch(
|
|
97
|
+
(error: unknown) => error
|
|
98
|
+
)
|
|
99
|
+
expect(error).toMatchObject({ code: 'ERR_VALIDATION' })
|
|
100
|
+
expect(getDocumentRevisionValidationDetails(error)).toEqual({
|
|
101
|
+
reason:
|
|
102
|
+
revision === undefined ? 'missing_document_revision' : 'invalid_document_revision',
|
|
103
|
+
})
|
|
104
|
+
expect(write).not.toHaveBeenCalled()
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ERR_DOCUMENT_HOOK_COMMITTED, ErrorCodes } from '@byline/core'
|
|
10
|
+
import { describe, expect, it } from 'vitest'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
hasCommittedDocumentHookFailure,
|
|
14
|
+
toCommittedDocumentHookFailureResponse,
|
|
15
|
+
} from './save-outcome.js'
|
|
16
|
+
|
|
17
|
+
describe('collection save outcome transport', () => {
|
|
18
|
+
it('returns only allowlisted committed hook metadata', () => {
|
|
19
|
+
const error = ERR_DOCUMENT_HOOK_COMMITTED({
|
|
20
|
+
message: 'internal message',
|
|
21
|
+
cause: new Error('secret hook failure'),
|
|
22
|
+
details: {
|
|
23
|
+
phase: 'afterCreate',
|
|
24
|
+
documentId: 'doc-1',
|
|
25
|
+
documentVersionId: 'version-1',
|
|
26
|
+
revision: 2,
|
|
27
|
+
sideEffectCode: ErrorCodes.STORAGE,
|
|
28
|
+
privatePath: '/private/cache/key',
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const response = toCommittedDocumentHookFailureResponse(error)
|
|
33
|
+
|
|
34
|
+
expect(response).toEqual({
|
|
35
|
+
status: 'committed-hook-failed',
|
|
36
|
+
documentId: 'doc-1',
|
|
37
|
+
documentVersionId: 'version-1',
|
|
38
|
+
revision: 2,
|
|
39
|
+
sideEffectFailure: { phase: 'afterCreate', code: 'ERR_STORAGE' },
|
|
40
|
+
})
|
|
41
|
+
expect(JSON.stringify(response)).not.toContain('secret hook failure')
|
|
42
|
+
expect(JSON.stringify(response)).not.toContain('/private/cache/key')
|
|
43
|
+
expect(hasCommittedDocumentHookFailure(response)).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('accepts the singleton afterSave phase exported by core', () => {
|
|
47
|
+
const response = toCommittedDocumentHookFailureResponse(
|
|
48
|
+
ERR_DOCUMENT_HOOK_COMMITTED({
|
|
49
|
+
message: 'singleton committed',
|
|
50
|
+
details: {
|
|
51
|
+
phase: 'afterSave',
|
|
52
|
+
documentId: 'doc-singleton',
|
|
53
|
+
documentVersionId: 'version-singleton',
|
|
54
|
+
revision: 2,
|
|
55
|
+
sideEffectCode: ErrorCodes.UNHANDLED,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
expect(response?.sideEffectFailure).toEqual({
|
|
61
|
+
phase: 'afterSave',
|
|
62
|
+
code: ErrorCodes.UNHANDLED,
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('does not classify ordinary or malformed failures as committed', () => {
|
|
67
|
+
expect(toCommittedDocumentHookFailureResponse(new Error('write failed'))).toBeNull()
|
|
68
|
+
expect(
|
|
69
|
+
toCommittedDocumentHookFailureResponse({
|
|
70
|
+
code: ErrorCodes.DOCUMENT_HOOK_COMMITTED,
|
|
71
|
+
message: 'missing ids',
|
|
72
|
+
details: {
|
|
73
|
+
phase: 'afterUpdate',
|
|
74
|
+
sideEffectCode: ErrorCodes.UNHANDLED,
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
).toBeNull()
|
|
78
|
+
expect(hasCommittedDocumentHookFailure({ status: 'ok' })).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type DocumentHookCommittedPhase,
|
|
11
|
+
type DocumentHookSideEffectCode,
|
|
12
|
+
getDocumentHookCommittedDetails,
|
|
13
|
+
} from '@byline/core/services'
|
|
14
|
+
|
|
15
|
+
export interface CollectionDocumentCommittedHookFailureResponse {
|
|
16
|
+
status: 'committed-hook-failed'
|
|
17
|
+
documentId: string
|
|
18
|
+
documentVersionId: string
|
|
19
|
+
revision: number
|
|
20
|
+
sideEffectFailure: {
|
|
21
|
+
phase: DocumentHookCommittedPhase
|
|
22
|
+
code: DocumentHookSideEffectCode
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert only validated core committed-hook errors into safe wire data. Raw
|
|
28
|
+
* hook errors, messages, stack traces, and storage paths remain server-side.
|
|
29
|
+
*/
|
|
30
|
+
export function toCommittedDocumentHookFailureResponse(
|
|
31
|
+
error: unknown
|
|
32
|
+
): CollectionDocumentCommittedHookFailureResponse | null {
|
|
33
|
+
const details = getDocumentHookCommittedDetails(error)
|
|
34
|
+
if (details == null) return null
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
status: 'committed-hook-failed',
|
|
38
|
+
documentId: details.documentId,
|
|
39
|
+
documentVersionId: details.documentVersionId,
|
|
40
|
+
revision: details.revision,
|
|
41
|
+
sideEffectFailure: {
|
|
42
|
+
phase: details.phase,
|
|
43
|
+
code: details.sideEffectCode,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hasCommittedDocumentHookFailure(
|
|
49
|
+
result: unknown
|
|
50
|
+
): result is CollectionDocumentCommittedHookFailureResponse {
|
|
51
|
+
return (
|
|
52
|
+
typeof result === 'object' &&
|
|
53
|
+
result != null &&
|
|
54
|
+
'status' in result &&
|
|
55
|
+
result.status === 'committed-hook-failed'
|
|
56
|
+
)
|
|
57
|
+
}
|