@stackbit/cms-sanity
Version:
Stackbit Sanity CMS Interface
210 lines (192 loc) • 6.63 kB
text/typescript
import https from 'https';
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<string, DocumentHistory[]>;
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<DocumentHistory[]> {
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<DocumentHistory> {
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<SanityDocument> {
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<SanityUser[]> {
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];
}