From 866249a8b04095ecc0c6c1b6d39a5bf7a8ebedef Mon Sep 17 00:00:00 2001 From: Steven Le Date: Fri, 16 May 2025 23:02:39 -0700 Subject: [PATCH] feat: save version on publish --- packages/root-cms/core/client.ts | 57 ++++++++++++++++++++++++++- packages/root-cms/ui/utils/doc.ts | 40 ++++++++++++++++--- packages/root-cms/ui/utils/release.ts | 2 +- 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/packages/root-cms/core/client.ts b/packages/root-cms/core/client.ts index e9c44e7a..1c58942a 100644 --- a/packages/root-cms/core/client.ts +++ b/packages/root-cms/core/client.ts @@ -345,7 +345,7 @@ export class RootCMSClient { */ async publishDocs( docIds: string[], - options?: {publishedBy: string; batch?: WriteBatch} + options?: {publishedBy: string; batch?: WriteBatch; releaseId?: string} ) { const projectCollectionsPath = `Projects/${this.projectId}/Collections`; const publishedBy = options?.publishedBy || 'root-cms-client'; @@ -384,6 +384,10 @@ export class RootCMSClient { // // https://firebase.google.com/docs/firestore/manage-data/transactions let batchCount = 0; const batch = options?.batch || this.db.batch(); + const versionTags = ['published']; + if (options?.releaseId) { + versionTags.push(`release:${options.releaseId}`); + } const publishedDocs: any[] = []; for (const doc of docs) { const {id, collection, slug, sys, fields} = doc; @@ -418,6 +422,29 @@ export class RootCMSClient { }); batchCount += 1; + // Save a version snapshot of the published doc. + const versionRef = this.db.doc( + `${projectCollectionsPath}/${collection}/Drafts/${slug}/Versions/${Date.now()}` + ); + const versionData: any = { + id, + collection, + slug, + fields: fields || {}, + sys: { + ...sys, + firstPublishedAt: firstPublishedAt, + firstPublishedBy: firstPublishedBy, + publishedAt: FieldValue.serverTimestamp(), + publishedBy: publishedBy, + }, + }; + if (versionTags.length) { + versionData.tags = versionTags; + } + batch.set(versionRef, versionData); + batchCount += 1; + // Remove scheduled doc, if any. batch.delete(scheduledRef); batchCount += 1; @@ -495,6 +522,7 @@ export class RootCMSClient { // https://firebase.google.com/docs/firestore/manage-data/transactions let batchCount = 0; const batch = this.db.batch(); + const versionTags = ['published']; const publishedDocs: any[] = []; for (const doc of docs) { const {id, collection, slug, data} = doc; @@ -530,6 +558,27 @@ export class RootCMSClient { }); batchCount += 1; + // Save a version snapshot of the published doc. + const versionRef = this.db.doc( + `${projectCollectionsPath}/${collection}/Drafts/${slug}/Versions/${Date.now()}` + ); + const versionData: any = { + id, + collection, + slug, + fields: data.fields || {}, + sys: { + ...sys, + firstPublishedAt: firstPublishedAt, + firstPublishedBy: firstPublishedBy, + publishedAt: FieldValue.serverTimestamp(), + publishedBy: scheduledBy || '', + }, + tags: versionTags, + }; + batch.set(versionRef, versionData); + batchCount += 1; + // Remove published doc. batch.delete(scheduledRef); batchCount += 1; @@ -583,7 +632,11 @@ export class RootCMSClient { scheduledAt: FieldValue.delete(), scheduledBy: FieldValue.delete(), }); - await this.publishDocs(release.docIds || [], {publishedBy, batch}); + await this.publishDocs(release.docIds || [], { + publishedBy, + batch, + releaseId: release.id, + }); } } diff --git a/packages/root-cms/ui/utils/doc.ts b/packages/root-cms/ui/utils/doc.ts index 0c133dd2..0363bdf9 100644 --- a/packages/root-cms/ui/utils/doc.ts +++ b/packages/root-cms/ui/utils/doc.ts @@ -15,6 +15,7 @@ import { updateDoc, documentId, where, + Query, WriteBatch, DocumentReference, } from 'firebase/firestore'; @@ -61,6 +62,7 @@ export interface CMSDoc { export type Version = CMSDoc & { _versionId: string; + tags?: string[]; }; export async function cmsDeleteDoc(docId: string) { @@ -116,7 +118,7 @@ export async function cmsPublishDoc(docId: string) { */ export async function cmsPublishDocs( docIds: string[], - options?: {batch?: WriteBatch} + options?: {batch?: WriteBatch; releaseId?: string} ) { if (docIds.length === 0) { console.log('no docs to publish'); @@ -133,12 +135,16 @@ export async function cmsPublishDocs( const draftDocs = await getDraftDocs(docIds); const batch = options?.batch || writeBatch(db); + const versionTags = ['published']; + if (options?.releaseId) { + versionTags.push(`release:${options.releaseId}`); + } docIds.forEach((docId) => { const draftData = draftDocs[docId]; if (!draftData) { throw new Error(`doc does not exist: ${docId}`); } - updatePublishedDocDataInBatch(batch, docId, draftData); + updatePublishedDocDataInBatch(batch, docId, draftData, versionTags); }); await batch.commit(); @@ -165,7 +171,8 @@ export async function cmsPublishDocs( function updatePublishedDocDataInBatch( batch: WriteBatch, docId: string, - draftData: CMSDoc + draftData: CMSDoc, + versionTags: string[] ) { if (testPublishingLocked(draftData)) { throw new Error(`publishing is locked for doc: ${draftData.id}`); @@ -221,6 +228,23 @@ function updatePublishedDocDataInBatch( batch.set(publishedDocRef, {...data, sys}); // Delete any "scheduled" docs if it exists. batch.delete(scheduledDocRef); + // Save a version snapshot of the published doc. + const versionRef = doc( + db, + 'Projects', + projectId, + 'Collections', + collectionId, + 'Drafts', + slug, + 'Versions', + String(Date.now()) + ); + const versionData: any = {id: docId, collection: collectionId, slug, fields: data.fields || {}, sys}; + if (versionTags?.length) { + versionData.tags = versionTags; + } + batch.set(versionRef, versionData); } /** @@ -673,11 +697,17 @@ export async function getDraftDocs( return drafts; } -export async function cmsListVersions(docId: string) { +export async function cmsListVersions( + docId: string, + options?: {tags?: string[]} +) { const db = window.firebase.db; const docRef = getDraftDocRef(docId); const versionsCollection = collection(db, docRef.path, 'Versions'); - const q = query(versionsCollection, orderBy('sys.modifiedAt', 'desc')); + let q: Query = query(versionsCollection, orderBy('sys.modifiedAt', 'desc')); + if (options?.tags) { + q = query(q, where('tags', 'array-contains-any', options.tags)); + } const querySnapshot = await getDocs(q); const versions: Version[] = []; querySnapshot.forEach((doc) => { diff --git a/packages/root-cms/ui/utils/release.ts b/packages/root-cms/ui/utils/release.ts index ba873399..bc4af0cf 100644 --- a/packages/root-cms/ui/utils/release.ts +++ b/packages/root-cms/ui/utils/release.ts @@ -117,7 +117,7 @@ export async function publishRelease(id: string) { scheduledAt: deleteField(), scheduledBy: deleteField(), }); - await cmsPublishDocs(docIds, {batch}); + await cmsPublishDocs(docIds, {batch, releaseId: id}); console.log(`published release: ${id}`); logAction('release.publish', {metadata: {releaseId: id, docIds: docIds}}); }