@stackbit/cms-sanity 0.2.23-develop.2 → 0.2.23-staging.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.
@@ -1,6 +1,7 @@
1
1
  import https from 'https';
2
2
  import _ from 'lodash';
3
3
  import { SanityClient as SanityClientType, SanityDocument } from '@sanity/client';
4
+ import { isDraftId } from './sanity-document-converter';
4
5
  export { default as SanityClient } from '@sanity/client';
5
6
 
6
7
  export interface SanityUser {
@@ -72,20 +73,35 @@ export async function testToken(apiToken: string): Promise<{
72
73
  });
73
74
  }
74
75
 
75
- export async function fetchDocumentsHistory(documentIds: string[], dataset: string, client: SanityClientType): Promise<DocumentHistory[]> {
76
+ export async function fetchDocumentsHistory({
77
+ documentIds,
78
+ dataset,
79
+ client,
80
+ limitTime = true
81
+ }: {
82
+ documentIds: string[];
83
+ dataset: string;
84
+ client: SanityClientType;
85
+ limitTime?: boolean;
86
+ }): Promise<DocumentHistory[]> {
76
87
  if (!documentIds.length) {
77
88
  return [];
78
89
  }
79
- // get docs history from last 30 days
80
- const fromTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
81
90
  // max size of one request to Sanity is 32Kb
82
91
  // Sanity response: AssertionError [ERR_ASSERTION]: Your message must be < 32kb.
83
92
  // it's undocumented, so 11Kb is query limit - https://www.sanity.io/docs/http-query, hence stick to that
84
93
  const chunkedDocumentIds = chunkArray(documentIds, 11000);
85
94
  const result: DocumentHistory[] = [];
86
95
  for (const idsChunk of chunkedDocumentIds) {
96
+ const searchParams = new URLSearchParams();
97
+ searchParams.append('excludeContent', 'true');
98
+ searchParams.append('reverse', 'true');
99
+ if (limitTime) {
100
+ // get docs history from last 30 days
101
+ searchParams.append('fromTime', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
102
+ }
87
103
  const data = await client.request({
88
- uri: `data/history/${dataset}/transactions/${idsChunk.join(',')}?excludeContent=true&fromTime=${fromTime}&reverse=true`
104
+ uri: `data/history/${dataset}/transactions/${idsChunk.join(',')}?${searchParams.toString()}`
89
105
  });
90
106
  for (const line of data.split('\n')) {
91
107
  if (line) {
@@ -96,6 +112,51 @@ export async function fetchDocumentsHistory(documentIds: string[], dataset: stri
96
112
  return result;
97
113
  }
98
114
 
115
+ export async function fetchDocumentRevision({
116
+ documentId,
117
+ draftDocumentId,
118
+ versionId,
119
+ dataset,
120
+ client
121
+ }: {
122
+ documentId: string;
123
+ draftDocumentId?: string;
124
+ versionId: string;
125
+ dataset: string;
126
+ client: SanityClientType;
127
+ }): Promise<DocumentHistory> {
128
+ const searchParams = new URLSearchParams();
129
+ searchParams.append('excludeContent', 'true');
130
+ searchParams.append('fromTransaction', versionId);
131
+ searchParams.append('toTransaction', versionId);
132
+ const documentIds = [documentId, draftDocumentId];
133
+ const data = await client.request({
134
+ uri: `data/history/${dataset}/transactions/${documentIds.join(',')}?${searchParams.toString()}`
135
+ });
136
+ return JSON.parse(data);
137
+ }
138
+
139
+ export async function fetchDocumentForRevision({
140
+ documentId,
141
+ draftDocumentId,
142
+ versionId,
143
+ dataset,
144
+ client
145
+ }: {
146
+ documentId: string;
147
+ draftDocumentId?: string;
148
+ versionId: string;
149
+ dataset: string;
150
+ client: SanityClientType;
151
+ }): Promise<SanityDocument> {
152
+ const documentIds = [documentId, draftDocumentId];
153
+ const data = await client.request({
154
+ uri: `data/history/${dataset}/documents/${documentIds.join(',')}?revision=${versionId}`
155
+ });
156
+ // take either latest draft revision object, or latest object
157
+ return data.documents.find((object: SanityDocument) => isDraftId(object._id)) ?? data.documents[data.documents.length - 1];
158
+ }
159
+
99
160
  export async function fetchUsers(userIds: string[], client: SanityClientType): Promise<SanityUser[]> {
100
161
  const chunkedDocumentIds = chunkArray(userIds, 11000);
101
162
  const result = [];
@@ -6,7 +6,18 @@ import path from 'path';
6
6
  import fse from 'fs-extra';
7
7
  import { glob } from 'glob';
8
8
  import { MutationEvent, PatchOperations, SanityAssetDocument, SanityClient as SanityClientType, SanityDocument, SanityDocumentStub } from '@sanity/client';
9
- import type { Field, FieldList, FieldListItems, FieldObjectProps, FieldSpecificProps, Model, Schema, UserCommandSpawner } from '@stackbit/types';
9
+ import type {
10
+ DocumentVersion,
11
+ DocumentVersionWithDocument,
12
+ Field,
13
+ FieldList,
14
+ FieldListItems,
15
+ FieldObjectProps,
16
+ FieldSpecificProps,
17
+ Model,
18
+ Schema,
19
+ UserCommandSpawner
20
+ } from '@stackbit/types';
10
21
  import type * as ContentSourceTypes from '@stackbit/types';
11
22
  import { getVersion as stackbitUtilsGetVersion } from '@stackbit/types';
12
23
 
@@ -16,10 +27,12 @@ import {
16
27
  testToken,
17
28
  DocumentHistory,
18
29
  DocumentHistoryMap,
19
- fetchDocumentsHistory,
20
30
  fetchUsers,
21
31
  SanityUser,
22
- fetchScheduledActions
32
+ fetchScheduledActions,
33
+ fetchDocumentsHistory,
34
+ fetchDocumentForRevision,
35
+ fetchDocumentRevision
23
36
  } from './sanity-api-client';
24
37
  import { convertSchema } from './sanity-schema-converter';
25
38
  import {
@@ -431,6 +444,30 @@ export class SanityContentSource implements ContentSourceTypes.ContentSourceInte
431
444
  return this.cache.updateContent(result);
432
445
  }
433
446
 
447
+ private convertVersionsFromDocumentHistory(versions: DocumentHistory[]): DocumentVersion[] {
448
+ return versions.map((version) => {
449
+ return {
450
+ id: version.id,
451
+ documentId: getPureObjectId(version.documentIDs[0]!),
452
+ srcType: this.getContentSourceType(),
453
+ srcProjectId: this.projectId,
454
+ createdAt: version.timestamp,
455
+ createdBy: this.userMap[version.author]?.email
456
+ };
457
+ });
458
+ }
459
+
460
+ private convertVersionForDocument(version: DocumentHistory, document: ContextualDocument): DocumentVersionWithDocument {
461
+ const [documentVersion] = this.convertVersionsFromDocumentHistory([version]);
462
+ if (!documentVersion) {
463
+ throw new Error('Document version could not be converted');
464
+ }
465
+ return {
466
+ ...documentVersion,
467
+ document
468
+ };
469
+ }
470
+
434
471
  async getSanitySchema(): Promise<any> {
435
472
  let sanitySchema;
436
473
  try {
@@ -461,7 +498,7 @@ export class SanityContentSource implements ContentSourceTypes.ContentSourceInte
461
498
  this.logger.debug('getDocumentsHistory started', documentIds.length);
462
499
 
463
500
  const draftDocumentIds = documentIds.filter((documentId) => isDraftId(documentId));
464
- const historyData = await fetchDocumentsHistory(draftDocumentIds, this.dataset, this.client);
501
+ const historyData = await fetchDocumentsHistory({ documentIds: draftDocumentIds, dataset: this.dataset, client: this.client });
465
502
 
466
503
  const notCachedDocAuthors = historyData.reduce((acc: string[], doc: DocumentHistory) => {
467
504
  const user = this.userMap[doc.author];
@@ -899,6 +936,44 @@ export class SanityContentSource implements ContentSourceTypes.ContentSourceInte
899
936
  apiVersion: SANITY_API_VERSION
900
937
  });
901
938
  }
939
+
940
+ async getDocumentVersions({ documentId }: { documentId: string }): Promise<{ versions: DocumentVersion[] }> {
941
+ this.logger.debug('getDocumentVersions', { documentId });
942
+
943
+ const documentHistory = await fetchDocumentsHistory({
944
+ documentIds: [documentId, getDraftObjectId(documentId)],
945
+ dataset: this.dataset,
946
+ client: this.client,
947
+ limitTime: false
948
+ });
949
+
950
+ if (!documentHistory) {
951
+ return { versions: [] };
952
+ }
953
+
954
+ return { versions: this.convertVersionsFromDocumentHistory(documentHistory) };
955
+ }
956
+
957
+ async getDocumentForVersion({ documentId, versionId }: { documentId: string; versionId: string }): Promise<{ version: DocumentVersionWithDocument }> {
958
+ this.logger.debug('getDocumentForVersion', { documentId, versionId });
959
+
960
+ const draftDocumentId = getDraftObjectId(documentId);
961
+ const [version, document] = await Promise.all([
962
+ fetchDocumentRevision({ documentId, draftDocumentId, versionId, dataset: this.dataset, client: this.client }),
963
+ fetchDocumentForRevision({ documentId, draftDocumentId, versionId, dataset: this.dataset, client: this.client })
964
+ ]);
965
+ const [contextualDocument] = await this.convertDocuments({
966
+ documents: [document],
967
+ getModelByName: this.cache.getModelByName,
968
+ studioUrl: this.studioUrl
969
+ });
970
+
971
+ if (!contextualDocument) {
972
+ throw new Error(`Could not get document ${documentId} for revision ${versionId}`);
973
+ }
974
+
975
+ return { version: this.convertVersionForDocument(version, contextualDocument) };
976
+ }
902
977
  }
903
978
 
904
979
  function mapUpdateOperationFieldToSanityValue(
@@ -5,7 +5,8 @@ import { deepMap, omitByNil } from '@stackbit/utils';
5
5
  import { resolveLabelFieldForModel } from './utils';
6
6
 
7
7
  // sanity cloudinary plugin adds these models, we are replacing them to our internal image model, so we removing them from schema response
8
- const skipModels = ['cloudinary.asset', 'cloudinary.assetDerived', 'bynder.asset', 'aprimo.asset', 'aprimo.cdnasset', 'media.tag'];
8
+ const thirdPartyImageModels = ['cloudinary.asset', 'cloudinary.assetDerived', 'bynder.asset', 'aprimo.asset', 'aprimo.cdnasset'];
9
+ const skipModels = [...thirdPartyImageModels, 'media.tag'];
9
10
 
10
11
  export function convertSchema(schema: any): { models: Model[] } {
11
12
  // sanity schema allow arrays to be at the root level, we don't.
@@ -238,6 +239,10 @@ const fieldConverterMap = {
238
239
  if (_.has(arrayModelsByName, type)) {
239
240
  return fieldConverterMap.array(arrayModelsByName[type], arrayModelsByName);
240
241
  }
242
+ if (thirdPartyImageModels.includes(type)) {
243
+ const fn = (fieldConverterMap[type as keyof typeof fieldConverterMap] as any) ?? fieldConverterMap.image;
244
+ return fn();
245
+ }
241
246
  return {
242
247
  type: 'model',
243
248
  models: [type]