@blinkk/root-cms 2.4.9 → 2.5.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.
- package/README.md +17 -3
- package/dist/ai-OZY3JXDH.js +19 -0
- package/dist/altText-RDKJNVGH.js +7 -0
- package/dist/app.js +9 -269
- package/dist/chunk-KTUENARU.js +193 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NUUABQRN.js +1944 -0
- package/dist/chunk-RIJF2AHU.js +136 -0
- package/dist/chunk-RNDSZKAW.js +171 -0
- package/dist/chunk-RYF3UTHQ.js +302 -0
- package/dist/chunk-T5UK2H24.js +419 -0
- package/dist/cli.js +54 -362
- package/dist/{client-pSzji9ZN.d.ts → client-ROwBDNeR.d.ts} +39 -2
- package/dist/client.d.ts +2 -1
- package/dist/client.js +15 -1509
- package/dist/core.d.ts +3 -3
- package/dist/core.js +33 -1519
- package/dist/edit-XX3LAGK6.js +7 -0
- package/dist/functions.js +8 -1632
- package/dist/generate-types-TQBCE2SG.js +9 -0
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.js +138 -2519
- package/dist/project.d.ts +1 -1
- package/dist/project.js +6 -76
- package/dist/richtext.js +2 -0
- package/dist/{schema-Bux4PrV2.d.ts → schema-BKfPP_s9.d.ts} +78 -2
- package/dist/ui/ui.css +1 -1
- package/dist/ui/ui.js +234 -154
- package/dist/ui/ui.js.LEGAL.txt +126 -102
- package/package.json +5 -5
package/dist/core.js
CHANGED
|
@@ -1,1519 +1,24 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __export = (target, all) => {
|
|
3
|
-
for (var name in all)
|
|
4
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
// core/client.ts
|
|
8
|
-
import crypto from "crypto";
|
|
9
1
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
BatchRequest,
|
|
3
|
+
BatchResponse,
|
|
4
|
+
RootCMSClient,
|
|
5
|
+
TranslationsManager,
|
|
6
|
+
buildTranslationsDbPath,
|
|
7
|
+
buildTranslationsLocaleDocDbPath,
|
|
8
|
+
getCmsPlugin,
|
|
9
|
+
isRichTextData,
|
|
10
|
+
marshalArray,
|
|
11
|
+
marshalData,
|
|
12
|
+
normalizeData,
|
|
13
|
+
parseDocId,
|
|
14
|
+
toArrayObject,
|
|
15
|
+
translationsForLocale,
|
|
16
|
+
unmarshalArray,
|
|
17
|
+
unmarshalData
|
|
18
|
+
} from "./chunk-NUUABQRN.js";
|
|
15
19
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from "firebase-admin/firestore";
|
|
19
|
-
|
|
20
|
-
// shared/strings.ts
|
|
21
|
-
import fnv from "fnv-plus";
|
|
22
|
-
function normalizeStr(str) {
|
|
23
|
-
const lines = str.trim().split("\n").map((line) => removeTrailingWhitespace(line));
|
|
24
|
-
return lines.join("\n");
|
|
25
|
-
}
|
|
26
|
-
function removeTrailingWhitespace(str) {
|
|
27
|
-
return str.trimEnd().replace(/ $/, "");
|
|
28
|
-
}
|
|
29
|
-
function hashStr(str) {
|
|
30
|
-
if (!str || typeof str !== "string") {
|
|
31
|
-
throw new Error("input string is invalid");
|
|
32
|
-
}
|
|
33
|
-
return fnv.fast1a52hex(normalizeStr(str));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// core/translations-manager.ts
|
|
37
|
-
var TRANSLATIONS_DB_PATH_FORMAT = "/Projects/{project}/TranslationsManager/{mode}/Translations";
|
|
38
|
-
var TRANSLATIONS_LOCALE_DOC_DB_PATH_FORMAT = `${TRANSLATIONS_DB_PATH_FORMAT}/{id}:{locale}`;
|
|
39
|
-
var TranslationsManager = class {
|
|
40
|
-
constructor(cmsClient) {
|
|
41
|
-
this.cmsClient = cmsClient;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Saves draft translations for a translations doc id.
|
|
45
|
-
*
|
|
46
|
-
* Example:
|
|
47
|
-
* ```
|
|
48
|
-
* const strings = {
|
|
49
|
-
* 'one': {es: 'uno', fr: 'un'},
|
|
50
|
-
* 'two': {es: 'dos', fr: 'deux'},
|
|
51
|
-
* };
|
|
52
|
-
* await tm.saveTranslations('Pages/index', strings);
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
async saveTranslations(id, strings, options) {
|
|
56
|
-
const mode = "draft";
|
|
57
|
-
const localesSet = /* @__PURE__ */ new Set();
|
|
58
|
-
Object.values(strings).forEach((entry) => {
|
|
59
|
-
Object.keys(entry).forEach((locale) => {
|
|
60
|
-
if (locale !== "source") {
|
|
61
|
-
localesSet.add(locale);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
const batch = this.cmsClient.db.batch();
|
|
66
|
-
const locales = Array.from(localesSet);
|
|
67
|
-
locales.forEach((locale) => {
|
|
68
|
-
const updates = {
|
|
69
|
-
id,
|
|
70
|
-
locale,
|
|
71
|
-
sys: {
|
|
72
|
-
modifiedAt: Timestamp.now(),
|
|
73
|
-
modifiedBy: options?.modifiedBy || "root-cms-client"
|
|
74
|
-
},
|
|
75
|
-
strings: {}
|
|
76
|
-
};
|
|
77
|
-
if (options?.tags && options.tags.length > 0) {
|
|
78
|
-
updates.tags = FieldValue.arrayUnion(...options.tags);
|
|
79
|
-
}
|
|
80
|
-
let numUpdates = 0;
|
|
81
|
-
const hashMap = this.toLocaleDocHashMap(strings, locale);
|
|
82
|
-
Object.entries(hashMap).forEach(([hash, translations]) => {
|
|
83
|
-
Object.entries(translations).forEach(([locale2, translation]) => {
|
|
84
|
-
if (translation) {
|
|
85
|
-
updates.strings[hash] ??= {};
|
|
86
|
-
updates.strings[hash][locale2] = translation;
|
|
87
|
-
numUpdates += 1;
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
if (numUpdates > 0) {
|
|
92
|
-
const localeDocPath = buildTranslationsLocaleDocDbPath({
|
|
93
|
-
project: this.cmsClient.projectId,
|
|
94
|
-
mode,
|
|
95
|
-
id,
|
|
96
|
-
locale
|
|
97
|
-
});
|
|
98
|
-
const localeDocRef = this.cmsClient.db.doc(localeDocPath);
|
|
99
|
-
batch.set(localeDocRef, updates, { merge: true });
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
await batch.commit();
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Publishes a translations doc.
|
|
106
|
-
*/
|
|
107
|
-
async publishTranslations(id, options) {
|
|
108
|
-
const db = this.cmsClient.db;
|
|
109
|
-
const project = this.cmsClient.projectId;
|
|
110
|
-
const draftPath = buildTranslationsDbPath({ project, mode: "draft" });
|
|
111
|
-
const query = db.collection(draftPath).where("id", "==", id);
|
|
112
|
-
const res = await query.get();
|
|
113
|
-
if (res.size === 0) {
|
|
114
|
-
console.warn(`no translations to publish for ${id}`);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const batch = options?.batch || db.batch();
|
|
118
|
-
res.docs.forEach((doc) => {
|
|
119
|
-
const translationsLocaleDoc = doc.data();
|
|
120
|
-
const sys = {
|
|
121
|
-
...translationsLocaleDoc.sys,
|
|
122
|
-
publishedAt: Timestamp.now(),
|
|
123
|
-
publishedBy: options?.publishedBy || "root-cms-client"
|
|
124
|
-
};
|
|
125
|
-
batch.update(doc.ref, { sys });
|
|
126
|
-
const publishedDocPath = buildTranslationsLocaleDocDbPath({
|
|
127
|
-
project,
|
|
128
|
-
mode: "published",
|
|
129
|
-
id: translationsLocaleDoc.id,
|
|
130
|
-
locale: translationsLocaleDoc.locale
|
|
131
|
-
});
|
|
132
|
-
const publishedDocRef = db.doc(publishedDocPath);
|
|
133
|
-
batch.set(publishedDocRef, { ...translationsLocaleDoc, sys });
|
|
134
|
-
});
|
|
135
|
-
const shouldCommitBatch = !options?.batch;
|
|
136
|
-
if (shouldCommitBatch) {
|
|
137
|
-
await batch.commit();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Fetches translations from one or more translations docs in the translations
|
|
142
|
-
* manager.
|
|
143
|
-
*
|
|
144
|
-
* Example:
|
|
145
|
-
* ```
|
|
146
|
-
* await tm.loadTranslations();
|
|
147
|
-
* // =>
|
|
148
|
-
* // {
|
|
149
|
-
* // "one": {"es": "uno", "fr": "un"},
|
|
150
|
-
* // "two": {"es": "dos", "fr": "deux"}
|
|
151
|
-
* // }
|
|
152
|
-
* ```
|
|
153
|
-
*
|
|
154
|
-
* To load a specific set of translations docs by id:
|
|
155
|
-
* ```
|
|
156
|
-
* const translationsToLoad = ['Global/strings', 'Global/header', 'Global/footer', 'Pages/index'];
|
|
157
|
-
* await tm.loadTranslations({ids: translationsToLoad});
|
|
158
|
-
* // =>
|
|
159
|
-
* // {
|
|
160
|
-
* // "one": {"es": "uno", "fr": "un"},
|
|
161
|
-
* // "two": {"es": "dos", "fr": "deux"}
|
|
162
|
-
* // }
|
|
163
|
-
* ```
|
|
164
|
-
*
|
|
165
|
-
* To load a subset of locales (more performant):
|
|
166
|
-
* ```
|
|
167
|
-
* await tm.loadTranslations({locales: ['es']});
|
|
168
|
-
* // =>
|
|
169
|
-
* // {
|
|
170
|
-
* // "one": {"es": "uno"},
|
|
171
|
-
* // "two": {"es": "dos"}
|
|
172
|
-
* // }
|
|
173
|
-
* ```
|
|
174
|
-
*/
|
|
175
|
-
async loadTranslations(options) {
|
|
176
|
-
const mode = options?.mode || "published";
|
|
177
|
-
const dbPath = buildTranslationsDbPath({
|
|
178
|
-
project: this.cmsClient.projectId,
|
|
179
|
-
mode
|
|
180
|
-
});
|
|
181
|
-
let query = this.cmsClient.db.collection(dbPath);
|
|
182
|
-
if (options?.ids && options.ids.length > 0) {
|
|
183
|
-
query = query.where("id", "in", options.ids);
|
|
184
|
-
}
|
|
185
|
-
if (options?.tags && options.tags.length > 0) {
|
|
186
|
-
query = query.where("tags", "array-contains", options.tags);
|
|
187
|
-
}
|
|
188
|
-
if (options?.locales && options.locales.length > 0) {
|
|
189
|
-
query = query.where("locale", "in", options.locales);
|
|
190
|
-
}
|
|
191
|
-
const results = await query.get();
|
|
192
|
-
const strings = {};
|
|
193
|
-
results.forEach((result) => {
|
|
194
|
-
const localeDoc = result.data();
|
|
195
|
-
Object.values(localeDoc.strings || {}).forEach((item) => {
|
|
196
|
-
strings[item.source] ??= { source: item.source };
|
|
197
|
-
strings[item.source][localeDoc.locale] = item.translation;
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
return strings;
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Fetches translations for a given locale, with optional fallbacks.
|
|
204
|
-
* The return value is a map of source string to translated string.
|
|
205
|
-
*
|
|
206
|
-
* Example:
|
|
207
|
-
* ```
|
|
208
|
-
* await translationsDoc.loadTranslationsForLocale('es');
|
|
209
|
-
* // =>
|
|
210
|
-
* // {
|
|
211
|
-
* // "one": "uno",
|
|
212
|
-
* // "two": "dos",
|
|
213
|
-
* // }
|
|
214
|
-
* ```
|
|
215
|
-
*/
|
|
216
|
-
async loadTranslationsForLocale(locale, options) {
|
|
217
|
-
const localeSet = /* @__PURE__ */ new Set([
|
|
218
|
-
locale,
|
|
219
|
-
...options?.fallbackLocales || []
|
|
220
|
-
]);
|
|
221
|
-
const fallbackLocales = Array.from(localeSet);
|
|
222
|
-
const multiLocaleStrings = await this.loadTranslations({
|
|
223
|
-
mode: options?.mode,
|
|
224
|
-
locales: fallbackLocales
|
|
225
|
-
});
|
|
226
|
-
return this.toSingleLocaleMap(multiLocaleStrings, fallbackLocales);
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Converts a multi-locale translations map to a flat single-locale map,
|
|
230
|
-
* with optional support for fallback locales.
|
|
231
|
-
*
|
|
232
|
-
* ```
|
|
233
|
-
* const multiLocaleStrings = {
|
|
234
|
-
* 'one': {es: 'uno', fr: 'un'},
|
|
235
|
-
* 'two': {es: 'dos', fr: 'deux'}
|
|
236
|
-
* };
|
|
237
|
-
* translationsDoc.toSingleLocaleMap(multiLocaleStrings, ['es']);
|
|
238
|
-
* // =>
|
|
239
|
-
* // {
|
|
240
|
-
* // "one": "uno",
|
|
241
|
-
* // "two": "dos",
|
|
242
|
-
* // }
|
|
243
|
-
* ```
|
|
244
|
-
*/
|
|
245
|
-
toSingleLocaleMap(multiLocaleStrings, fallbackLocales) {
|
|
246
|
-
const singleLocaleStrings = {};
|
|
247
|
-
Object.entries(multiLocaleStrings).forEach(([source, translations]) => {
|
|
248
|
-
let translation = source;
|
|
249
|
-
for (const locale of fallbackLocales) {
|
|
250
|
-
if (translations[locale]) {
|
|
251
|
-
translation = translations[locale];
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
singleLocaleStrings[source] = translation;
|
|
256
|
-
});
|
|
257
|
-
return singleLocaleStrings;
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Converts a multi-locale translations map to a single-locale hashed version,
|
|
261
|
-
* used for storage in in the DB.
|
|
262
|
-
*
|
|
263
|
-
* ```
|
|
264
|
-
* const multiLocaleStrings = {
|
|
265
|
-
* 'one': {es: 'uno', fr: 'un'},
|
|
266
|
-
* 'two': {es: 'dos', fr: 'deux'}
|
|
267
|
-
* };
|
|
268
|
-
* translationsDoc.toLocaleDocHashMap(multiLocaleStrings, 'es');
|
|
269
|
-
* // =>
|
|
270
|
-
* // {
|
|
271
|
-
* // "<hash1>": {"source": "one", "translation": "uno"},
|
|
272
|
-
* // "<hash2>": {"source": "two", "translation": "dos"},
|
|
273
|
-
* // }
|
|
274
|
-
* ```
|
|
275
|
-
*
|
|
276
|
-
* One reason for using hashes is because the DB has limits on the number of
|
|
277
|
-
* chars that can be used as the "key" in a object map.
|
|
278
|
-
*/
|
|
279
|
-
toLocaleDocHashMap(multiLocaleStrings, locale) {
|
|
280
|
-
const hashMap = {};
|
|
281
|
-
Object.entries(multiLocaleStrings).forEach(([source, translations]) => {
|
|
282
|
-
const translation = translations[locale];
|
|
283
|
-
if (translation) {
|
|
284
|
-
const hash = hashStr(source);
|
|
285
|
-
hashMap[hash] = { source, translation };
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
return hashMap;
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Import translations from the v1 system to the TranslationsManager.
|
|
292
|
-
*/
|
|
293
|
-
async importTranslationsFromV1() {
|
|
294
|
-
const projectId = this.cmsClient.projectId;
|
|
295
|
-
const db = this.cmsClient.db;
|
|
296
|
-
const dbPath = `Projects/${projectId}/Translations`;
|
|
297
|
-
const query = db.collection(dbPath);
|
|
298
|
-
const querySnapshot = await query.get();
|
|
299
|
-
if (querySnapshot.size === 0) {
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
console.log(
|
|
303
|
-
"[root cms] importing v1 Translations to v2 TranslationsManager"
|
|
304
|
-
);
|
|
305
|
-
const translationsDocs = {};
|
|
306
|
-
querySnapshot.forEach((doc) => {
|
|
307
|
-
const translation = doc.data();
|
|
308
|
-
const source = this.cmsClient.normalizeString(translation.source);
|
|
309
|
-
delete translation.source;
|
|
310
|
-
const tags = translation.tags || [];
|
|
311
|
-
delete translation.tags;
|
|
312
|
-
for (const tag of tags) {
|
|
313
|
-
if (tag.includes("/")) {
|
|
314
|
-
const translationsId = tag;
|
|
315
|
-
translationsDocs[translationsId] ??= {
|
|
316
|
-
id: translationsId,
|
|
317
|
-
tags,
|
|
318
|
-
strings: {}
|
|
319
|
-
};
|
|
320
|
-
translationsDocs[translationsId].strings[source] = translation;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
if (Object.keys(translationsDocs).length === 0) {
|
|
325
|
-
console.log("[root cms] no v1 translations to save");
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
for (const docId in translationsDocs) {
|
|
329
|
-
const [collection2, slug] = docId.split("/");
|
|
330
|
-
if (collection2 && slug) {
|
|
331
|
-
const doc = await this.cmsClient.getDoc(collection2, slug, {
|
|
332
|
-
mode: "draft"
|
|
333
|
-
});
|
|
334
|
-
const linkedSheet = doc?.sys?.l10nSheet;
|
|
335
|
-
if (linkedSheet) {
|
|
336
|
-
translationsDocs[docId].sys.linkedSheet = linkedSheet;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
Object.entries(translationsDocs).forEach(([translationsId, data]) => {
|
|
341
|
-
const len = Object.keys(data.strings).length;
|
|
342
|
-
console.log(`[root cms] saving ${len} string(s) to ${translationsId}...`);
|
|
343
|
-
this.saveTranslations(translationsId, data.strings, {
|
|
344
|
-
tags: data.tags || [translationsId]
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
function buildTranslationsDbPath(options) {
|
|
350
|
-
return TRANSLATIONS_DB_PATH_FORMAT.replace(
|
|
351
|
-
"{project}",
|
|
352
|
-
options.project
|
|
353
|
-
).replace("{mode}", options.mode);
|
|
354
|
-
}
|
|
355
|
-
function buildTranslationsLocaleDocDbPath(options) {
|
|
356
|
-
return TRANSLATIONS_LOCALE_DOC_DB_PATH_FORMAT.replace(
|
|
357
|
-
"{project}",
|
|
358
|
-
options.project
|
|
359
|
-
).replace("{mode}", options.mode).replace("{id}", options.id.replaceAll("/", "--")).replace("{locale}", options.locale);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// core/client.ts
|
|
363
|
-
var RootCMSClient = class {
|
|
364
|
-
constructor(rootConfig) {
|
|
365
|
-
this.rootConfig = rootConfig;
|
|
366
|
-
this.cmsPlugin = getCmsPlugin(this.rootConfig);
|
|
367
|
-
const cmsPluginOptions = this.cmsPlugin.getConfig();
|
|
368
|
-
this.projectId = cmsPluginOptions.id || "default";
|
|
369
|
-
this.app = this.cmsPlugin.getFirebaseApp();
|
|
370
|
-
this.db = this.cmsPlugin.getFirestore();
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Retrieves doc data from Root.js CMS.
|
|
374
|
-
*/
|
|
375
|
-
async getDoc(collectionId, slug, options) {
|
|
376
|
-
const rawData = await this.getRawDoc(collectionId, slug, options);
|
|
377
|
-
if (rawData) {
|
|
378
|
-
return unmarshalData(rawData);
|
|
379
|
-
}
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Retrieves raw doc data as stored in the database. Only use this if you know
|
|
384
|
-
* what you are doing.
|
|
385
|
-
*/
|
|
386
|
-
async getRawDoc(collectionId, slug, options) {
|
|
387
|
-
const mode = options.mode;
|
|
388
|
-
const modeCollection = mode === "draft" ? "Drafts" : "Published";
|
|
389
|
-
slug = slug.replaceAll("/", "--");
|
|
390
|
-
const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}/${slug}`;
|
|
391
|
-
const docRef = this.db.doc(dbPath);
|
|
392
|
-
const doc = await docRef.get();
|
|
393
|
-
if (doc.exists) {
|
|
394
|
-
return doc.data();
|
|
395
|
-
}
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Firestore path for a collection.
|
|
400
|
-
*/
|
|
401
|
-
dbCollectionDocsPath(collectionId, options) {
|
|
402
|
-
let modeCollection = "";
|
|
403
|
-
if (options.mode === "draft") {
|
|
404
|
-
modeCollection = "Drafts";
|
|
405
|
-
} else if (options.mode === "published") {
|
|
406
|
-
modeCollection = "Published";
|
|
407
|
-
} else {
|
|
408
|
-
throw new Error(`unknown mode: ${options.mode}`);
|
|
409
|
-
}
|
|
410
|
-
return `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}`;
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Firestore path for a content doc.
|
|
414
|
-
*/
|
|
415
|
-
dbDocPath(collectionId, slug, options) {
|
|
416
|
-
const collectionDocsPath = this.dbCollectionDocsPath(collectionId, options);
|
|
417
|
-
const normalizedSlug = slug.replaceAll("/", "--");
|
|
418
|
-
return `${collectionDocsPath}/${normalizedSlug}`;
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Firestore doc ref for a content doc.
|
|
422
|
-
*/
|
|
423
|
-
dbDocRef(collectionId, slug, options) {
|
|
424
|
-
const docPath = this.dbDocPath(collectionId, slug, options);
|
|
425
|
-
return this.db.doc(docPath);
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Saves draft data to a doc.
|
|
429
|
-
*
|
|
430
|
-
* Note: this saves data to the "fields" attr of the draft doc. If you need to
|
|
431
|
-
* modify the sys-level attributes of the doc, use `setRawDoc()`.
|
|
432
|
-
*/
|
|
433
|
-
async saveDraftData(docId, fieldsData, options) {
|
|
434
|
-
const { collection: collection2, slug } = parseDocId(docId);
|
|
435
|
-
const draftDoc = await this.getRawDoc(collection2, slug, { mode: "draft" }) || {};
|
|
436
|
-
const draftSys = draftDoc.sys || {};
|
|
437
|
-
const modifiedBy = options?.modifiedBy || "root-cms-client";
|
|
438
|
-
const fields = marshalData(fieldsData || {});
|
|
439
|
-
const data = {
|
|
440
|
-
id: docId,
|
|
441
|
-
collection: collection2,
|
|
442
|
-
slug,
|
|
443
|
-
sys: {
|
|
444
|
-
...draftSys,
|
|
445
|
-
createdAt: draftSys.createdAt ?? Timestamp2.now(),
|
|
446
|
-
createdBy: draftSys.createdBy ?? modifiedBy,
|
|
447
|
-
modifiedAt: Timestamp2.now(),
|
|
448
|
-
modifiedBy,
|
|
449
|
-
locales: options?.locales ?? draftSys.locales ?? ["en"]
|
|
450
|
-
},
|
|
451
|
-
fields
|
|
452
|
-
};
|
|
453
|
-
await this.setRawDoc(collection2, slug, data, { mode: "draft" });
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Prefer `saveDraftData('Pages/foo', data)`. Only use this if you know what
|
|
457
|
-
* you're doing.
|
|
458
|
-
*/
|
|
459
|
-
async setRawDoc(collectionId, slug, data, options) {
|
|
460
|
-
const mode = options.mode;
|
|
461
|
-
const modeCollection = mode === "draft" ? "Drafts" : "Published";
|
|
462
|
-
const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}/${slug}`;
|
|
463
|
-
const docRef = this.db.doc(dbPath);
|
|
464
|
-
await docRef.set(data);
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Lists docs from a Root.js CMS collection.
|
|
468
|
-
*/
|
|
469
|
-
async listDocs(collectionId, options) {
|
|
470
|
-
const mode = options.mode;
|
|
471
|
-
const modeCollection = mode === "draft" ? "Drafts" : "Published";
|
|
472
|
-
const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}`;
|
|
473
|
-
let query = this.db.collection(dbPath);
|
|
474
|
-
if (options.limit) {
|
|
475
|
-
query = query.limit(options.limit);
|
|
476
|
-
}
|
|
477
|
-
if (options.offset) {
|
|
478
|
-
query = query.offset(options.offset);
|
|
479
|
-
}
|
|
480
|
-
if (options.orderBy) {
|
|
481
|
-
query = query.orderBy(options.orderBy, options.orderByDirection);
|
|
482
|
-
}
|
|
483
|
-
if (options.query) {
|
|
484
|
-
query = options.query(query);
|
|
485
|
-
}
|
|
486
|
-
const results = await query.get();
|
|
487
|
-
const docs = [];
|
|
488
|
-
results.forEach((result) => {
|
|
489
|
-
if (options.raw) {
|
|
490
|
-
const rawDoc = result.data();
|
|
491
|
-
docs.push(rawDoc);
|
|
492
|
-
} else {
|
|
493
|
-
const doc = unmarshalData(result.data());
|
|
494
|
-
docs.push(doc);
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
return { docs };
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* Returns the number of docs in a Root.js CMS collection.
|
|
501
|
-
*/
|
|
502
|
-
async getDocsCount(collectionId, options) {
|
|
503
|
-
const mode = options.mode;
|
|
504
|
-
const modeCollection = mode === "draft" ? "Drafts" : "Published";
|
|
505
|
-
const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}`;
|
|
506
|
-
let query = this.db.collection(dbPath);
|
|
507
|
-
if (options.query) {
|
|
508
|
-
query = options.query(query);
|
|
509
|
-
}
|
|
510
|
-
const results = await query.count().get();
|
|
511
|
-
const count = results.data().count;
|
|
512
|
-
return count;
|
|
513
|
-
}
|
|
514
|
-
/**
|
|
515
|
-
* Batch publishes a set of docs by id.
|
|
516
|
-
*/
|
|
517
|
-
async publishDocs(docIds, options) {
|
|
518
|
-
const projectCollectionsPath = `Projects/${this.projectId}/Collections`;
|
|
519
|
-
const publishedBy = options?.publishedBy || "root-cms-client";
|
|
520
|
-
const docRefs = docIds.map((docId) => {
|
|
521
|
-
const [collection2, slug] = docId.split("/");
|
|
522
|
-
if (!collection2 || !slug) {
|
|
523
|
-
throw new Error(`invalid doc id: ${docId}`);
|
|
524
|
-
}
|
|
525
|
-
const docRef = this.db.doc(
|
|
526
|
-
`${projectCollectionsPath}/${collection2}/Drafts/${slug}`
|
|
527
|
-
);
|
|
528
|
-
return docRef;
|
|
529
|
-
});
|
|
530
|
-
const docSnapshots = await this.db.getAll(...docRefs);
|
|
531
|
-
const docs = docSnapshots.map((snapshot) => snapshot.data()).filter((d) => !!d);
|
|
532
|
-
if (docs.length === 0) {
|
|
533
|
-
console.log("no docs to publish");
|
|
534
|
-
return [];
|
|
535
|
-
}
|
|
536
|
-
for (const doc of docs) {
|
|
537
|
-
if (this.testPublishingLocked(doc)) {
|
|
538
|
-
throw new Error(`publishing is locked for doc: ${doc.id}`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
let batchCount = 0;
|
|
542
|
-
const batch = options?.batch || this.db.batch();
|
|
543
|
-
const versionTags = ["published"];
|
|
544
|
-
if (options?.releaseId) {
|
|
545
|
-
versionTags.push(`release:${options.releaseId}`);
|
|
546
|
-
}
|
|
547
|
-
const publishedDocs = [];
|
|
548
|
-
for (const doc of docs) {
|
|
549
|
-
const { id, collection: collection2, slug, sys, fields } = doc;
|
|
550
|
-
const draftRef = this.db.doc(
|
|
551
|
-
`${projectCollectionsPath}/${collection2}/Drafts/${slug}`
|
|
552
|
-
);
|
|
553
|
-
const scheduledRef = this.db.doc(
|
|
554
|
-
`${projectCollectionsPath}/${collection2}/Scheduled/${slug}`
|
|
555
|
-
);
|
|
556
|
-
const publishedRef = this.db.doc(
|
|
557
|
-
`${projectCollectionsPath}/${collection2}/Published/${slug}`
|
|
558
|
-
);
|
|
559
|
-
const firstPublishedAt = sys.firstPublishedAt ?? FieldValue2.serverTimestamp();
|
|
560
|
-
const firstPublishedBy = sys.firstPublishedBy ?? publishedBy;
|
|
561
|
-
batch.set(publishedRef, {
|
|
562
|
-
id,
|
|
563
|
-
collection: collection2,
|
|
564
|
-
slug,
|
|
565
|
-
fields: fields || {},
|
|
566
|
-
sys: {
|
|
567
|
-
...sys,
|
|
568
|
-
firstPublishedAt,
|
|
569
|
-
firstPublishedBy,
|
|
570
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
571
|
-
publishedBy
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
batchCount += 1;
|
|
575
|
-
const versionRef = this.db.doc(
|
|
576
|
-
`${projectCollectionsPath}/${collection2}/Drafts/${slug}/Versions/${Date.now()}`
|
|
577
|
-
);
|
|
578
|
-
const versionData = {
|
|
579
|
-
id,
|
|
580
|
-
collection: collection2,
|
|
581
|
-
slug,
|
|
582
|
-
fields: fields || {},
|
|
583
|
-
sys: {
|
|
584
|
-
...sys,
|
|
585
|
-
firstPublishedAt,
|
|
586
|
-
firstPublishedBy,
|
|
587
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
588
|
-
publishedBy
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
if (versionTags.length) {
|
|
592
|
-
versionData.tags = versionTags;
|
|
593
|
-
}
|
|
594
|
-
batch.set(versionRef, versionData);
|
|
595
|
-
batchCount += 1;
|
|
596
|
-
batch.delete(scheduledRef);
|
|
597
|
-
batchCount += 1;
|
|
598
|
-
batch.update(draftRef, {
|
|
599
|
-
"sys.scheduledAt": FieldValue2.delete(),
|
|
600
|
-
"sys.scheduledBy": FieldValue2.delete(),
|
|
601
|
-
"sys.firstPublishedAt": firstPublishedAt,
|
|
602
|
-
"sys.firstPublishedBy": firstPublishedBy,
|
|
603
|
-
"sys.publishedAt": FieldValue2.serverTimestamp(),
|
|
604
|
-
"sys.publishedBy": publishedBy
|
|
605
|
-
});
|
|
606
|
-
batchCount += 1;
|
|
607
|
-
publishedDocs.push(doc);
|
|
608
|
-
if (batchCount >= 400) {
|
|
609
|
-
await batch.commit();
|
|
610
|
-
batchCount = 0;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
if (batchCount > 0) {
|
|
614
|
-
await batch.commit();
|
|
615
|
-
}
|
|
616
|
-
console.log(`published ${publishedDocs.length} docs!`);
|
|
617
|
-
return publishedDocs;
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Publishes scheduled docs.
|
|
621
|
-
*/
|
|
622
|
-
async publishScheduledDocs() {
|
|
623
|
-
const projectCollectionsPath = `Projects/${this.projectId}/Collections`;
|
|
624
|
-
const now = Math.ceil((/* @__PURE__ */ new Date()).getTime());
|
|
625
|
-
const snapshot = await this.db.collectionGroup("Scheduled").get();
|
|
626
|
-
const docs = snapshot.docs.filter((d) => {
|
|
627
|
-
if (!d.ref.path.startsWith(projectCollectionsPath)) {
|
|
628
|
-
return false;
|
|
629
|
-
}
|
|
630
|
-
const data = d.data() || {};
|
|
631
|
-
const scheduledAt = data.sys?.scheduledAt;
|
|
632
|
-
return scheduledAt && scheduledAt.toMillis && scheduledAt.toMillis() <= now;
|
|
633
|
-
}).map((d) => {
|
|
634
|
-
const dbPath = d.ref.path;
|
|
635
|
-
const segments = dbPath.split("/");
|
|
636
|
-
const slug = segments.at(-1);
|
|
637
|
-
const collection2 = segments.at(-3);
|
|
638
|
-
const id = `${collection2}/${slug}`;
|
|
639
|
-
return {
|
|
640
|
-
data: d.data(),
|
|
641
|
-
id,
|
|
642
|
-
collection: collection2,
|
|
643
|
-
slug
|
|
644
|
-
};
|
|
645
|
-
});
|
|
646
|
-
if (docs.length === 0) {
|
|
647
|
-
console.log("no docs to schedule");
|
|
648
|
-
return [];
|
|
649
|
-
}
|
|
650
|
-
let batchCount = 0;
|
|
651
|
-
const batch = this.db.batch();
|
|
652
|
-
const versionTags = ["published"];
|
|
653
|
-
const publishedDocs = [];
|
|
654
|
-
for (const doc of docs) {
|
|
655
|
-
const { id, collection: collection2, slug, data } = doc;
|
|
656
|
-
const draftRef = this.db.doc(
|
|
657
|
-
`${projectCollectionsPath}/${collection2}/Drafts/${slug}`
|
|
658
|
-
);
|
|
659
|
-
const scheduledRef = this.db.doc(
|
|
660
|
-
`${projectCollectionsPath}/${collection2}/Scheduled/${slug}`
|
|
661
|
-
);
|
|
662
|
-
const publishedRef = this.db.doc(
|
|
663
|
-
`${projectCollectionsPath}/${collection2}/Published/${slug}`
|
|
664
|
-
);
|
|
665
|
-
const { scheduledAt, scheduledBy, ...sys } = data.sys || {};
|
|
666
|
-
const firstPublishedAt = sys.firstPublishedAt ?? scheduledAt;
|
|
667
|
-
const firstPublishedBy = sys.firstPublishedBy ?? (scheduledBy || "root-cms-client");
|
|
668
|
-
batch.set(publishedRef, {
|
|
669
|
-
id,
|
|
670
|
-
collection: collection2,
|
|
671
|
-
slug,
|
|
672
|
-
fields: data.fields || {},
|
|
673
|
-
sys: {
|
|
674
|
-
...sys,
|
|
675
|
-
firstPublishedAt,
|
|
676
|
-
firstPublishedBy,
|
|
677
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
678
|
-
publishedBy: scheduledBy || ""
|
|
679
|
-
}
|
|
680
|
-
});
|
|
681
|
-
batchCount += 1;
|
|
682
|
-
const versionRef = this.db.doc(
|
|
683
|
-
`${projectCollectionsPath}/${collection2}/Drafts/${slug}/Versions/${Date.now()}`
|
|
684
|
-
);
|
|
685
|
-
const versionData = {
|
|
686
|
-
id,
|
|
687
|
-
collection: collection2,
|
|
688
|
-
slug,
|
|
689
|
-
fields: data.fields || {},
|
|
690
|
-
sys: {
|
|
691
|
-
...sys,
|
|
692
|
-
firstPublishedAt,
|
|
693
|
-
firstPublishedBy,
|
|
694
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
695
|
-
publishedBy: scheduledBy || ""
|
|
696
|
-
},
|
|
697
|
-
tags: versionTags
|
|
698
|
-
};
|
|
699
|
-
batch.set(versionRef, versionData);
|
|
700
|
-
batchCount += 1;
|
|
701
|
-
batch.delete(scheduledRef);
|
|
702
|
-
batchCount += 1;
|
|
703
|
-
batch.update(draftRef, {
|
|
704
|
-
"sys.scheduledAt": FieldValue2.delete(),
|
|
705
|
-
"sys.scheduledBy": FieldValue2.delete(),
|
|
706
|
-
"sys.firstPublishedAt": firstPublishedAt,
|
|
707
|
-
"sys.firstPublishedBy": firstPublishedBy,
|
|
708
|
-
"sys.publishedAt": FieldValue2.serverTimestamp(),
|
|
709
|
-
"sys.publishedBy": scheduledBy || "root-cms-client"
|
|
710
|
-
});
|
|
711
|
-
batchCount += 1;
|
|
712
|
-
publishedDocs.push(doc);
|
|
713
|
-
if (batchCount >= 400) {
|
|
714
|
-
await batch.commit();
|
|
715
|
-
batchCount = 0;
|
|
716
|
-
continue;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
if (batchCount > 0) {
|
|
720
|
-
await batch.commit();
|
|
721
|
-
}
|
|
722
|
-
console.log(`published ${publishedDocs.length} docs!`);
|
|
723
|
-
return publishedDocs;
|
|
724
|
-
}
|
|
725
|
-
/**
|
|
726
|
-
* Publishes docs in scheduled releases.
|
|
727
|
-
*/
|
|
728
|
-
async publishScheduledReleases() {
|
|
729
|
-
const releasesPath = `Projects/${this.projectId}/Releases`;
|
|
730
|
-
const now = Math.ceil((/* @__PURE__ */ new Date()).getTime());
|
|
731
|
-
const query = this.db.collection(releasesPath).where("scheduledAt", "<=", Timestamp2.fromMillis(now));
|
|
732
|
-
const querySnapshot = await query.get();
|
|
733
|
-
for (const snapshot of querySnapshot.docs) {
|
|
734
|
-
const release = snapshot.data();
|
|
735
|
-
const batch = this.db.batch();
|
|
736
|
-
const publishedBy = release.scheduledBy || "root-cms-client";
|
|
737
|
-
batch.update(snapshot.ref, {
|
|
738
|
-
publishedAt: Timestamp2.now(),
|
|
739
|
-
publishedBy,
|
|
740
|
-
scheduledAt: FieldValue2.delete(),
|
|
741
|
-
scheduledBy: FieldValue2.delete()
|
|
742
|
-
});
|
|
743
|
-
const dataSourceIds = release.dataSourceIds || [];
|
|
744
|
-
if (dataSourceIds.length > 0) {
|
|
745
|
-
await this.publishDataSources(dataSourceIds, {
|
|
746
|
-
publishedBy,
|
|
747
|
-
batch,
|
|
748
|
-
commitBatch: false
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
await this.publishDocs(release.docIds || [], {
|
|
752
|
-
publishedBy,
|
|
753
|
-
batch,
|
|
754
|
-
releaseId: release.id
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* Checks if a doc is currently "locked" for publishing.
|
|
760
|
-
*/
|
|
761
|
-
testPublishingLocked(doc) {
|
|
762
|
-
if (doc.sys?.publishingLocked) {
|
|
763
|
-
if (doc.sys.publishingLocked.until) {
|
|
764
|
-
const now = Timestamp2.now().toMillis();
|
|
765
|
-
const until = doc.sys.publishingLocked.until.toMillis();
|
|
766
|
-
return now < until;
|
|
767
|
-
}
|
|
768
|
-
return true;
|
|
769
|
-
}
|
|
770
|
-
return false;
|
|
771
|
-
}
|
|
772
|
-
/**
|
|
773
|
-
* Returns a `TranslationsManager` object for managing translations.
|
|
774
|
-
*
|
|
775
|
-
* To get translations:
|
|
776
|
-
* ```
|
|
777
|
-
* await tm.loadTranslations({
|
|
778
|
-
* ids: ['Global/strings', 'Pages/index'],
|
|
779
|
-
* locales: ['es'],
|
|
780
|
-
* });
|
|
781
|
-
* ```
|
|
782
|
-
*
|
|
783
|
-
* NOTE: The `TranslationsManager` is a v2 feature that will eventually
|
|
784
|
-
* replace the v1 translations system.
|
|
785
|
-
*/
|
|
786
|
-
getTranslationsManager() {
|
|
787
|
-
const cmsPluginOptions = this.cmsPlugin.getConfig();
|
|
788
|
-
if (cmsPluginOptions.experiments?.v2TranslationsManager) {
|
|
789
|
-
throw new Error(
|
|
790
|
-
"`v2TranslationsManager` is not enabled. update root.config.ts and add: `{experiments: {v2TranslationsManager: true}}`"
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
return new TranslationsManager(this);
|
|
794
|
-
}
|
|
795
|
-
/**
|
|
796
|
-
* Loads translations saved in the translations collection, optionally
|
|
797
|
-
* filtered by tag.
|
|
798
|
-
*
|
|
799
|
-
* Returns a map like:
|
|
800
|
-
* ```
|
|
801
|
-
* {
|
|
802
|
-
* "<hash>": {"source": "Hello", "es": "Hola", "fr": "Bonjour"},
|
|
803
|
-
* }
|
|
804
|
-
* ```
|
|
805
|
-
*/
|
|
806
|
-
async loadTranslations(options) {
|
|
807
|
-
const dbPath = `Projects/${this.projectId}/Translations`;
|
|
808
|
-
let query = this.db.collection(dbPath);
|
|
809
|
-
if (options?.tags) {
|
|
810
|
-
query = query.where("tags", "array-contains-any", options.tags);
|
|
811
|
-
}
|
|
812
|
-
const querySnapshot = await query.get();
|
|
813
|
-
const translationsMap = {};
|
|
814
|
-
querySnapshot.forEach((doc) => {
|
|
815
|
-
const hash = doc.id;
|
|
816
|
-
translationsMap[hash] = doc.data();
|
|
817
|
-
});
|
|
818
|
-
return translationsMap;
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* Saves a map of translations, e.g.:
|
|
822
|
-
* ```
|
|
823
|
-
* await client.saveTranslations({
|
|
824
|
-
* "Hello": {"es": "Hola", "fr": "Bonjour"},
|
|
825
|
-
* });
|
|
826
|
-
* ```
|
|
827
|
-
*/
|
|
828
|
-
async saveTranslations(translations, tags) {
|
|
829
|
-
const translationsPath = `Projects/${this.projectId}/Translations`;
|
|
830
|
-
const batch = this.db.batch();
|
|
831
|
-
let batchCount = 0;
|
|
832
|
-
Object.entries(translations).forEach(([source, sourceTranslations]) => {
|
|
833
|
-
const hash = this.getTranslationKey(source);
|
|
834
|
-
const translationRef = this.db.doc(`${translationsPath}/${hash}`);
|
|
835
|
-
const data = {
|
|
836
|
-
...sourceTranslations,
|
|
837
|
-
source: this.normalizeString(source)
|
|
838
|
-
};
|
|
839
|
-
if (tags) {
|
|
840
|
-
data.tags = FieldValue2.arrayUnion(...tags);
|
|
841
|
-
}
|
|
842
|
-
batch.set(translationRef, data, { merge: true });
|
|
843
|
-
batchCount += 1;
|
|
844
|
-
});
|
|
845
|
-
if (batchCount > 500) {
|
|
846
|
-
throw new Error("up to 500 translations can be saved at a time.");
|
|
847
|
-
}
|
|
848
|
-
await batch.commit();
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* Returns the "key" used for a translation as stored in the db. Translations
|
|
852
|
-
* are stored under `Projects/<project id>/Translations/<sha1 hash>`.
|
|
853
|
-
*/
|
|
854
|
-
getTranslationKey(source) {
|
|
855
|
-
const sha1 = crypto.createHash("sha1").update(this.normalizeString(source)).digest("hex");
|
|
856
|
-
return sha1;
|
|
857
|
-
}
|
|
858
|
-
/**
|
|
859
|
-
* Cleans a string that's used for translations. Performs the following:
|
|
860
|
-
* - Removes any leading/trailing whitespace
|
|
861
|
-
* - Removes spaces at the end of any line
|
|
862
|
-
*/
|
|
863
|
-
normalizeString(str) {
|
|
864
|
-
const lines = String(str).trim().split("\n").map((line) => line.trimEnd());
|
|
865
|
-
return lines.join("\n");
|
|
866
|
-
}
|
|
867
|
-
/**
|
|
868
|
-
* Loads translations for a particular locale.
|
|
869
|
-
*
|
|
870
|
-
* Returns a map like:
|
|
871
|
-
* ```
|
|
872
|
-
* {
|
|
873
|
-
* "Hello": "Bonjour",
|
|
874
|
-
* }
|
|
875
|
-
* ```
|
|
876
|
-
*/
|
|
877
|
-
async loadTranslationsForLocale(locale, options) {
|
|
878
|
-
const translationsMap = await this.loadTranslations(options);
|
|
879
|
-
return translationsForLocale(translationsMap, locale);
|
|
880
|
-
}
|
|
881
|
-
/**
|
|
882
|
-
* Firestore path for a translations file.
|
|
883
|
-
*/
|
|
884
|
-
dbTranslationsPath(translationsId, options) {
|
|
885
|
-
const mode = options.mode;
|
|
886
|
-
if (!(mode === "draft" || mode === "published")) {
|
|
887
|
-
throw new Error(`invalid mode: ${mode}`);
|
|
888
|
-
}
|
|
889
|
-
const slug = translationsId.replaceAll("/", "--");
|
|
890
|
-
const dbPath = `Projects/${this.projectId}/TranslationsManager/${mode}/Translations/${slug}`;
|
|
891
|
-
return dbPath;
|
|
892
|
-
}
|
|
893
|
-
/**
|
|
894
|
-
* Firestore doc ref for a translations file.
|
|
895
|
-
*/
|
|
896
|
-
dbTranslationsRef(translationsId, options) {
|
|
897
|
-
const dbPath = this.dbTranslationsPath(translationsId, options);
|
|
898
|
-
return this.db.doc(dbPath);
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* Returns a data source configuration object.
|
|
902
|
-
*/
|
|
903
|
-
async getDataSource(dataSourceId) {
|
|
904
|
-
const dbPath = `Projects/${this.projectId}/DataSources/${dataSourceId}`;
|
|
905
|
-
const docRef = this.db.doc(dbPath);
|
|
906
|
-
const doc = await docRef.get();
|
|
907
|
-
if (doc.exists) {
|
|
908
|
-
return doc.data();
|
|
909
|
-
}
|
|
910
|
-
return null;
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* Syncs a data source to draft state.
|
|
914
|
-
*/
|
|
915
|
-
async syncDataSource(dataSourceId, options) {
|
|
916
|
-
const dataSource = await this.getDataSource(dataSourceId);
|
|
917
|
-
if (!dataSource) {
|
|
918
|
-
throw new Error(`data source not found: ${dataSourceId}`);
|
|
919
|
-
}
|
|
920
|
-
const data = await this.fetchData(dataSource);
|
|
921
|
-
const dataSourceDocRef = this.db.doc(
|
|
922
|
-
`Projects/${this.projectId}/DataSources/${dataSourceId}`
|
|
923
|
-
);
|
|
924
|
-
const dataDocRef = this.db.doc(
|
|
925
|
-
`Projects/${this.projectId}/DataSources/${dataSourceId}/Data/draft`
|
|
926
|
-
);
|
|
927
|
-
const syncedBy = options?.syncedBy || "root-cms-client";
|
|
928
|
-
const updatedDataSource = {
|
|
929
|
-
...dataSource,
|
|
930
|
-
syncedAt: Timestamp2.now(),
|
|
931
|
-
syncedBy
|
|
932
|
-
};
|
|
933
|
-
const batch = this.db.batch();
|
|
934
|
-
batch.set(dataDocRef, {
|
|
935
|
-
dataSource: updatedDataSource,
|
|
936
|
-
data
|
|
937
|
-
});
|
|
938
|
-
batch.update(dataSourceDocRef, {
|
|
939
|
-
syncedAt: Timestamp2.now(),
|
|
940
|
-
syncedBy
|
|
941
|
-
});
|
|
942
|
-
await batch.commit();
|
|
943
|
-
console.log(`synced data source: ${dataSourceId}`);
|
|
944
|
-
console.log(`synced by: ${syncedBy}`);
|
|
945
|
-
}
|
|
946
|
-
async publishDataSource(dataSourceId, options) {
|
|
947
|
-
const dataSource = await this.getDataSource(dataSourceId);
|
|
948
|
-
if (!dataSource) {
|
|
949
|
-
throw new Error(`data source not found: ${dataSourceId}`);
|
|
950
|
-
}
|
|
951
|
-
const dataSourceDocRef = this.db.doc(
|
|
952
|
-
`Projects/${this.projectId}/DataSources/${dataSourceId}`
|
|
953
|
-
);
|
|
954
|
-
const dataDocRefDraft = this.db.doc(
|
|
955
|
-
`Projects/${this.projectId}/DataSources/${dataSourceId}/draft`
|
|
956
|
-
);
|
|
957
|
-
const dataDocRefPublished = this.db.doc(
|
|
958
|
-
`Projects/${this.projectId}/DataSources/${dataSourceId}/published`
|
|
959
|
-
);
|
|
960
|
-
const dataRes = await this.getFromDataSource(dataSourceId, { mode: "draft" });
|
|
961
|
-
const publishedBy = options?.publishedBy || "root-cms-client";
|
|
962
|
-
const updatedDataSource = {
|
|
963
|
-
...dataSource,
|
|
964
|
-
publishedAt: Timestamp2.now(),
|
|
965
|
-
publishedBy
|
|
966
|
-
};
|
|
967
|
-
const batch = this.db.batch();
|
|
968
|
-
batch.set(dataDocRefPublished, {
|
|
969
|
-
dataSource: updatedDataSource,
|
|
970
|
-
data: dataRes?.data || null,
|
|
971
|
-
...dataRes?.headers ? { headers: dataRes.headers } : {}
|
|
972
|
-
});
|
|
973
|
-
batch.update(dataDocRefDraft, {
|
|
974
|
-
dataSource: updatedDataSource
|
|
975
|
-
});
|
|
976
|
-
batch.update(dataSourceDocRef, {
|
|
977
|
-
publishedAt: Timestamp2.now(),
|
|
978
|
-
publishedBy
|
|
979
|
-
});
|
|
980
|
-
await batch.commit();
|
|
981
|
-
console.log(`published data ${dataSourceId}`);
|
|
982
|
-
console.log(`published by: ${publishedBy}`);
|
|
983
|
-
}
|
|
984
|
-
async publishDataSources(dataSourceIds, options) {
|
|
985
|
-
const publishedBy = options?.publishedBy || "root-cms-client";
|
|
986
|
-
const batch = options?.batch || this.db.batch();
|
|
987
|
-
for (const id of dataSourceIds) {
|
|
988
|
-
const dataSource = await this.getDataSource(id);
|
|
989
|
-
if (!dataSource) {
|
|
990
|
-
throw new Error(`data source not found: ${id}`);
|
|
991
|
-
}
|
|
992
|
-
const dataSourceDocRef = this.db.doc(
|
|
993
|
-
`Projects/${this.projectId}/DataSources/${id}`
|
|
994
|
-
);
|
|
995
|
-
const dataDocRefDraft = this.db.doc(
|
|
996
|
-
`Projects/${this.projectId}/DataSources/${id}/draft`
|
|
997
|
-
);
|
|
998
|
-
const dataDocRefPublished = this.db.doc(
|
|
999
|
-
`Projects/${this.projectId}/DataSources/${id}/published`
|
|
1000
|
-
);
|
|
1001
|
-
const dataRes = await this.getFromDataSource(id, { mode: "draft" });
|
|
1002
|
-
const updatedDataSource = {
|
|
1003
|
-
...dataSource,
|
|
1004
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
1005
|
-
publishedBy
|
|
1006
|
-
};
|
|
1007
|
-
batch.set(dataDocRefPublished, {
|
|
1008
|
-
dataSource: updatedDataSource,
|
|
1009
|
-
data: dataRes?.data || null,
|
|
1010
|
-
...dataRes?.headers ? { headers: dataRes.headers } : {}
|
|
1011
|
-
});
|
|
1012
|
-
batch.update(dataDocRefDraft, { dataSource: updatedDataSource });
|
|
1013
|
-
batch.update(dataSourceDocRef, {
|
|
1014
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
1015
|
-
publishedBy
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1018
|
-
if (!options?.batch || options?.commitBatch) {
|
|
1019
|
-
await batch.commit();
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
async fetchData(dataSource) {
|
|
1023
|
-
if (dataSource.type === "http") {
|
|
1024
|
-
return await this.fetchHttpData(dataSource);
|
|
1025
|
-
}
|
|
1026
|
-
throw new Error(`unsupported data source: ${dataSource.type}`);
|
|
1027
|
-
}
|
|
1028
|
-
async fetchHttpData(dataSource) {
|
|
1029
|
-
const url = dataSource.url || "";
|
|
1030
|
-
if (!url.startsWith("https://")) {
|
|
1031
|
-
throw new Error(`url not supported: ${url}`);
|
|
1032
|
-
}
|
|
1033
|
-
const res = await fetch(url, {
|
|
1034
|
-
method: dataSource.httpOptions?.method || "GET",
|
|
1035
|
-
headers: dataSource.httpOptions?.headers || [],
|
|
1036
|
-
body: dataSource.httpOptions?.body || void 0
|
|
1037
|
-
});
|
|
1038
|
-
if (res.status !== 200) {
|
|
1039
|
-
const err = await res.text();
|
|
1040
|
-
throw new Error(`req failed: ${err}`);
|
|
1041
|
-
}
|
|
1042
|
-
const contentType = String(res.headers.get("content-type"));
|
|
1043
|
-
if (contentType.includes("application/json")) {
|
|
1044
|
-
return await res.json();
|
|
1045
|
-
}
|
|
1046
|
-
return res.text();
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Fetches data from a data source.
|
|
1050
|
-
*/
|
|
1051
|
-
async getFromDataSource(dataSourceId, options) {
|
|
1052
|
-
const mode = options?.mode || "published";
|
|
1053
|
-
if (!(mode === "draft" || mode === "published")) {
|
|
1054
|
-
throw new Error(`invalid mode: ${mode}`);
|
|
1055
|
-
}
|
|
1056
|
-
if (!dataSourceId || dataSourceId.includes("/")) {
|
|
1057
|
-
throw new Error(`invalid data source id: ${dataSourceId}`);
|
|
1058
|
-
}
|
|
1059
|
-
const docRef = this.dbDataSourceDataRef(dataSourceId, { mode });
|
|
1060
|
-
const doc = await docRef.get();
|
|
1061
|
-
if (doc.exists) {
|
|
1062
|
-
return doc.data();
|
|
1063
|
-
}
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Firestore path for a datasource data.
|
|
1068
|
-
*/
|
|
1069
|
-
dbDataSourceDataPath(dataSourceId, options) {
|
|
1070
|
-
if (!dataSourceId || dataSourceId.includes("/")) {
|
|
1071
|
-
throw new Error(`invalid data source id: ${dataSourceId}`);
|
|
1072
|
-
}
|
|
1073
|
-
const mode = options.mode;
|
|
1074
|
-
if (!(mode === "draft" || mode === "published")) {
|
|
1075
|
-
throw new Error(`invalid mode: ${mode}`);
|
|
1076
|
-
}
|
|
1077
|
-
const dbPath = `Projects/${this.projectId}/DataSources/${dataSourceId}/Data/${mode}`;
|
|
1078
|
-
return dbPath;
|
|
1079
|
-
}
|
|
1080
|
-
/**
|
|
1081
|
-
* Firestore doc ref for a datasource data.
|
|
1082
|
-
*/
|
|
1083
|
-
dbDataSourceDataRef(dataSourceId, options) {
|
|
1084
|
-
const dbPath = this.dbDataSourceDataPath(dataSourceId, options);
|
|
1085
|
-
return this.db.doc(dbPath);
|
|
1086
|
-
}
|
|
1087
|
-
/**
|
|
1088
|
-
* Gets the user's role from the project's ACL.
|
|
1089
|
-
*/
|
|
1090
|
-
async getUserRole(email) {
|
|
1091
|
-
if (!email) {
|
|
1092
|
-
return null;
|
|
1093
|
-
}
|
|
1094
|
-
const docRef = this.db.doc(`Projects/${this.projectId}`);
|
|
1095
|
-
const snapshot = await docRef.get();
|
|
1096
|
-
const data = snapshot.data() || {};
|
|
1097
|
-
const acl = data.roles || {};
|
|
1098
|
-
if (email in acl) {
|
|
1099
|
-
return acl[email];
|
|
1100
|
-
}
|
|
1101
|
-
if (!email.includes("@")) {
|
|
1102
|
-
console.warn(`invalid email: ${email}`);
|
|
1103
|
-
return null;
|
|
1104
|
-
}
|
|
1105
|
-
const domain = email.split("@").at(-1);
|
|
1106
|
-
if (domain && `*@${domain}` in acl) {
|
|
1107
|
-
return acl[`*@${domain}`];
|
|
1108
|
-
}
|
|
1109
|
-
return null;
|
|
1110
|
-
}
|
|
1111
|
-
/**
|
|
1112
|
-
* Verifies user exists in the ACL list.
|
|
1113
|
-
*/
|
|
1114
|
-
async userExistsInAcl(email) {
|
|
1115
|
-
if (!email) {
|
|
1116
|
-
return false;
|
|
1117
|
-
}
|
|
1118
|
-
const docRef = this.db.doc(`Projects/${this.projectId}`);
|
|
1119
|
-
const snapshot = await docRef.get();
|
|
1120
|
-
const data = snapshot.data() || {};
|
|
1121
|
-
const acl = data.roles || {};
|
|
1122
|
-
if (email in acl) {
|
|
1123
|
-
return true;
|
|
1124
|
-
}
|
|
1125
|
-
if (!email.includes("@")) {
|
|
1126
|
-
console.warn(`invalid email: ${email}`);
|
|
1127
|
-
return false;
|
|
1128
|
-
}
|
|
1129
|
-
const domain = email.split("@").at(-1);
|
|
1130
|
-
if (domain && `*@${domain}` in acl) {
|
|
1131
|
-
return true;
|
|
1132
|
-
}
|
|
1133
|
-
return false;
|
|
1134
|
-
}
|
|
1135
|
-
async logAction(action, options) {
|
|
1136
|
-
if (!action) {
|
|
1137
|
-
throw new Error('missing required: "action"');
|
|
1138
|
-
}
|
|
1139
|
-
const data = {
|
|
1140
|
-
action,
|
|
1141
|
-
timestamp: Timestamp2.now(),
|
|
1142
|
-
by: options?.by || "system",
|
|
1143
|
-
metadata: options?.metadata || {}
|
|
1144
|
-
};
|
|
1145
|
-
if (options?.links) {
|
|
1146
|
-
data.links = options.links;
|
|
1147
|
-
}
|
|
1148
|
-
const colRef = this.db.collection(`Projects/${this.projectId}/ActionLogs`);
|
|
1149
|
-
await colRef.add(data);
|
|
1150
|
-
const metaStr = options?.metadata ? stringifyObj(options.metadata) : "";
|
|
1151
|
-
console.log(`[${data.timestamp.toMillis()}] action: ${action} ${metaStr}`);
|
|
1152
|
-
const cmsPluginConfig = this.cmsPlugin.getConfig();
|
|
1153
|
-
if (cmsPluginConfig.onAction) {
|
|
1154
|
-
try {
|
|
1155
|
-
cmsPluginConfig.onAction(data);
|
|
1156
|
-
} catch (err) {
|
|
1157
|
-
console.error(err);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
/**
|
|
1162
|
-
* Creates a batch request that is capable of fetching one or more docs,
|
|
1163
|
-
* corresponding translations, and dataSources.
|
|
1164
|
-
*/
|
|
1165
|
-
createBatchRequest(options) {
|
|
1166
|
-
return new BatchRequest(this, options);
|
|
1167
|
-
}
|
|
1168
|
-
};
|
|
1169
|
-
function isRichTextData(data) {
|
|
1170
|
-
return Boolean(
|
|
1171
|
-
isObject(data) && Array.isArray(data.blocks) && data.time && data.version
|
|
1172
|
-
);
|
|
1173
|
-
}
|
|
1174
|
-
function getCmsPlugin(rootConfig) {
|
|
1175
|
-
const plugins = rootConfig.plugins || [];
|
|
1176
|
-
const plugin = plugins.find((plugin2) => plugin2.name === "root-cms");
|
|
1177
|
-
if (!plugin) {
|
|
1178
|
-
throw new Error("could not find root-cms plugin config in root.config.ts");
|
|
1179
|
-
}
|
|
1180
|
-
return plugin;
|
|
1181
|
-
}
|
|
1182
|
-
function marshalData(data) {
|
|
1183
|
-
if (isRichTextData(data)) {
|
|
1184
|
-
return data;
|
|
1185
|
-
}
|
|
1186
|
-
const result = {};
|
|
1187
|
-
for (const key in data) {
|
|
1188
|
-
const val = data[key];
|
|
1189
|
-
if (isObject(val)) {
|
|
1190
|
-
result[key] = marshalData(val);
|
|
1191
|
-
} else if (Array.isArray(val)) {
|
|
1192
|
-
if (val.length > 0 && val.some((item) => isObject(item))) {
|
|
1193
|
-
const items = val.map((item) => {
|
|
1194
|
-
if (isObject(item)) {
|
|
1195
|
-
return marshalData(item);
|
|
1196
|
-
}
|
|
1197
|
-
return item;
|
|
1198
|
-
});
|
|
1199
|
-
result[key] = toArrayObject(items);
|
|
1200
|
-
} else {
|
|
1201
|
-
result[key] = val;
|
|
1202
|
-
}
|
|
1203
|
-
} else {
|
|
1204
|
-
result[key] = val;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
return result;
|
|
1208
|
-
}
|
|
1209
|
-
function unmarshalData(data) {
|
|
1210
|
-
const result = {};
|
|
1211
|
-
for (const key in data) {
|
|
1212
|
-
const val = data[key];
|
|
1213
|
-
if (isObject(val)) {
|
|
1214
|
-
if (val.toMillis) {
|
|
1215
|
-
result[key] = val.toMillis();
|
|
1216
|
-
} else if (Object.hasOwn(val, "_array") && Array.isArray(val._array)) {
|
|
1217
|
-
const arr = val._array.map((arrayKey) => {
|
|
1218
|
-
return {
|
|
1219
|
-
...unmarshalData(val[arrayKey] || {}),
|
|
1220
|
-
_arrayKey: arrayKey
|
|
1221
|
-
};
|
|
1222
|
-
});
|
|
1223
|
-
result[key] = arr;
|
|
1224
|
-
} else {
|
|
1225
|
-
result[key] = unmarshalData(val);
|
|
1226
|
-
}
|
|
1227
|
-
} else {
|
|
1228
|
-
result[key] = val;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
return result;
|
|
1232
|
-
}
|
|
1233
|
-
function normalizeData(data) {
|
|
1234
|
-
return unmarshalData(data);
|
|
1235
|
-
}
|
|
1236
|
-
function toArrayObject(arr) {
|
|
1237
|
-
if (!Array.isArray(arr)) {
|
|
1238
|
-
return arr;
|
|
1239
|
-
}
|
|
1240
|
-
const arrObject = { _array: [] };
|
|
1241
|
-
for (const item of arr) {
|
|
1242
|
-
const key = randString(6);
|
|
1243
|
-
arrObject[key] = item;
|
|
1244
|
-
arrObject._array.push(key);
|
|
1245
|
-
}
|
|
1246
|
-
return arrObject;
|
|
1247
|
-
}
|
|
1248
|
-
var marshalArray = toArrayObject;
|
|
1249
|
-
function unmarshalArray(arrObject) {
|
|
1250
|
-
if (!Array.isArray(arrObject?._array)) {
|
|
1251
|
-
return [];
|
|
1252
|
-
}
|
|
1253
|
-
const arr = arrObject._array.map((k) => arrObject[k]);
|
|
1254
|
-
return arr;
|
|
1255
|
-
}
|
|
1256
|
-
function isObject(data) {
|
|
1257
|
-
return typeof data === "object" && !Array.isArray(data) && data !== null;
|
|
1258
|
-
}
|
|
1259
|
-
function randString(len) {
|
|
1260
|
-
const result = [];
|
|
1261
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
1262
|
-
for (let i = 0; i < len; i++) {
|
|
1263
|
-
const rand = Math.floor(Math.random() * chars.length);
|
|
1264
|
-
result.push(chars.charAt(rand));
|
|
1265
|
-
}
|
|
1266
|
-
return result.join("");
|
|
1267
|
-
}
|
|
1268
|
-
function translationsForLocale(translationsMap, locale) {
|
|
1269
|
-
const localeTranslations = {};
|
|
1270
|
-
Object.values(translationsMap).forEach((string2) => {
|
|
1271
|
-
const source = string2.source;
|
|
1272
|
-
const translation = string2[locale] || string2.en || string2.source;
|
|
1273
|
-
localeTranslations[source] = translation;
|
|
1274
|
-
});
|
|
1275
|
-
return localeTranslations;
|
|
1276
|
-
}
|
|
1277
|
-
function stringifyObj(obj) {
|
|
1278
|
-
function format(obj2) {
|
|
1279
|
-
if (obj2 === null) {
|
|
1280
|
-
return "null";
|
|
1281
|
-
}
|
|
1282
|
-
if (typeof obj2 === "undefined") {
|
|
1283
|
-
return "undefined";
|
|
1284
|
-
}
|
|
1285
|
-
if (typeof obj2 === "string") {
|
|
1286
|
-
return `"${obj2.replaceAll('"', '\\"')}"`;
|
|
1287
|
-
}
|
|
1288
|
-
if (typeof obj2 !== "object") {
|
|
1289
|
-
return String(obj2);
|
|
1290
|
-
}
|
|
1291
|
-
if (Array.isArray(obj2)) {
|
|
1292
|
-
return `[${obj2.map(format).join(", ")}]`;
|
|
1293
|
-
}
|
|
1294
|
-
const entries = Object.entries(obj2).map(([key, value]) => {
|
|
1295
|
-
return `${key}: ${format(value)}`;
|
|
1296
|
-
});
|
|
1297
|
-
return `{${entries.join(", ")}}`;
|
|
1298
|
-
}
|
|
1299
|
-
return format(obj);
|
|
1300
|
-
}
|
|
1301
|
-
function parseDocId(docId) {
|
|
1302
|
-
const sepIndex = docId.indexOf("/");
|
|
1303
|
-
if (sepIndex <= 0) {
|
|
1304
|
-
throw new Error(`invalid doc id: ${docId}`);
|
|
1305
|
-
}
|
|
1306
|
-
const collection2 = docId.slice(0, sepIndex);
|
|
1307
|
-
const slug = docId.slice(sepIndex + 1).replaceAll("/", "--");
|
|
1308
|
-
if (!collection2 || !slug) {
|
|
1309
|
-
throw new Error(`invalid doc id: ${docId}`);
|
|
1310
|
-
}
|
|
1311
|
-
return { collection: collection2, slug };
|
|
1312
|
-
}
|
|
1313
|
-
var BatchRequest = class {
|
|
1314
|
-
constructor(cmsClient, options) {
|
|
1315
|
-
this.docIds = [];
|
|
1316
|
-
this.dataSourceIds = [];
|
|
1317
|
-
this.queries = [];
|
|
1318
|
-
this.translationsIds = [];
|
|
1319
|
-
this.cmsClient = cmsClient;
|
|
1320
|
-
this.db = cmsClient.db;
|
|
1321
|
-
this.options = options;
|
|
1322
|
-
}
|
|
1323
|
-
/**
|
|
1324
|
-
* Adds a doc to the batch request.
|
|
1325
|
-
*/
|
|
1326
|
-
addDoc(docId) {
|
|
1327
|
-
this.docIds.push(docId);
|
|
1328
|
-
}
|
|
1329
|
-
/**
|
|
1330
|
-
* Adds a data source to the batch request.
|
|
1331
|
-
*/
|
|
1332
|
-
addDataSource(dataSourceId) {
|
|
1333
|
-
this.dataSourceIds.push(dataSourceId);
|
|
1334
|
-
}
|
|
1335
|
-
/**
|
|
1336
|
-
* Adds a collection-based query to the batch request.
|
|
1337
|
-
*/
|
|
1338
|
-
addQuery(queryId, collectionId, queryOptions) {
|
|
1339
|
-
this.queries.push({
|
|
1340
|
-
queryId,
|
|
1341
|
-
collectionId,
|
|
1342
|
-
queryOptions
|
|
1343
|
-
});
|
|
1344
|
-
}
|
|
1345
|
-
/**
|
|
1346
|
-
* Adds a translation file to the request.
|
|
1347
|
-
*/
|
|
1348
|
-
addTranslations(translationsId) {
|
|
1349
|
-
this.translationsIds.push(translationsId);
|
|
1350
|
-
}
|
|
1351
|
-
/**
|
|
1352
|
-
* Fetches data from the DB.
|
|
1353
|
-
*/
|
|
1354
|
-
async fetch() {
|
|
1355
|
-
const res = new BatchResponse();
|
|
1356
|
-
const promises = [
|
|
1357
|
-
this.fetchDocs(res),
|
|
1358
|
-
this.fetchQueries(res),
|
|
1359
|
-
this.fetchDataSources(res)
|
|
1360
|
-
];
|
|
1361
|
-
if (!this.options.translate && this.translationsIds.length > 0) {
|
|
1362
|
-
promises.push(this.fetchTranslations(res));
|
|
1363
|
-
}
|
|
1364
|
-
await Promise.all(promises);
|
|
1365
|
-
if (this.translationsIds.length > 0) {
|
|
1366
|
-
await this.fetchTranslations(res);
|
|
1367
|
-
}
|
|
1368
|
-
return res;
|
|
1369
|
-
}
|
|
1370
|
-
async fetchDocs(res) {
|
|
1371
|
-
if (this.docIds.length === 0) {
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
const docRefs = this.docIds.map((docId) => {
|
|
1375
|
-
const [collectionId, slug] = docId.split("/");
|
|
1376
|
-
return this.cmsClient.dbDocRef(collectionId, slug, {
|
|
1377
|
-
mode: this.options.mode
|
|
1378
|
-
});
|
|
1379
|
-
});
|
|
1380
|
-
const docs = await this.db.getAll(...docRefs);
|
|
1381
|
-
this.docIds.forEach((docId, i) => {
|
|
1382
|
-
const doc = docs[i];
|
|
1383
|
-
if (!doc.exists) {
|
|
1384
|
-
console.warn(`doc "${docId}" does not exist`);
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
const docData = unmarshalData(doc.data());
|
|
1388
|
-
res.docs[docId] = docData;
|
|
1389
|
-
if (this.options.translate) {
|
|
1390
|
-
this.addTranslations(docId);
|
|
1391
|
-
}
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
async fetchQueries(res) {
|
|
1395
|
-
if (this.queries.length === 0) {
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
const mode = this.options.mode;
|
|
1399
|
-
const handleQuery = async (queryItem) => {
|
|
1400
|
-
const docsPath = this.cmsClient.dbCollectionDocsPath(
|
|
1401
|
-
queryItem.collectionId,
|
|
1402
|
-
{ mode }
|
|
1403
|
-
);
|
|
1404
|
-
const queryOptions = queryItem.queryOptions || {};
|
|
1405
|
-
let query = this.db.collection(docsPath);
|
|
1406
|
-
if (queryOptions.limit) {
|
|
1407
|
-
query = query.limit(queryOptions.limit);
|
|
1408
|
-
}
|
|
1409
|
-
if (queryOptions.offset) {
|
|
1410
|
-
query = query.offset(queryOptions.offset);
|
|
1411
|
-
}
|
|
1412
|
-
if (queryOptions.orderBy) {
|
|
1413
|
-
query = query.orderBy(
|
|
1414
|
-
queryOptions.orderBy,
|
|
1415
|
-
queryOptions.orderByDirection
|
|
1416
|
-
);
|
|
1417
|
-
}
|
|
1418
|
-
if (queryOptions.query) {
|
|
1419
|
-
query = queryOptions.query(query);
|
|
1420
|
-
}
|
|
1421
|
-
const results = await query.get();
|
|
1422
|
-
const docs = [];
|
|
1423
|
-
results.forEach((result) => {
|
|
1424
|
-
const doc = unmarshalData(result.data());
|
|
1425
|
-
docs.push(doc);
|
|
1426
|
-
});
|
|
1427
|
-
res.queries[queryItem.queryId] = docs;
|
|
1428
|
-
};
|
|
1429
|
-
await Promise.all(this.queries.map((queryItem) => handleQuery(queryItem)));
|
|
1430
|
-
}
|
|
1431
|
-
async fetchDataSources(res) {
|
|
1432
|
-
if (this.dataSourceIds.length === 0) {
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
const docRefs = this.dataSourceIds.map((dataSourceId) => {
|
|
1436
|
-
return this.cmsClient.dbDataSourceDataRef(dataSourceId, {
|
|
1437
|
-
mode: this.options.mode
|
|
1438
|
-
});
|
|
1439
|
-
});
|
|
1440
|
-
const docs = await this.db.getAll(...docRefs);
|
|
1441
|
-
this.dataSourceIds.forEach((dataSourceId, i) => {
|
|
1442
|
-
const doc = docs[i];
|
|
1443
|
-
if (!doc.exists) {
|
|
1444
|
-
console.warn(`"data source "${dataSourceId}" does not exist`);
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
res.dataSources[dataSourceId] = doc.data();
|
|
1448
|
-
});
|
|
1449
|
-
}
|
|
1450
|
-
async fetchTranslations(res) {
|
|
1451
|
-
if (this.translationsIds.length === 0) {
|
|
1452
|
-
return;
|
|
1453
|
-
}
|
|
1454
|
-
const docRefs = this.translationsIds.map((translationsId) => {
|
|
1455
|
-
return this.cmsClient.dbTranslationsRef(translationsId, {
|
|
1456
|
-
mode: this.options.mode
|
|
1457
|
-
});
|
|
1458
|
-
});
|
|
1459
|
-
const docs = await this.db.getAll(...docRefs);
|
|
1460
|
-
this.translationsIds.forEach((translationsId, i) => {
|
|
1461
|
-
const doc = docs[i];
|
|
1462
|
-
if (!doc.exists) {
|
|
1463
|
-
return;
|
|
1464
|
-
}
|
|
1465
|
-
res.translations[translationsId] = doc.data();
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
};
|
|
1469
|
-
var BatchResponse = class {
|
|
1470
|
-
constructor() {
|
|
1471
|
-
this.docs = {};
|
|
1472
|
-
this.queries = {};
|
|
1473
|
-
this.dataSources = {};
|
|
1474
|
-
this.translations = {};
|
|
1475
|
-
}
|
|
1476
|
-
/**
|
|
1477
|
-
* Returns a map of translations for a given locale or locale fallbacks.
|
|
1478
|
-
*
|
|
1479
|
-
* The input is either a single locale (e.g. "de") or an array of locales
|
|
1480
|
-
* representing the fallback tree, e.g. ["en-CA", "en-GB", "en"].
|
|
1481
|
-
*
|
|
1482
|
-
* TODO(stevenle): support the locale fallback tree.
|
|
1483
|
-
*
|
|
1484
|
-
* The returned value is a flat map of source string to translated string,
|
|
1485
|
-
* e.g.:
|
|
1486
|
-
* {"<source>": "<translation>"}
|
|
1487
|
-
*/
|
|
1488
|
-
getTranslations(locale) {
|
|
1489
|
-
const translationsMap = this.getTranslationsMap();
|
|
1490
|
-
const translations = translationsForLocale(translationsMap, locale);
|
|
1491
|
-
return translations;
|
|
1492
|
-
}
|
|
1493
|
-
/**
|
|
1494
|
-
* Merges the strings from all translations files retrieved in the request.
|
|
1495
|
-
* The returned value is a map of string to translations, e.g.:
|
|
1496
|
-
*
|
|
1497
|
-
* {"<hash>": {"source": "<source>", "<locale>": "<translation>"}}
|
|
1498
|
-
*/
|
|
1499
|
-
getTranslationsMap() {
|
|
1500
|
-
const translationsDocs = Object.values(this.translations).reverse();
|
|
1501
|
-
const translationsMap = {};
|
|
1502
|
-
for (const translationsDoc of translationsDocs) {
|
|
1503
|
-
const strings = translationsDoc.strings || {};
|
|
1504
|
-
for (const hash in strings) {
|
|
1505
|
-
const translations = strings[hash];
|
|
1506
|
-
translationsMap[hash] ??= { source: translations.source };
|
|
1507
|
-
for (const locale in translations) {
|
|
1508
|
-
if (locale !== "source" && translations[locale]) {
|
|
1509
|
-
translationsMap[hash][locale] = translations[locale];
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
return translationsMap;
|
|
1515
|
-
}
|
|
1516
|
-
};
|
|
20
|
+
__export
|
|
21
|
+
} from "./chunk-MLKGABMK.js";
|
|
1517
22
|
|
|
1518
23
|
// core/route.ts
|
|
1519
24
|
import {
|
|
@@ -1798,7 +303,7 @@ async function getMode(req) {
|
|
|
1798
303
|
}
|
|
1799
304
|
|
|
1800
305
|
// core/runtime.ts
|
|
1801
|
-
import { FieldValue
|
|
306
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
1802
307
|
async function getDoc(rootConfig, collectionId, slug, options) {
|
|
1803
308
|
const cmsPlugin = getCmsPlugin(rootConfig);
|
|
1804
309
|
const cmsPluginOptions = cmsPlugin.getConfig();
|
|
@@ -1919,7 +424,7 @@ async function publishScheduledDocs(rootConfig) {
|
|
|
1919
424
|
...sys,
|
|
1920
425
|
firstPublishedAt,
|
|
1921
426
|
firstPublishedBy,
|
|
1922
|
-
publishedAt:
|
|
427
|
+
publishedAt: FieldValue.serverTimestamp(),
|
|
1923
428
|
publishedBy: scheduledBy || ""
|
|
1924
429
|
}
|
|
1925
430
|
});
|
|
@@ -1927,11 +432,11 @@ async function publishScheduledDocs(rootConfig) {
|
|
|
1927
432
|
batch.delete(scheduledRef);
|
|
1928
433
|
batchCount += 1;
|
|
1929
434
|
batch.update(draftRef, {
|
|
1930
|
-
"sys.scheduledAt":
|
|
1931
|
-
"sys.scheduledBy":
|
|
435
|
+
"sys.scheduledAt": FieldValue.delete(),
|
|
436
|
+
"sys.scheduledBy": FieldValue.delete(),
|
|
1932
437
|
"sys.firstPublishedAt": firstPublishedAt,
|
|
1933
438
|
"sys.firstPublishedBy": firstPublishedBy,
|
|
1934
|
-
"sys.publishedAt":
|
|
439
|
+
"sys.publishedAt": FieldValue.serverTimestamp(),
|
|
1935
440
|
"sys.publishedBy": scheduledBy || ""
|
|
1936
441
|
});
|
|
1937
442
|
batchCount += 1;
|
|
@@ -1979,6 +484,7 @@ __export(schema_exports, {
|
|
|
1979
484
|
defineCollection: () => defineCollection,
|
|
1980
485
|
defineSchema: () => defineSchema,
|
|
1981
486
|
file: () => file,
|
|
487
|
+
glob: () => glob,
|
|
1982
488
|
image: () => image,
|
|
1983
489
|
multiselect: () => multiselect,
|
|
1984
490
|
number: () => number,
|
|
@@ -2060,6 +566,14 @@ function defineCollection(collection2) {
|
|
|
2060
566
|
return collection2;
|
|
2061
567
|
}
|
|
2062
568
|
var collection = defineCollection;
|
|
569
|
+
function glob(pattern, options) {
|
|
570
|
+
return {
|
|
571
|
+
_schemaPattern: true,
|
|
572
|
+
pattern,
|
|
573
|
+
exclude: options?.exclude,
|
|
574
|
+
omitFields: options?.omitFields
|
|
575
|
+
};
|
|
576
|
+
}
|
|
2063
577
|
export {
|
|
2064
578
|
BatchRequest,
|
|
2065
579
|
BatchResponse,
|