import https from 'https'; import { URLSearchParams } from 'url'; import _ from 'lodash'; import { SanityClient as SanityClientType, SanityDocument } from '@sanity/client'; import { isDraftId } from './sanity-document-converter'; export { default as SanityClient } from '@sanity/client'; export interface SanityUser { id: string; projectId: string; displayName: string; email?: string; familyName: string | null; givenName: string | null; middleName: string | null; imageUrl: string | null; createdAt: string; updatedAt: string; sanityUserId: string; provider: string; } export interface DocumentHistory { id: string; timestamp: string; author: string; documentIDs: string[]; } export type DocumentHistoryMap = Record; export async function testToken(apiToken: string): Promise<{ accessToken?: string; accessTokenExpiresAt?: string; client?: { id?: string }; user?: { id?: string }; }> { return new Promise((resolve, reject) => { let output = ''; const req = https.request( { hostname: 'api.sanity.io', path: `/v1/auth/oauth/tokens/${apiToken}`, method: 'GET' }, (res) => { res.setEncoding('utf8'); res.on('data', (chunk) => { output += chunk; }); res.on('end', () => { let response = null; try { response = JSON.parse(output); } catch (error) { reject(error); return; } if (res.statusCode !== 200) { reject(response); return; } resolve(response); }); } ); req.on('error', (error) => { reject(error); }); req.end(); }); } export async function fetchDocumentsHistory({ documentIds, dataset, client, limitTime = true }: { documentIds: string[]; dataset: string; client: SanityClientType; limitTime?: boolean; }): Promise { if (!documentIds.length) { return []; } // max size of one request to Sanity is 32Kb // Sanity response: AssertionError [ERR_ASSERTION]: Your message must be < 32kb. // it's undocumented, so 11Kb is query limit - https://www.sanity.io/docs/http-query, hence stick to that const chunkedDocumentIds = chunkArray(documentIds, 11000); const result: DocumentHistory[] = []; for (const idsChunk of chunkedDocumentIds) { const searchParams = new URLSearchParams(); searchParams.append('excludeContent', 'true'); searchParams.append('reverse', 'true'); if (limitTime) { // get docs history from last 30 days searchParams.append('fromTime', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()); } const data = await client.request({ uri: `data/history/${dataset}/transactions/${idsChunk.join(',')}?${searchParams.toString()}` }); for (const line of data.split('\n')) { if (line) { result.push(JSON.parse(line)); } } } return result; } export async function fetchDocumentRevision({ documentId, draftDocumentId, versionId, dataset, client }: { documentId: string; draftDocumentId?: string; versionId: string; dataset: string; client: SanityClientType; }): Promise { const searchParams = new URLSearchParams(); searchParams.append('excludeContent', 'true'); searchParams.append('fromTransaction', versionId); searchParams.append('toTransaction', versionId); const documentIds = [documentId, draftDocumentId]; const data = await client.request({ uri: `data/history/${dataset}/transactions/${documentIds.join(',')}?${searchParams.toString()}` }); return JSON.parse(data); } export async function fetchDocumentForRevision({ documentId, draftDocumentId, versionId, dataset, client }: { documentId: string; draftDocumentId?: string; versionId: string; dataset: string; client: SanityClientType; }): Promise { const documentIds = [documentId, draftDocumentId]; const data = await client.request({ uri: `data/history/${dataset}/documents/${documentIds.join(',')}?revision=${versionId}` }); // take either latest draft revision object, or latest object return data.documents.find((object: SanityDocument) => isDraftId(object._id)) ?? data.documents[data.documents.length - 1]; } export async function fetchUsers(userIds: string[], client: SanityClientType): Promise { const chunkedDocumentIds = chunkArray(userIds, 11000); const result = []; for (const idsChunk of chunkedDocumentIds) { const data = await client.request({ uri: `users/${idsChunk.join(',')}` }); // sanity respond differently for one user and multiple users // users/p-3vin4nyLUym1 => { 'id': 'p-3vin4nyLUym1', displayName: ... } // users/p-3vin4nyLUym1,p12Yq4phv => [{ 'id': 'p-3vin4nyLUym1', displayName: ... }, { 'id': 'p12Yq4phv', displayName: ... }] result.push(...(_.isArray(data) ? data : [data])); } return result; } export async function fetchScheduledActions( opts: { projectId: string; dataset: string }, client: SanityClientType, filters?: { documentIds?: string[]; state?: string } ) { const uri = `/schedules/${opts.projectId}/${opts.dataset}`; const result = await client.request({ method: 'GET', uri: uri, query: filters }); return result?.schedules ?? []; } export function chunkArray(arr: string[], maxChunkLength: number): string[][] { const chunks = []; let currentChunkLength = 0; let currentChunk = [] as string[]; for (const [index, val] of arr.entries()) { if (val.length + currentChunkLength >= maxChunkLength) { if (currentChunk.length) { chunks.push(currentChunk); } currentChunkLength = val.length; currentChunk = [val]; } else { currentChunk.push(val); currentChunkLength += val.length + 1; // for comma } if (arr.length === index + 1) { chunks.push(currentChunk); } } return chunks.length ? chunks : [arr]; }