@byline/host-tanstack-start 3.12.2 → 3.13.1

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,203 @@
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
+ /**
10
+ * Document-tree server functions for `tree: true` collections — the transport
11
+ * behind the sidebar tree-placement widget. Thin wrappers over the
12
+ * `@byline/client` tree API, which asserts the actor ability, enforces the
13
+ * cycle / same-collection invariants, and fires the `afterTreeChange`
14
+ * invalidation hook. See docs/DOCUMENT-TREE.md.
15
+ */
16
+
17
+ import { createServerFn } from '@tanstack/react-start'
18
+
19
+ import { ERR_NOT_FOUND, getLogger } from '@byline/core'
20
+
21
+ import { ensureCollection } from '../../integrations/api-utils.js'
22
+ import { getAdminBylineClient } from '../../integrations/byline-client.js'
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Place / move a node within the tree (place / reorder / re-parent)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export const placeTreeNode = createServerFn({ method: 'POST' })
29
+ .validator(
30
+ (input: {
31
+ collection: string
32
+ documentId: string
33
+ parentDocumentId: string | null
34
+ beforeDocumentId?: string | null
35
+ afterDocumentId?: string | null
36
+ }) => input
37
+ )
38
+ .handler(async ({ data }) => {
39
+ const { collection: path, documentId } = data
40
+ const config = await ensureCollection(path)
41
+ if (!config) {
42
+ throw ERR_NOT_FOUND({
43
+ message: 'Collection not found',
44
+ details: { collectionPath: path },
45
+ }).log(getLogger())
46
+ }
47
+ const handle = getAdminBylineClient().collection(path)
48
+ const result = await handle.placeTreeNode(documentId, {
49
+ parentDocumentId: data.parentDocumentId,
50
+ beforeDocumentId: data.beforeDocumentId ?? null,
51
+ afterDocumentId: data.afterDocumentId ?? null,
52
+ })
53
+ return { status: 'ok' as const, orderKey: result.orderKey }
54
+ })
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Remove a node from the tree (back to the unplaced state)
58
+ // ---------------------------------------------------------------------------
59
+
60
+ export const removeFromTree = createServerFn({ method: 'POST' })
61
+ .validator((input: { collection: string; documentId: string }) => input)
62
+ .handler(async ({ data }) => {
63
+ const { collection: path, documentId } = data
64
+ const config = await ensureCollection(path)
65
+ if (!config) {
66
+ throw ERR_NOT_FOUND({
67
+ message: 'Collection not found',
68
+ details: { collectionPath: path },
69
+ }).log(getLogger())
70
+ }
71
+ await getAdminBylineClient().collection(path).removeFromTree(documentId)
72
+ return { status: 'ok' as const }
73
+ })
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Resolve a document's ancestor chain (root-first), hydrated with titles
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export const getTreeAncestors = createServerFn({ method: 'GET' })
80
+ .validator((input: { collection: string; documentId: string }) => input)
81
+ .handler(async ({ data }) => {
82
+ const { collection: path, documentId } = data
83
+ const config = await ensureCollection(path)
84
+ if (!config) {
85
+ throw ERR_NOT_FOUND({
86
+ message: 'Collection not found',
87
+ details: { collectionPath: path },
88
+ }).log(getLogger())
89
+ }
90
+ const useAsTitle = config.definition.useAsTitle
91
+ // Admin context: read with `status: 'any'` so draft ancestors still show.
92
+ const ancestors = await getAdminBylineClient()
93
+ .collection(path)
94
+ .getAncestors(documentId, { status: 'any' })
95
+ return ancestors.map((doc) => {
96
+ const fields = doc.fields as Record<string, any> | undefined
97
+ const title = useAsTitle ? fields?.[useAsTitle] : undefined
98
+ return {
99
+ id: doc.id,
100
+ title: typeof title === 'string' && title.length > 0 ? title : doc.path,
101
+ path: doc.path,
102
+ }
103
+ })
104
+ })
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Resolve a document's placement state in the tree (unplaced / root / child)
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export const getTreeParent = createServerFn({ method: 'GET' })
111
+ .validator((input: { collection: string; documentId: string }) => input)
112
+ .handler(async ({ data }) => {
113
+ const { collection: path, documentId } = data
114
+ const config = await ensureCollection(path)
115
+ if (!config) {
116
+ throw ERR_NOT_FOUND({
117
+ message: 'Collection not found',
118
+ details: { collectionPath: path },
119
+ }).log(getLogger())
120
+ }
121
+ return getAdminBylineClient().collection(path).getTreeParent(documentId)
122
+ })
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Read the whole collection tree as ordered, depth-tagged rows for the built-in
126
+ // tree list view. Placed nodes come first (pre-order, root-first), then any
127
+ // *unplaced* documents (created but not yet positioned) so nothing is
128
+ // unreachable from the list. Admin reads use `status: 'any'`.
129
+ // ---------------------------------------------------------------------------
130
+
131
+ export interface CollectionTreeRow {
132
+ id: string
133
+ /** Parent document id, or `null` for a root or unplaced node. Drives the
134
+ * client-side tree reconstruction for drag-to-reorder / re-parent. */
135
+ parentId: string | null
136
+ depth: number
137
+ unplaced: boolean
138
+ status: string
139
+ path: string
140
+ createdAt: Date
141
+ updatedAt: Date
142
+ fields: Record<string, any>
143
+ }
144
+
145
+ export const getCollectionTree = createServerFn({ method: 'GET' })
146
+ .validator((input: { collection: string; locale?: string }) => input)
147
+ .handler(async ({ data }) => {
148
+ const { collection: path } = data
149
+ const config = await ensureCollection(path)
150
+ if (!config) {
151
+ throw ERR_NOT_FOUND({
152
+ message: 'Collection not found',
153
+ details: { collectionPath: path },
154
+ }).log(getLogger())
155
+ }
156
+ const handle = getAdminBylineClient().collection(path)
157
+
158
+ const forest = await handle.getSubtree({ status: 'any', locale: data.locale })
159
+ const rows: CollectionTreeRow[] = []
160
+ const placed = new Set<string>()
161
+ const walk = (nodes: typeof forest, depth: number, parentId: string | null): void => {
162
+ for (const node of nodes) {
163
+ const doc = node.document
164
+ rows.push({
165
+ id: doc.id,
166
+ parentId,
167
+ depth,
168
+ unplaced: false,
169
+ status: doc.status,
170
+ path: doc.path,
171
+ createdAt: doc.createdAt,
172
+ updatedAt: doc.updatedAt,
173
+ fields: doc.fields as Record<string, any>,
174
+ })
175
+ placed.add(doc.id)
176
+ walk(node.children, depth + 1, doc.id)
177
+ }
178
+ }
179
+ walk(forest, 0, null)
180
+
181
+ // Surface documents not yet in the tree (e.g. freshly created) so they
182
+ // remain reachable. Trees are small by design, so a single wide read is fine.
183
+ const all = await handle.find({ status: 'any', pageSize: 1000, _bypassBeforeRead: true })
184
+ for (const doc of all.docs) {
185
+ if (placed.has(doc.id)) continue
186
+ rows.push({
187
+ id: doc.id,
188
+ parentId: null,
189
+ depth: 0,
190
+ unplaced: true,
191
+ status: doc.status,
192
+ path: doc.path,
193
+ createdAt: doc.createdAt,
194
+ updatedAt: doc.updatedAt,
195
+ fields: doc.fields as Record<string, any>,
196
+ })
197
+ }
198
+
199
+ return {
200
+ rows,
201
+ included: { collection: { path: config.collection.path, labels: config.definition.labels } },
202
+ }
203
+ })