@byline/host-tanstack-start 3.9.0 → 3.10.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.
@@ -21,6 +21,7 @@ import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs
21
21
  import { HistoryView } from '../admin-shell/collections/history.js'
22
22
  import {
23
23
  getCollectionDocument,
24
+ getCollectionDocumentAuditLog,
24
25
  getCollectionDocumentHistory,
25
26
  } from '../server-fns/collections/index.js'
26
27
  import type { ContentLocaleOption } from '../admin-shell/collections/view-menu.js'
@@ -31,8 +32,17 @@ const searchSchema = z.object({
31
32
  order: z.string().optional(),
32
33
  desc: z.coerce.boolean().optional(),
33
34
  locale: z.string().optional(),
35
+ // Which sub-view of the history page is active (docs/AUDIT.md — Workstream
36
+ // 3). 'versions' is the content version stream; 'document' is the
37
+ // document-grain audit log. Absent → 'versions'.
38
+ tab: z.enum(['versions', 'document']).optional(),
34
39
  })
35
40
 
41
+ // The per-document audit log is small and bounded (path / locale / status
42
+ // changes + the deletion event), so v1 fetches a single generous page rather
43
+ // than wiring a second, tab-specific pager into the shared history route.
44
+ const AUDIT_LOG_PAGE_SIZE = 100
45
+
36
46
  interface CollectionHistoryOpts {
37
47
  contentLocales: ReadonlyArray<ContentLocaleOption>
38
48
  defaultContentLocale: string
@@ -67,7 +77,7 @@ export function createCollectionHistoryRoute(path: string, opts: CollectionHisto
67
77
  throw notFound()
68
78
  }
69
79
 
70
- const [history, currentDocument] = await Promise.all([
80
+ const [history, currentDocument, auditLog] = await Promise.all([
71
81
  getCollectionDocumentHistory({
72
82
  data: {
73
83
  collection: params.collection,
@@ -84,15 +94,26 @@ export function createCollectionHistoryRoute(path: string, opts: CollectionHisto
84
94
  // Fetch the current document with the same locale (or 'all') so diffs
85
95
  // compare the same shape as what the user is viewing.
86
96
  getCollectionDocument(params.collection, params.id, deps.locale ?? 'all'),
97
+ // Document-grain audit log for the "Document history" tab (W3). Fetched
98
+ // in parallel and unconditionally — it's cheap, and the active tab is a
99
+ // pure render concern read from the URL, so switching tabs never
100
+ // refetches.
101
+ getCollectionDocumentAuditLog({
102
+ data: {
103
+ collection: params.collection,
104
+ id: params.id,
105
+ params: { page: 1, page_size: AUDIT_LOG_PAGE_SIZE },
106
+ },
107
+ }),
87
108
  ])
88
109
 
89
- return { history, currentDocument }
110
+ return { history, currentDocument, auditLog }
90
111
  },
91
112
  staleTime: 0,
92
113
  gcTime: 0,
93
114
  shouldReload: true,
94
115
  component: function CollectionHistoryComponent() {
95
- const { history, currentDocument } = Route.useLoaderData()
116
+ const { history, currentDocument, auditLog } = Route.useLoaderData()
96
117
  const { collection } = Route.useParams() as { collection: string; id: string }
97
118
  const collectionDef = getCollectionDefinition(collection) as CollectionDefinition
98
119
  const adminConfig = getCollectionAdminConfig(collection)
@@ -119,6 +140,7 @@ export function createCollectionHistoryRoute(path: string, opts: CollectionHisto
119
140
  workflowStatuses={getWorkflowStatuses(collectionDef)}
120
141
  adminConfig={adminConfig ?? undefined}
121
142
  data={history}
143
+ auditLog={auditLog}
122
144
  currentDocument={currentDocument as Record<string, unknown> | null}
123
145
  contentLocales={opts.contentLocales}
124
146
  defaultContentLocale={opts.defaultContentLocale}
@@ -0,0 +1,97 @@
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 { ERR_NOT_FOUND, getLogger } from '@byline/core'
12
+
13
+ import { ensureCollection } from '../../integrations/api-utils.js'
14
+ import { getAdminBylineClient } from '../../integrations/byline-client.js'
15
+ import { type ActorLabelMap, resolveActorLabels } from './actors.js'
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Shared param types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface AuditLogSearchParams {
22
+ page?: number
23
+ page_size?: number
24
+ }
25
+
26
+ /**
27
+ * Audit entry as it crosses the server-fn boundary. `occurredAt` is an ISO
28
+ * string (Date doesn't survive serialization) and `before` / `after` are
29
+ * narrowed from the storage layer's `unknown` jsonb to the concrete shapes the
30
+ * shipped actions actually carry (path/status strings, the available-locales
31
+ * array, or null for the deletion event) — `unknown` is not a serializable
32
+ * type the TanStack server-fn validator accepts.
33
+ */
34
+ export interface AuditLogEntryDto {
35
+ id: string
36
+ documentId: string | null
37
+ collectionId: string | null
38
+ actorId: string | null
39
+ actorRealm: string
40
+ action: string
41
+ field: string | null
42
+ before: string | string[] | null
43
+ after: string | string[] | null
44
+ occurredAt: string
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Get document-grain audit log (docs/AUDIT.md — Workstream 3)
49
+ // ---------------------------------------------------------------------------
50
+
51
+ export const getCollectionDocumentAuditLog = createServerFn({ method: 'GET' })
52
+ .inputValidator(
53
+ (input: { collection: string; id: string; params?: AuditLogSearchParams }) => input
54
+ )
55
+ .handler(async ({ data }) => {
56
+ const { collection: path, id, params } = data
57
+ const config = await ensureCollection(path)
58
+ if (!config) {
59
+ throw ERR_NOT_FOUND({
60
+ message: 'Collection not found',
61
+ details: { collectionPath: path },
62
+ }).log(getLogger())
63
+ }
64
+
65
+ // Routes through CollectionHandle.auditLog so the document's own read gate
66
+ // (`beforeRead` via `findById`) is applied — identical to the history
67
+ // server fn. An actor whose predicate excludes the document gets an empty
68
+ // log rather than leaked change metadata.
69
+ const result = await getAdminBylineClient().collection(path).auditLog(id, {
70
+ page: params?.page,
71
+ pageSize: params?.page_size,
72
+ })
73
+
74
+ // Acting-user labels for the audit list (docs/AUDIT.md — W3). Resolved
75
+ // here, in the admin realm, from each entry's raw `actorId`; the UI joins
76
+ // by id. System/tooling rows (NULL actorId) and deleted users are absent
77
+ // from the map — the UI renders the corresponding tombstone label.
78
+ const actors: ActorLabelMap = await resolveActorLabels(result.entries.map((e) => e.actorId))
79
+
80
+ // Map to the serializable DTO: ISO-string the timestamp and narrow the
81
+ // jsonb before/after off `unknown` so the value survives the TanStack
82
+ // server-fn boundary.
83
+ const entries: AuditLogEntryDto[] = result.entries.map((e) => ({
84
+ id: e.id,
85
+ documentId: e.documentId,
86
+ collectionId: e.collectionId,
87
+ actorId: e.actorId,
88
+ actorRealm: e.actorRealm,
89
+ action: e.action,
90
+ field: e.field,
91
+ before: e.before as string | string[] | null,
92
+ after: e.after as string | string[] | null,
93
+ occurredAt: e.occurredAt instanceof Date ? e.occurredAt.toISOString() : String(e.occurredAt),
94
+ }))
95
+
96
+ return { entries, meta: result.meta, actors }
97
+ })
@@ -5,6 +5,7 @@
5
5
  * function and exports a clean public API.
6
6
  */
7
7
 
8
+ export * from './audit'
8
9
  export * from './copy-to-locale'
9
10
  export * from './create'
10
11
  export * from './delete'