Skip to content

Commit b1fe8b1

Browse files
committed
Add Content History
1 parent f34e9f2 commit b1fe8b1

16 files changed

+506
-129
lines changed

functions/src/contents.ts

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
import { logger } from 'firebase-functions/v2';
22
import { HttpsError, onCall } from 'firebase-functions/v2/https';
33
import { onDocumentDeleted, onDocumentUpdated, onDocumentWritten } from 'firebase-functions/v2/firestore';
4-
import { FieldValue, UpdateData } from 'firebase-admin/firestore';
4+
import { FieldValue, UpdateData, WithFieldValue } from 'firebase-admin/firestore';
55
import { canPerform } from './utils/security-utils';
66
import { bucket, firestoreService } from './config';
7-
import { Content, ContentDocument, ContentDocumentStorage, ContentKind, PublishContentData, Schema, Space, UserPermission } from './models';
8-
import { extractContent, findContentByFullSlug, findContentById, findSchemas, findSpaceById } from './services';
7+
import {
8+
Content,
9+
ContentDocument,
10+
ContentDocumentStorage,
11+
ContentHistory,
12+
ContentHistoryType,
13+
ContentKind,
14+
PublishContentData,
15+
Schema,
16+
Space,
17+
UserPermission,
18+
} from './models';
19+
import { extractContent, findContentByFullSlug, findContentById, findContentsHistory, findSchemas, findSpaceById } from './services';
920

1021
// Publish
11-
const contentPublish = onCall<PublishContentData>(async request => {
22+
const publish = onCall<PublishContentData>(async request => {
1223
logger.info('[Content::contentPublish] data: ' + JSON.stringify(request.data));
1324
logger.info('[Content::contentPublish] context.auth: ' + JSON.stringify(request.auth));
14-
if (!canPerform(UserPermission.CONTENT_PUBLISH, request.auth)) throw new HttpsError('permission-denied', 'permission-denied');
15-
const { spaceId, contentId } = request.data;
25+
const { auth, data } = request;
26+
if (!canPerform(UserPermission.CONTENT_PUBLISH, auth)) throw new HttpsError('permission-denied', 'permission-denied');
27+
const { spaceId, contentId } = data;
1628
const spaceSnapshot = await findSpaceById(spaceId).get();
1729
const contentSnapshot = await findContentById(spaceId, contentId).get();
1830
const schemasSnapshot = await findSchemas(spaceId).get();
@@ -44,6 +56,13 @@ const contentPublish = onCall<PublishContentData>(async request => {
4456
await bucket.file(`spaces/${spaceId}/contents/${contentId}/cache.json`).save('');
4557
// Update publishedAt
4658
await contentSnapshot.ref.update({ publishedAt: FieldValue.serverTimestamp() });
59+
const addHistory: WithFieldValue<ContentHistory> = {
60+
type: ContentHistoryType.PUBLISHED,
61+
name: auth?.token['name'] || FieldValue.delete(),
62+
email: auth?.token.email || FieldValue.delete(),
63+
createdAt: FieldValue.serverTimestamp(),
64+
};
65+
await findContentsHistory(spaceId, contentId).add(addHistory);
4766
return;
4867
} else {
4968
logger.info(`[Content::contentPublish] Content ${contentId} does not exist.`);
@@ -129,17 +148,21 @@ const onContentDelete = onDocumentDeleted('spaces/{spaceId}/contents/{contentId}
129148
const { spaceId, contentId } = event.params;
130149
// No Data
131150
if (!event.data) return;
151+
// TODO add 500 LIMIT
152+
const batch = firestoreService.batch();
153+
const contentsHistorySnapshot = await findContentsHistory(spaceId, contentId).get();
154+
contentsHistorySnapshot.docs.forEach(it => batch.delete(it.ref));
132155
const content = event.data.data() as Content;
133156
logger.info(`[Content::onDelete] eventId='${event.id}' id='${event.data.id}' fullSlug='${content.fullSlug}'`);
134157
// Logic related to delete, in case a folder is deleted it should be cascaded to all childs
135158
if (content.kind === ContentKind.DOCUMENT) {
136-
return bucket.deleteFiles({
159+
await bucket.deleteFiles({
137160
prefix: `spaces/${spaceId}/contents/${contentId}`,
138161
});
162+
return batch.commit();
139163
} else if (content.kind === ContentKind.FOLDER) {
140164
// cascade changes to all child's in case it is a FOLDER
141165
// It will create recursion
142-
const batch = firestoreService.batch();
143166
const contentsSnapshot = await findContentByFullSlug(spaceId, content.fullSlug).get();
144167
contentsSnapshot.docs.filter(it => it.exists).forEach(it => batch.delete(it.ref));
145168
return batch.commit();
@@ -150,15 +173,58 @@ const onContentDelete = onDocumentDeleted('spaces/{spaceId}/contents/{contentId}
150173
const onContentWrite = onDocumentWritten('spaces/{spaceId}/contents/{contentId}', async event => {
151174
logger.info(`[Content::onWrite] eventId='${event.id}'`);
152175
logger.info(`[Content::onWrite] params='${JSON.stringify(event.params)}'`);
153-
const { spaceId } = event.params;
176+
const { spaceId, contentId } = event.params;
154177
// Save Cache, to make sure LINKS are cached correctly with cache version
155178
logger.info(`[Content::onWrite] Save file to spaces/${spaceId}/contents/cache.json`);
156179
await bucket.file(`spaces/${spaceId}/contents/cache.json`).save('');
180+
// History
181+
// No Data
182+
if (!event.data) return;
183+
const { before, after } = event.data;
184+
const beforeData = before.data() as Content | undefined;
185+
const afterData = after.data() as Content | undefined;
186+
let addHistory: WithFieldValue<ContentHistory> = {
187+
type: ContentHistoryType.PUBLISHED,
188+
createdAt: FieldValue.serverTimestamp(),
189+
};
190+
if (beforeData && afterData) {
191+
// change
192+
if (beforeData.kind === ContentKind.DOCUMENT && afterData.kind === ContentKind.DOCUMENT) {
193+
if (beforeData.publishedAt?.nanoseconds !== afterData.publishedAt?.nanoseconds) {
194+
// SKIP Publish event
195+
return;
196+
}
197+
}
198+
addHistory = {
199+
type: ContentHistoryType.UPDATE,
200+
createdAt: FieldValue.serverTimestamp(),
201+
};
202+
if (beforeData.name !== afterData.name) {
203+
addHistory.cName = afterData.name;
204+
}
205+
if (beforeData.slug !== afterData.slug) {
206+
addHistory.cSlug = afterData.slug;
207+
}
208+
if (beforeData.kind === ContentKind.DOCUMENT && afterData.kind === ContentKind.DOCUMENT) {
209+
if (JSON.stringify(beforeData.data) !== JSON.stringify(afterData.data)) {
210+
addHistory.cData = true;
211+
}
212+
}
213+
} else if (afterData) {
214+
// create
215+
addHistory = {
216+
type: ContentHistoryType.CREATE,
217+
cName: afterData.name,
218+
cSlug: afterData.slug,
219+
createdAt: FieldValue.serverTimestamp(),
220+
};
221+
}
222+
await findContentsHistory(spaceId, contentId).add(addHistory);
157223
return;
158224
});
159225

160226
export const content = {
161-
publish: contentPublish,
227+
publish: publish,
162228
onupdate: onContentUpdate,
163229
ondelete: onContentDelete,
164230
onwrite: onContentWrite,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Timestamp } from 'firebase-admin/firestore';
2+
3+
export enum ContentHistoryType {
4+
PUBLISHED = 'PUBLISHED',
5+
CREATE = 'CREATE',
6+
UPDATE = 'UPDATE',
7+
DELETE = 'DELETE',
8+
}
9+
10+
export type ContentHistory = ContentHistoryPublish | ContentHistoryCreate | ContentHistoryUpdate | ContentHistoryDelete;
11+
12+
export interface ContentHistoryBase {
13+
type: ContentHistoryType;
14+
createdAt: Timestamp;
15+
}
16+
17+
export interface ContentHistoryPublish extends ContentHistoryBase {
18+
type: ContentHistoryType.PUBLISHED;
19+
name?: string;
20+
email?: string;
21+
}
22+
23+
export interface ContentHistoryCreate extends ContentHistoryBase {
24+
type: ContentHistoryType.CREATE;
25+
cName: string;
26+
cSlug: string;
27+
}
28+
29+
export interface ContentHistoryUpdate extends ContentHistoryBase {
30+
type: ContentHistoryType.UPDATE;
31+
cName?: string;
32+
cSlug?: string;
33+
cData?: boolean;
34+
}
35+
36+
export interface ContentHistoryDelete extends ContentHistoryBase {
37+
type: ContentHistoryType.DELETE;
38+
}

functions/src/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './asset.model';
22
export * from './content.model';
3+
export * from './content-history.model';
34
export * from './firebase.model';
45
export * from './locale.model';
56
export * from './plugin.model';

functions/src/models/translation-history.model.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,30 @@ export enum TranslationHistoryType {
77
DELETE = 'DELETE',
88
}
99

10-
export interface TranslationHistory {
10+
export type TranslationHistory = TranslationHistoryPublish | TranslationHistoryCreate | TranslationHistoryUpdate | TranslationHistoryDelete;
11+
12+
export interface TranslationHistoryBase {
1113
type: TranslationHistoryType;
12-
description?: string;
13-
key?: string;
14+
createdAt: Timestamp;
15+
}
16+
17+
export interface TranslationHistoryPublish extends TranslationHistoryBase {
18+
type: TranslationHistoryType.PUBLISHED;
1419
name?: string;
1520
email?: string;
16-
createdAt: Timestamp;
21+
}
22+
23+
export interface TranslationHistoryCreate extends TranslationHistoryBase {
24+
type: TranslationHistoryType.CREATE;
25+
key: string;
26+
}
27+
28+
export interface TranslationHistoryUpdate extends TranslationHistoryBase {
29+
type: TranslationHistoryType.UPDATE;
30+
key: string;
31+
}
32+
33+
export interface TranslationHistoryDelete extends TranslationHistoryBase {
34+
type: TranslationHistoryType.DELETE;
35+
key: string;
1736
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { firestoreService } from '../config';
2+
import { CollectionReference, DocumentReference } from 'firebase-admin/firestore';
3+
4+
/**
5+
* find Content History by ID
6+
* @param {string} spaceId Space identifier
7+
* @param {string} contentId Content identifier
8+
* @param {string} id Content identifier
9+
* @return {DocumentReference} document reference to the space
10+
*/
11+
export function findContentHistoryById(spaceId: string, contentId: string, id: string): DocumentReference {
12+
return firestoreService.doc(`spaces/${spaceId}/contents/${contentId}/histories/${id}`);
13+
}
14+
15+
/**
16+
* find Contents History
17+
* @param {string} spaceId Space identifier
18+
* @param {string} contentId Content identifier
19+
* @return {DocumentReference} document reference to the space
20+
*/
21+
export function findContentsHistory(spaceId: string, contentId: string): CollectionReference {
22+
return firestoreService.collection(`spaces/${spaceId}/contents/${contentId}/histories`);
23+
}

functions/src/services/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './asset.service';
22
export * from './content.service';
3+
export * from './content-history.service';
34
export * from './plugin.service';
45
export * from './schema.service';
56
export * from './space.service';

functions/src/spaces.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ const onSpaceDelete = onDocumentDeleted('spaces/{spaceId}', event => {
1414
return bucket.deleteFiles({
1515
prefix: `spaces/${spaceId}/`,
1616
});
17+
// TODO delete all sub collections
18+
// assets
19+
// contents
20+
// plugins
21+
// schemas
22+
// tasks
23+
// tokens
24+
// translations
25+
// translations_history
1726
});
1827

1928
const calculateOverview = onCall<SpaceOverviewData>(async request => {

functions/src/translations.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import { PublishTranslationsData, Space, Translation, TranslationHistory, Transl
99
import { findSpaceById, findTranslations, findTranslationsHistory } from './services';
1010

1111
// Publish
12-
const translationsPublish = onCall<PublishTranslationsData>(async request => {
12+
const publish = onCall<PublishTranslationsData>(async request => {
1313
logger.info('[translationsPublish] data: ' + JSON.stringify(request.data));
1414
logger.info('[translationsPublish] context.auth: ' + JSON.stringify(request.auth));
1515
const { auth, data } = request;
16+
if (!canPerform(UserPermission.TRANSLATION_PUBLISH, auth)) throw new HttpsError('permission-denied', 'permission-denied');
1617
const { spaceId } = data;
17-
if (!canPerform(UserPermission.TRANSLATION_PUBLISH, request.auth)) throw new HttpsError('permission-denied', 'permission-denied');
1818
const spaceSnapshot = await findSpaceById(spaceId).get();
1919
const translationsSnapshot = await findTranslations(spaceId).get();
2020
if (spaceSnapshot.exists && !translationsSnapshot.empty) {
@@ -59,7 +59,7 @@ const translationsPublish = onCall<PublishTranslationsData>(async request => {
5959
}
6060
});
6161

62-
const onTranslationCreate = onDocumentCreated('spaces/{spaceId}/translations/{translationId}', async event => {
62+
const onCreate = onDocumentCreated('spaces/{spaceId}/translations/{translationId}', async event => {
6363
logger.info(`[Translation:onCreate] eventId='${event.id}'`);
6464
logger.info(`[Translation:onCreate] params='${JSON.stringify(event.params)}'`);
6565
const { spaceId } = event.params;
@@ -121,7 +121,7 @@ const onTranslationCreate = onDocumentCreated('spaces/{spaceId}/translations/{tr
121121
return;
122122
});
123123

124-
const onTranslationWrite = onDocumentWritten('spaces/{spaceId}/translations/{translationId}', async event => {
124+
const onWriteToHistory = onDocumentWritten('spaces/{spaceId}/translations/{translationId}', async event => {
125125
logger.info(`[Translation:onWrite] eventId='${event.id}'`);
126126
logger.info(`[Translation:onWrite] params='${JSON.stringify(event.params)}'`);
127127
const { spaceId } = event.params;
@@ -131,29 +131,38 @@ const onTranslationWrite = onDocumentWritten('spaces/{spaceId}/translations/{tra
131131
const { before, after } = event.data;
132132
const beforeData = before.data() as Translation | undefined;
133133
const afterData = after.data() as Translation | undefined;
134-
const addHistory: WithFieldValue<TranslationHistory> = {
135-
type: TranslationHistoryType.CREATE,
134+
let addHistory: WithFieldValue<TranslationHistory> = {
135+
type: TranslationHistoryType.PUBLISHED,
136136
createdAt: FieldValue.serverTimestamp(),
137137
};
138138
if (beforeData && afterData) {
139139
// change
140-
addHistory.type = TranslationHistoryType.UPDATE;
141-
addHistory.key = afterData.name;
140+
addHistory = {
141+
type: TranslationHistoryType.UPDATE,
142+
key: afterData.name,
143+
createdAt: FieldValue.serverTimestamp(),
144+
};
142145
} else if (beforeData) {
143146
// delete
144-
addHistory.type = TranslationHistoryType.DELETE;
145-
addHistory.key = beforeData.name;
147+
addHistory = {
148+
type: TranslationHistoryType.DELETE,
149+
key: beforeData.name,
150+
createdAt: FieldValue.serverTimestamp(),
151+
};
146152
} else if (afterData) {
147153
// create
148-
addHistory.type = TranslationHistoryType.CREATE;
149-
addHistory.key = afterData.name;
154+
addHistory = {
155+
type: TranslationHistoryType.CREATE,
156+
key: afterData.name,
157+
createdAt: FieldValue.serverTimestamp(),
158+
};
150159
}
151160
await findTranslationsHistory(spaceId).add(addHistory);
152161
return;
153162
});
154163

155164
export const translation = {
156-
publish: translationsPublish,
157-
oncreate: onTranslationCreate,
158-
onwrite: onTranslationWrite,
165+
publish: publish,
166+
oncreate: onCreate,
167+
onwritetohistory: onWriteToHistory,
159168
};

src/app/features/contents/contents.module.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { TaskService } from '@shared/services/task.service';
2222
import { TokenService } from '@shared/services/token.service';
2323
import { ReferenceSelectComponent } from './shared/reference-select/reference-select.component';
2424
import { ReferencesSelectComponent } from './shared/references-select/references-select.component';
25+
import { ContentHistoryService } from '@shared/services/content-history.service';
2526

2627
@NgModule({
2728
declarations: [
@@ -40,6 +41,15 @@ import { ReferencesSelectComponent } from './shared/references-select/references
4041
ImportDialogComponent,
4142
],
4243
imports: [SharedModule, ContentsRoutingModule],
43-
providers: [SpaceService, SchemaService, ContentService, ContentHelperService, AssetService, TaskService, TokenService],
44+
providers: [
45+
SpaceService,
46+
SchemaService,
47+
ContentService,
48+
ContentHistoryService,
49+
ContentHelperService,
50+
AssetService,
51+
TaskService,
52+
TokenService,
53+
],
4454
})
4555
export class ContentsModule {}

0 commit comments

Comments
 (0)