1
1
import { logger } from 'firebase-functions/v2' ;
2
2
import { HttpsError , onCall } from 'firebase-functions/v2/https' ;
3
3
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' ;
5
5
import { canPerform } from './utils/security-utils' ;
6
6
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' ;
9
20
10
21
// Publish
11
- const contentPublish = onCall < PublishContentData > ( async request => {
22
+ const publish = onCall < PublishContentData > ( async request => {
12
23
logger . info ( '[Content::contentPublish] data: ' + JSON . stringify ( request . data ) ) ;
13
24
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 ;
16
28
const spaceSnapshot = await findSpaceById ( spaceId ) . get ( ) ;
17
29
const contentSnapshot = await findContentById ( spaceId , contentId ) . get ( ) ;
18
30
const schemasSnapshot = await findSchemas ( spaceId ) . get ( ) ;
@@ -44,6 +56,13 @@ const contentPublish = onCall<PublishContentData>(async request => {
44
56
await bucket . file ( `spaces/${ spaceId } /contents/${ contentId } /cache.json` ) . save ( '' ) ;
45
57
// Update publishedAt
46
58
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 ) ;
47
66
return ;
48
67
} else {
49
68
logger . info ( `[Content::contentPublish] Content ${ contentId } does not exist.` ) ;
@@ -129,17 +148,21 @@ const onContentDelete = onDocumentDeleted('spaces/{spaceId}/contents/{contentId}
129
148
const { spaceId, contentId } = event . params ;
130
149
// No Data
131
150
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 ) ) ;
132
155
const content = event . data . data ( ) as Content ;
133
156
logger . info ( `[Content::onDelete] eventId='${ event . id } ' id='${ event . data . id } ' fullSlug='${ content . fullSlug } '` ) ;
134
157
// Logic related to delete, in case a folder is deleted it should be cascaded to all childs
135
158
if ( content . kind === ContentKind . DOCUMENT ) {
136
- return bucket . deleteFiles ( {
159
+ await bucket . deleteFiles ( {
137
160
prefix : `spaces/${ spaceId } /contents/${ contentId } ` ,
138
161
} ) ;
162
+ return batch . commit ( ) ;
139
163
} else if ( content . kind === ContentKind . FOLDER ) {
140
164
// cascade changes to all child's in case it is a FOLDER
141
165
// It will create recursion
142
- const batch = firestoreService . batch ( ) ;
143
166
const contentsSnapshot = await findContentByFullSlug ( spaceId , content . fullSlug ) . get ( ) ;
144
167
contentsSnapshot . docs . filter ( it => it . exists ) . forEach ( it => batch . delete ( it . ref ) ) ;
145
168
return batch . commit ( ) ;
@@ -150,15 +173,58 @@ const onContentDelete = onDocumentDeleted('spaces/{spaceId}/contents/{contentId}
150
173
const onContentWrite = onDocumentWritten ( 'spaces/{spaceId}/contents/{contentId}' , async event => {
151
174
logger . info ( `[Content::onWrite] eventId='${ event . id } '` ) ;
152
175
logger . info ( `[Content::onWrite] params='${ JSON . stringify ( event . params ) } '` ) ;
153
- const { spaceId } = event . params ;
176
+ const { spaceId, contentId } = event . params ;
154
177
// Save Cache, to make sure LINKS are cached correctly with cache version
155
178
logger . info ( `[Content::onWrite] Save file to spaces/${ spaceId } /contents/cache.json` ) ;
156
179
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 ) ;
157
223
return ;
158
224
} ) ;
159
225
160
226
export const content = {
161
- publish : contentPublish ,
227
+ publish : publish ,
162
228
onupdate : onContentUpdate ,
163
229
ondelete : onContentDelete ,
164
230
onwrite : onContentWrite ,
0 commit comments