@byline/host-tanstack-start 1.10.2 → 1.11.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.
@@ -0,0 +1,166 @@
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 { createServerFn } from '@tanstack/react-start'
10
+
11
+ import {
12
+ assertActorCanPerform,
13
+ ERR_NOT_FOUND,
14
+ ERR_VALIDATION,
15
+ generateKeyBetween,
16
+ generateNKeysBetween,
17
+ getCollectionAdminConfig,
18
+ getLogger,
19
+ getServerConfig,
20
+ } from '@byline/core'
21
+
22
+ import { getAdminRequestContext } from '../../auth/auth-context.js'
23
+ import { ensureCollection } from '../../integrations/api-utils.js'
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Reorder a single document within an `orderable: true` collection.
27
+ //
28
+ // One of `beforeDocumentId` / `afterDocumentId` should be provided — they
29
+ // identify the neighbours the dragged row should land between. Either may
30
+ // be null:
31
+ // - both null → empty collection or "append-to-end" no-op (writes
32
+ // a fresh key)
33
+ // - beforeId set, afterId null → append after `beforeId`
34
+ // - beforeId null, afterId set → prepend before `afterId`
35
+ //
36
+ // Writes a single column on `byline_documents` and does NOT create a new
37
+ // document version. Goes through the `collections.<path>.update` ability;
38
+ // reordering is metadata-level update, not a new ability slug.
39
+ // ---------------------------------------------------------------------------
40
+
41
+ export const reorderCollectionDocument = createServerFn({ method: 'POST' })
42
+ .inputValidator(
43
+ (input: {
44
+ collection: string
45
+ documentId: string
46
+ beforeDocumentId?: string | null
47
+ afterDocumentId?: string | null
48
+ }) => input
49
+ )
50
+ .handler(async ({ data: input }) => {
51
+ const { collection: path, documentId, beforeDocumentId, afterDocumentId } = input
52
+ const logger = getLogger()
53
+
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
+ }
61
+
62
+ const adminConfig = getCollectionAdminConfig(path)
63
+ if (adminConfig?.orderable !== true) {
64
+ throw ERR_VALIDATION({
65
+ message: `collection '${path}' is not orderable; set \`orderable: true\` on its admin config to enable reordering`,
66
+ details: { collectionPath: path },
67
+ }).log(logger)
68
+ }
69
+
70
+ const requestContext = await getAdminRequestContext()
71
+ assertActorCanPerform(requestContext, path, 'update')
72
+
73
+ const serverConfig = getServerConfig()
74
+ const collectionId = config.collection.id
75
+
76
+ // Read the whole collection in canonical display order. Small by design
77
+ // for orderable use cases (bios, FAQs, sections), so a full read is
78
+ // cheap — and it gives us a single, consistent snapshot to reason about
79
+ // both backfill and recovery in one pass.
80
+ const canonical = await serverConfig.db.queries.documents.getCanonicalDocumentOrder({
81
+ collection_id: collectionId,
82
+ })
83
+
84
+ // Detect pathological key state — duplicates or non-ascending runs of
85
+ // keyed rows. These can land in the DB from prior buggy code paths
86
+ // or interrupted writes; once present, `generateKeyBetween` throws
87
+ // with `a >= b`. When detected, re-key the entire collection in the
88
+ // editor's current visible order so subsequent operations work from
89
+ // a clean baseline.
90
+ let corrupted = false
91
+ {
92
+ const seen = new Set<string>()
93
+ let lastKey: string | null = null
94
+ for (const doc of canonical) {
95
+ if (doc.order_key == null) continue
96
+ if (seen.has(doc.order_key)) {
97
+ corrupted = true
98
+ break
99
+ }
100
+ if (lastKey != null && doc.order_key <= lastKey) {
101
+ corrupted = true
102
+ break
103
+ }
104
+ seen.add(doc.order_key)
105
+ lastKey = doc.order_key
106
+ }
107
+ }
108
+
109
+ if (corrupted) {
110
+ const allKeys = generateNKeysBetween(null, null, canonical.length)
111
+ for (let i = 0; i < canonical.length; i++) {
112
+ await serverConfig.db.commands.documents.setOrderKey({
113
+ document_id: canonical[i]?.id,
114
+ order_key: allKeys[i]!,
115
+ })
116
+ }
117
+ } else {
118
+ // Happy path — backfill trailing NULLs after the largest existing
119
+ // key. After canonical sort the NULL rows are always contiguous at
120
+ // the tail (`NULLS LAST`).
121
+ const firstNullIdx = canonical.findIndex((d) => d.order_key == null)
122
+ if (firstNullIdx !== -1) {
123
+ const nullDocs = canonical.slice(firstNullIdx)
124
+ const lastExistingKey = firstNullIdx === 0 ? null : canonical[firstNullIdx - 1]?.order_key
125
+ const newKeys = generateNKeysBetween(lastExistingKey, null, nullDocs.length)
126
+ for (let i = 0; i < nullDocs.length; i++) {
127
+ await serverConfig.db.commands.documents.setOrderKey({
128
+ document_id: nullDocs[i]?.id,
129
+ order_key: newKeys[i]!,
130
+ })
131
+ }
132
+ }
133
+ }
134
+
135
+ // Keys are now clean — resolve neighbours and place the moved row.
136
+ const { left, right } = await serverConfig.db.queries.documents.getNeighborOrderKeys({
137
+ collection_id: collectionId,
138
+ before_document_id: beforeDocumentId ?? null,
139
+ after_document_id: afterDocumentId ?? null,
140
+ })
141
+
142
+ let newKey: string
143
+ try {
144
+ newKey = generateKeyBetween(left, right)
145
+ } catch (err) {
146
+ throw ERR_VALIDATION({
147
+ message: 'cannot generate order_key between supplied neighbors',
148
+ details: {
149
+ collectionPath: path,
150
+ documentId,
151
+ beforeDocumentId,
152
+ afterDocumentId,
153
+ left,
154
+ right,
155
+ cause: err instanceof Error ? err.message : String(err),
156
+ },
157
+ }).log(logger)
158
+ }
159
+
160
+ await serverConfig.db.commands.documents.setOrderKey({
161
+ document_id: documentId,
162
+ order_key: newKey,
163
+ })
164
+
165
+ return { status: 'ok' as const, orderKey: newKey }
166
+ })