Skip to content

Commit

Permalink
chore(core): add getTransactionLogs helper (#8261)
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin authored Jan 15, 2025
1 parent cef5237 commit 03e7760
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 23 deletions.
118 changes: 118 additions & 0 deletions packages/sanity/src/core/store/translog/getTransactionsLogs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {type SanityClient} from '@sanity/client'
import {beforeEach, describe, expect, it, type Mock, vi} from 'vitest'

import {getJsonStream} from '../_legacy/history/history/getJsonStream'
import {getTransactionsLogs} from './getTransactionsLogs'

vi.mock('../_legacy/history/history/getJsonStream', () => ({
getJsonStream: vi.fn(),
}))

const getJsonStreamMock = getJsonStream as Mock

describe('getTransactionsLogs', () => {
const mockClient = {
config: vi.fn(() => ({
dataset: 'mockDataset',
token: 'mockToken',
})),
getUrl: vi.fn((path) => `https://mock.sanity.api${path}`),
} as unknown as SanityClient

const mockStream = {
getReader: vi.fn(() => ({
async read() {
return {done: true}
},
})),
}

beforeEach(() => {
vi.clearAllMocks()
})

it('should fetch transaction logs with default parameters', async () => {
getJsonStreamMock.mockResolvedValueOnce(mockStream)

const documentId = 'doc1'
const result = await getTransactionsLogs(mockClient, documentId, {})

expect(mockClient.getUrl).toHaveBeenCalledWith(
'/data/history/mockDataset/transactions/doc1?tag=sanity.studio.transactions-log&excludeContent=true&limit=50&includeIdentifiedDocumentsOnly=true',
)
expect(getJsonStream).toHaveBeenCalledWith(
'https://mock.sanity.api/data/history/mockDataset/transactions/doc1?tag=sanity.studio.transactions-log&excludeContent=true&limit=50&includeIdentifiedDocumentsOnly=true',
'mockToken',
)
expect(result).toEqual([])
})

it('should handle multiple document IDs', async () => {
getJsonStreamMock.mockResolvedValueOnce(mockStream)

const documentIds = ['doc1', 'doc2']
await getTransactionsLogs(mockClient, documentIds, {})

expect(mockClient.getUrl).toHaveBeenCalledWith(
'/data/history/mockDataset/transactions/doc1,doc2?tag=sanity.studio.transactions-log&excludeContent=true&limit=50&includeIdentifiedDocumentsOnly=true',
)
expect(getJsonStream).toHaveBeenCalledWith(
'https://mock.sanity.api/data/history/mockDataset/transactions/doc1,doc2?tag=sanity.studio.transactions-log&excludeContent=true&limit=50&includeIdentifiedDocumentsOnly=true',
'mockToken',
)
})

it('should override default parameters with user-provided params', async () => {
getJsonStreamMock.mockResolvedValueOnce(mockStream)

const documentId = 'doc1'
await getTransactionsLogs(mockClient, documentId, {
tag: 'sanity.studio.custom-tag',
limit: 100,
fromTransaction: 'tx1',
toTransaction: 'tx3',
})

expect(mockClient.getUrl).toHaveBeenCalledWith(
'/data/history/mockDataset/transactions/doc1?tag=sanity.studio.custom-tag&excludeContent=true&limit=100&includeIdentifiedDocumentsOnly=true&fromTransaction=tx1&toTransaction=tx3',
)
})

it('should throw an error if the stream contains an error', async () => {
const mockErrorStream = {
getReader: vi.fn(() => ({
async read() {
return {done: false, value: {error: {description: 'Error occurred'}}}
},
})),
}
getJsonStreamMock.mockResolvedValueOnce(mockErrorStream)

const documentId = 'doc1'

await expect(getTransactionsLogs(mockClient, documentId, {})).rejects.toThrow('Error occurred')
})

it('should collect transactions from the stream', async () => {
const mockDataStream = {
getReader: vi.fn(() => {
let callCount = 0
return {
async read() {
if (callCount < 3) {
callCount++
return {done: false, value: {id: `txn${callCount}`}}
}
return {done: true}
},
}
}),
}
getJsonStreamMock.mockResolvedValueOnce(mockDataStream)

const documentId = 'doc1'
const result = await getTransactionsLogs(mockClient as any, documentId, {})

expect(result).toEqual([{id: 'txn1'}, {id: 'txn2'}, {id: 'txn3'}])
})
})
108 changes: 108 additions & 0 deletions packages/sanity/src/core/store/translog/getTransactionsLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {type SanityClient} from '@sanity/client'
import {type TransactionLogEventWithEffects} from '@sanity/types'

import {getJsonStream} from '../_legacy/history/history/getJsonStream'

/**
* Fetches transaction logs for the specified document IDs from the translog
* It adds the default query parameters to the request and also reads the stream of transactions.
* @internal
*/
export async function getTransactionsLogs(
client: SanityClient,
/**
* 1 or more document IDs to fetch transaction logs */
documentIds: string | string[],
/**
* {@link https://www.sanity.io/docs/history-api#45ac5eece4ca}
*/
params: {
/**
* The tag that will be use when fetching the transactions.
* (Default: sanity.studio.transactions-log)
*/
tag?: `sanity.studio.${string}`
/**
* Exclude the document contents from the responses. (You are required to set excludeContent as true for now.)
* (Default: true)
*/
excludeContent?: true
/**
* Limit the number of returned transactions. (Default: 50)
*/
limit?: number

/**
* Only include the documents that are part of the document ids list
* (Default: true)
*/
includeIdentifiedDocumentsOnly?: boolean

/**
* How the effects are formatted in the response.
* "mendoza": Super efficient format for expressing differences between JSON documents. {@link https://www.sanity.io/blog/mendoza}
*/
effectFormat?: 'mendoza' | undefined
/**
* Return transactions in reverse order.
*/
reverse?: boolean
/**
* Time from which the transactions are fetched.
*/
fromTime?: string
/**
* Time until the transactions are fetched.
*/
toTime?: string
/**
* Transaction ID (Or, Revision ID) from which the transactions are fetched.
*/
fromTransaction?: string
/**
* Transaction ID (Or, Revision ID) until the transactions are fetched.
*/
toTransaction?: string
/**
* Comma separated list of authors to filter the transactions by.
*/
authors?: string
},
): Promise<TransactionLogEventWithEffects[]> {
const clientConfig = client.config()
const dataset = clientConfig.dataset
const queryParams = new URLSearchParams({
// Default values
tag: 'sanity.studio.transactions-log',
excludeContent: 'true',
limit: '50',
includeIdentifiedDocumentsOnly: 'true',
})
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.set(key, value.toString())
}
})

const transactionsUrl = client.getUrl(
`/data/history/${dataset}/transactions/${
Array.isArray(documentIds) ? documentIds.join(',') : documentIds
}?${queryParams.toString()}`,
)

const stream = await getJsonStream(transactionsUrl, clientConfig.token)
const transactions: TransactionLogEventWithEffects[] = []

const reader = stream.getReader()
for (;;) {
// eslint-disable-next-line no-await-in-loop
const result = await reader.read()
if (result.done) break

if ('error' in result.value) {
throw new Error(result.value.error.description || result.value.error.type)
}
transactions.push(result.value)
}
return transactions
}
30 changes: 7 additions & 23 deletions packages/sanity/src/core/tasks/hooks/useActivityLog.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {type TransactionLogEventWithEffects} from '@sanity/types'
import {useCallback, useEffect, useState} from 'react'
import {useEffectEvent} from 'use-effect-event'

import {useClient} from '../../hooks'
import {getJsonStream} from '../../store/_legacy/history/history/getJsonStream'
import {getTransactionsLogs} from '../../store/translog/getTransactionsLogs'
import {getPublishedId} from '../../util'
import {type FieldChange, trackFieldChanges} from '../components/activity/helpers/parseTransactions'
import {API_VERSION} from '../constants'
Expand All @@ -14,33 +13,18 @@ export function useActivityLog(task: TaskDocument): {
} {
const [changes, setChanges] = useState<FieldChange[]>([])
const client = useClient({apiVersion: API_VERSION})
const {dataset, token} = client.config()

const queryParams = `tag=sanity.studio.tasks.history&effectFormat=mendoza&excludeContent=true&includeIdentifiedDocumentsOnly=true&reverse=true`
const publishedId = getPublishedId(task?._id ?? '')
const transactionsUrl = client.getUrl(
`/data/history/${dataset}/transactions/${publishedId}?${queryParams}`,
)

const fetchAndParse = useCallback(
async (newestTaskDocument: TaskDocument) => {
try {
if (!publishedId) return
const transactions: TransactionLogEventWithEffects[] = []

const stream = await getJsonStream(transactionsUrl, token)
const reader = stream.getReader()
let result
for (;;) {
result = await reader.read()
if (result.done) {
break
}
if ('error' in result.value) {
throw new Error(result.value.error.description || result.value.error.type)
}
transactions.push(result.value)
}
const transactions = await getTransactionsLogs(client, publishedId, {
tag: 'sanity.studio.tasks.history',
effectFormat: 'mendoza',
reverse: true,
})

const fieldsToTrack: (keyof Omit<TaskDocument, '_rev'>)[] = [
'createdByUser',
Expand All @@ -63,7 +47,7 @@ export function useActivityLog(task: TaskDocument): {
console.error('Failed to fetch and parse activity log', error)
}
},
[transactionsUrl, token, publishedId],
[publishedId, client],
)

// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
Expand Down

0 comments on commit 03e7760

Please sign in to comment.