@blinkk/root-cms 2.4.8 → 2.4.10
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-DRQJXU4N.js +13 -0
- package/dist/altText-RDKJNVGH.js +7 -0
- package/dist/app.js +9 -269
- package/dist/chunk-377TGNX2.js +82 -0
- package/dist/chunk-62EVNFXB.js +1883 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-RYF3UTHQ.js +302 -0
- package/dist/chunk-SI44FG3H.js +136 -0
- package/dist/chunk-Y65VGJLE.js +193 -0
- package/dist/chunk-ZOEPOKGE.js +294 -0
- package/dist/cli.js +54 -362
- package/dist/{client-pSzji9ZN.d.ts → client-PhodvL2Q.d.ts} +32 -2
- package/dist/client.d.ts +2 -1
- package/dist/client.js +15 -1509
- package/dist/core.d.ts +2 -2
- package/dist/core.js +24 -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 +66 -2515
- package/dist/project.js +6 -76
- package/dist/richtext.js +2 -0
- package/dist/ui/ui.css +1 -1
- package/dist/ui/ui.js +237 -157
- package/dist/ui/ui.js.LEGAL.txt +124 -101
- package/package.json +5 -5
package/dist/plugin.js
CHANGED
|
@@ -1,2315 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
var __copyProps = (to, from, except, desc) => {
|
|
18
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
19
|
-
for (let key of __getOwnPropNames(from))
|
|
20
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
21
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
22
|
-
}
|
|
23
|
-
return to;
|
|
24
|
-
};
|
|
25
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
26
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
27
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
28
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
29
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
30
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
31
|
-
mod
|
|
32
|
-
));
|
|
33
|
-
|
|
34
|
-
// shared/ai/prompts/edit.txt
|
|
35
|
-
var require_edit = __commonJS({
|
|
36
|
-
"shared/ai/prompts/edit.txt"(exports, module) {
|
|
37
|
-
module.exports = 'Role: You are a highly accurate AI assistant specializing in converting unstructured documents into JSON files compatible with Root CMS. Root CMS is a web-based CMS for building websites and landing pages, storing its content as JSON files in a Firebase Firestore database.\n\nObjective: To edit and/or generate highly accurate JSON files compatible with Root CMS, based on user input (screenshots, chat messages, etc.) and, when editing JSON files, the original JSON file you are editing. The resulting content should contain all of the marketing content provided by the user input (and none of the annotations or superfluous content). The JSON files should be perfectly compatible with Root CMS.\n\nInput Requirements & Handling:\n\n- You will receive unstructured documents and you must convert them into JSON files compatible with Root CMS by either editing the existing JSON files or generating new ones from scratch. The JSON files you produce will typically represent a single section of a page of the website, such as a hero section, a feature section, or a footer.\n\n- When editing an existing JSON file, it will be annotated with a key `_type` that indicates the type of section it is, such as `Hero`, `Feature`, or `Footer`. You must ensure that the resulting JSON file is compatible with Root CMS and contains all of the marketing content provided by the user input. You will also be given a TypeScript interface in `root-cms.d.ts` that describes the structure of the JSON file you are editing. You must ensure that the resulting JSON file is compatible with this interface.\n\n- The input you receive will be broken into three parts. (1) `root-cms-d.ts` (the TypeScript interface), (2) the existing JSON file you are editing (or an empty JSON file when generating new content), and (3) the user input (screenshots, chat messages, instructions, etc.). You must ensure that the resulting JSON file is compatible with the structure defined by the TypeScript interface and contains all of the marketing content provided by the user input.\n\n- When you provide your response, if the user instructs you to only edit one part of the original JSON, make sure to leave the rest of the JSON file untouched. You must return the entire JSON document after applying changes. Never omit, rename, reorder or invent top-level keys. Copy all unmodified fields exactly as they appear in the input.\n\nRoot CMS JSON File Structure:\n\n- Root CMS JSON files are standard JSON files described by the TypeScript interface in `root-cms.d.ts`. However, there is one critical syntax rule that you must understand and always follow regarding array values. Array values are expressed as hash maps. The order of the resulting data when Root CMS unmarshals the data is provided by a special key called `_array`. For example, to represent an array [1, 2, 3] in the CMS, the JSON data may look like this. You may generate your own UIDs for the keys and the items within `_array`.\n\n```json\n{\n "_array": ["2HGwoV", "LjFHWk", "OLakGA", "UaGHJh"],\n "2HGwoV": 1,\n "LjFHWk": 2,\n "OLakGA": 3,\n "UaGHJh": 4\n}\n```\n\nWhen creating arrays in your JSON output, you must _never_ forget to add an `_array` key. If you forget to add an `_array` key, the output will NOT work with Root CMS. When adding new array items, you can create your own array keys.\n\n2. Images and file fields have system-provided metadata, that looks like this example:\n\n```\n{\n "src": "https://storage.googleapis.com/rootjs-dev.appspot.com/www/uploads/2cad5d65bb7690bad5dcd4a041cf2fef46361678.svg",\n "filename": "600x400.svg",\n "uploadedAt": "1754023634564",\n "alt": "",\n "width": "600",\n "uploadedBy": "user@example.com",\n "height": "400"\n}\n```\n\n3. Rich Text fields are stored as Editor.js documents, which look like this. You may generate your own UIDs for the `id` fields.\n\n```\n{\n "version": "2.28.2",\n "time": 1754024041637,\n "blocks": [\n {\n "type": "paragraph",\n "data": {\n "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <a href=\\"https://example.com\\">This accepts HTML</a>."\n },\n "id": "_8PEf3kBx2"\n },\n {\n "data": {\n "style": "unordered",\n "items": [\n {\n "content": "This is list item 1.",\n "items": []\n },\n {\n "items": [],\n "content": "This is list item 2."\n }\n ]\n },\n "id": "G5zMDP1ytM",\n "type": "unorderedList"\n }\n ]\n}\n```\n\n4. Root CMS supports "oneOf" fields, where values can be "one of" another schema. These are typically used as array values or object values, where the user can from one of other schema types to structure the data value. For example, each module in a page\'s module list is "one of" the modules available in the library. One of values have a system-provided `_type` key that describes the type of the value. For example, this is a hero module, whose type is "TemplateHeadline", one of the available templates from the module library:\n\n```\n{\n "eyebrow": "Lorem ipsum",\n "_type": "TemplateHeadline",\n "body": {\n "time": 1754023554170,\n "version": "2.28.2",\n "blocks": [\n {\n "type": "paragraph",\n "data": {\n "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."\n },\n "id": "IVmxEVsxmt"\n },\n {\n "id": "aMiHiywkQD",\n "type": "paragraph",\n "data": {\n "text": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."\n }\n }\n ]\n },\n "title": "Welcome to our website"\n}\n```\n\nWhen adding new items to arrays that use `oneOf` fields, try your best to figure out which content type would best satisfy the user\'s request. You must never _invent_ new content types, for example, if "TemplateFoo" doesn\'t exist in `root-cms.d.ts`, you must never add a value with `{"_type": "TemplateFoo"}`. If none of the available content types within `root-cms.d.ts` satisfy the user\'s request, avoid creating the new array item and inform the user that there is no existing template compatible.\n\nAdditionally, when adding new items to existing JSON data, you must carefully check the structure of those items as defined by `root-cms.d.ts`. Avoid inventing new fields for content types, use only those defined by the `root-cms.d.ts`. For example, if you are adding a button to a module, carefully check the button structure defined in `root-cms.d.ts` and only populate fields that are defined there.\n\nFinally, when you provide your response, it MUST be structured in the following way for handling by the chat client.\n\n{\n "data": <The JSON data generated by you via the instructions above>,\n "message": <A brief message that describes what you did.>\n}\n\nYou must always ONLY reply in this exact format. The brief message that describes what you did should be written succinctly, tersely, in a friendly but not cute tone, and be no more than a sentence or two.\n';
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// shared/ai/prompts/altText.txt
|
|
42
|
-
var require_altText = __commonJS({
|
|
43
|
-
"shared/ai/prompts/altText.txt"(exports, module) {
|
|
44
|
-
module.exports = "Create a descriptive and concise alt text for the attached image.\n\n- The alt text should be a brief but comprehensive description of the image, including key subjects, the setting, and any relevant details or actions.\n- The alt text should not exceed 125 characters.\n- Only provide one generation, and include only that data in the response. No surrounding text, clarifications, etc. Just the alt text.\n";
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// core/ai.ts
|
|
49
|
-
var ai_exports = {};
|
|
50
|
-
__export(ai_exports, {
|
|
51
|
-
Chat: () => Chat,
|
|
52
|
-
ChatClient: () => ChatClient,
|
|
53
|
-
generateImage: () => generateImage,
|
|
54
|
-
summarizeDiff: () => summarizeDiff
|
|
55
|
-
});
|
|
56
|
-
import crypto from "crypto";
|
|
57
|
-
import fs from "fs";
|
|
58
|
-
import path from "path";
|
|
59
|
-
import { vertexAI } from "@genkit-ai/google-genai";
|
|
60
|
-
import { Timestamp } from "firebase-admin/firestore";
|
|
61
|
-
import { genkit } from "genkit";
|
|
62
|
-
import { logger } from "genkit/logging";
|
|
63
|
-
async function summarizeDiff(cmsClient, options) {
|
|
64
|
-
const cmsPluginOptions = cmsClient.cmsPlugin.getConfig();
|
|
65
|
-
const firebaseConfig = cmsPluginOptions.firebaseConfig;
|
|
66
|
-
const model = (typeof cmsPluginOptions.experiments?.ai === "object" ? cmsPluginOptions.experiments.ai.model : void 0) || DEFAULT_MODEL;
|
|
67
|
-
const ai = genkit({
|
|
68
|
-
plugins: [
|
|
69
|
-
vertexAI({
|
|
70
|
-
projectId: firebaseConfig.projectId,
|
|
71
|
-
location: firebaseConfig.location || "us-central1"
|
|
72
|
-
})
|
|
73
|
-
]
|
|
74
|
-
});
|
|
75
|
-
const beforeJson = JSON.stringify(options.before ?? null, null, 2);
|
|
76
|
-
const afterJson = JSON.stringify(options.after ?? null, null, 2);
|
|
77
|
-
const systemPrompt = [
|
|
78
|
-
"You are an assistant that summarizes changes made to CMS documents stored as JSON.",
|
|
79
|
-
"Provide a concise description of the most important updates using short bullet points.",
|
|
80
|
-
'If there are no meaningful differences, respond with "No significant changes."',
|
|
81
|
-
`Focus on just the content changes, ignore insignificant changes to richtext blocks and structure, such as updates to the richtext block's "timestamp" and "version" fields.`
|
|
82
|
-
].join("\n");
|
|
83
|
-
const diffPrompt = [
|
|
84
|
-
"Previous version JSON:",
|
|
85
|
-
"```json",
|
|
86
|
-
beforeJson,
|
|
87
|
-
"```",
|
|
88
|
-
"",
|
|
89
|
-
"Updated version JSON:",
|
|
90
|
-
"```json",
|
|
91
|
-
afterJson,
|
|
92
|
-
"```",
|
|
93
|
-
"",
|
|
94
|
-
"Summarize the differences between the two payloads."
|
|
95
|
-
].join("\n");
|
|
96
|
-
const res = await ai.generate({
|
|
97
|
-
model: vertexAI.model(model),
|
|
98
|
-
messages: [
|
|
99
|
-
{
|
|
100
|
-
role: "system",
|
|
101
|
-
content: [{ text: systemPrompt }]
|
|
102
|
-
}
|
|
103
|
-
],
|
|
104
|
-
prompt: [{ text: diffPrompt }]
|
|
105
|
-
});
|
|
106
|
-
return res.text?.trim() || "";
|
|
107
|
-
}
|
|
108
|
-
async function generateImage(cmsClient, options) {
|
|
109
|
-
const cmsPluginOptions = cmsClient.cmsPlugin.getConfig();
|
|
110
|
-
const firebaseConfig = cmsPluginOptions.firebaseConfig;
|
|
111
|
-
const model = options.model || DEFAULT_IMAGEGEN_MODEL;
|
|
112
|
-
const ai = genkit({
|
|
113
|
-
plugins: [
|
|
114
|
-
vertexAI({
|
|
115
|
-
projectId: firebaseConfig.projectId,
|
|
116
|
-
location: firebaseConfig.location || "us-central1"
|
|
117
|
-
})
|
|
118
|
-
]
|
|
119
|
-
});
|
|
120
|
-
const res = await ai.generate({
|
|
121
|
-
model: vertexAI.model(model),
|
|
122
|
-
prompt: [
|
|
123
|
-
{
|
|
124
|
-
text: options.prompt
|
|
125
|
-
}
|
|
126
|
-
],
|
|
127
|
-
config: {
|
|
128
|
-
imageConfig: {
|
|
129
|
-
aspectRatio: options.aspectRatio
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
output: {
|
|
133
|
-
format: "media"
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
if (!res.media) {
|
|
137
|
-
throw new Error("No image generated");
|
|
138
|
-
}
|
|
139
|
-
return res.media.url;
|
|
140
|
-
}
|
|
141
|
-
function cleanModelName(model) {
|
|
142
|
-
return LEGACY_MODEL_RENAME[model] || model;
|
|
143
|
-
}
|
|
144
|
-
var DEFAULT_MODEL, DEFAULT_IMAGEGEN_MODEL, LEGACY_MODEL_RENAME, Chat, ChatClient;
|
|
145
|
-
var init_ai = __esm({
|
|
146
|
-
"core/ai.ts"() {
|
|
147
|
-
"use strict";
|
|
148
|
-
logger.setLogLevel("warn");
|
|
149
|
-
DEFAULT_MODEL = "gemini-3-flash-preview";
|
|
150
|
-
DEFAULT_IMAGEGEN_MODEL = "gemini-2.5-flash-image";
|
|
151
|
-
LEGACY_MODEL_RENAME = {
|
|
152
|
-
"vertexai/gemini-2.5-flash": "gemini-2.5-flash",
|
|
153
|
-
"vertexai/gemini-2.0-pro": "gemini-2.0-pro"
|
|
154
|
-
};
|
|
155
|
-
Chat = class {
|
|
156
|
-
constructor(chatClient, id, options) {
|
|
157
|
-
this.chatClient = chatClient;
|
|
158
|
-
this.cmsClient = chatClient.cmsClient;
|
|
159
|
-
this.cmsPluginOptions = this.cmsClient.cmsPlugin.getConfig();
|
|
160
|
-
this.id = id;
|
|
161
|
-
this.history = options?.history ?? [];
|
|
162
|
-
this.model = cleanModelName(
|
|
163
|
-
options?.model || (typeof this.cmsPluginOptions.experiments?.ai === "object" ? this.cmsPluginOptions.experiments.ai.model : void 0) || DEFAULT_MODEL
|
|
164
|
-
);
|
|
165
|
-
const firebaseConfig = this.cmsPluginOptions.firebaseConfig;
|
|
166
|
-
this.ai = genkit({
|
|
167
|
-
plugins: [
|
|
168
|
-
vertexAI({
|
|
169
|
-
projectId: firebaseConfig.projectId,
|
|
170
|
-
location: firebaseConfig.location || "us-central1"
|
|
171
|
-
})
|
|
172
|
-
]
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
/** Builds the messages for the AI request. */
|
|
176
|
-
async buildMessages(options) {
|
|
177
|
-
const messages = this.history;
|
|
178
|
-
const hasSystemPrompt = messages.some(
|
|
179
|
-
(msg) => msg.role === "system" && msg.content.length > 0
|
|
180
|
-
);
|
|
181
|
-
if (!hasSystemPrompt) {
|
|
182
|
-
messages.push({
|
|
183
|
-
role: "system",
|
|
184
|
-
content: [{ text: await this.buildSystemPrompt(options) }]
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
if (options.mode === "edit") {
|
|
188
|
-
messages.push({
|
|
189
|
-
role: "user",
|
|
190
|
-
content: [
|
|
191
|
-
{
|
|
192
|
-
text: [
|
|
193
|
-
"The JSON you must edit is:",
|
|
194
|
-
"",
|
|
195
|
-
JSON.stringify(options.editData || {}, null, 2)
|
|
196
|
-
].join("")
|
|
197
|
-
}
|
|
198
|
-
]
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
return messages;
|
|
202
|
-
}
|
|
203
|
-
/** Builds the request sent to the AI based on the `ChatMode`. */
|
|
204
|
-
async buildGenerateRequest(prompt, options) {
|
|
205
|
-
if (options.mode === "edit") {
|
|
206
|
-
return {
|
|
207
|
-
messages: await this.buildMessages(options),
|
|
208
|
-
model: this.model,
|
|
209
|
-
prompt
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
return {
|
|
213
|
-
messages: await this.buildMessages(options),
|
|
214
|
-
model: this.model,
|
|
215
|
-
prompt
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
/** Sends the request to the AI and stores the history in the session and the database. */
|
|
219
|
-
async sendPrompt(prompt, options = {}) {
|
|
220
|
-
const chatRequest = await this.buildGenerateRequest(prompt, options);
|
|
221
|
-
const res = await this.ai.generate({
|
|
222
|
-
model: vertexAI.model(chatRequest.model),
|
|
223
|
-
messages: chatRequest.messages,
|
|
224
|
-
prompt: Array.isArray(prompt) ? prompt.flat() : prompt,
|
|
225
|
-
// NOTE(stevenle): the global location is required for
|
|
226
|
-
// `gemini-3-flash-preview`, it may be possible to remove this in the
|
|
227
|
-
// future.
|
|
228
|
-
config: { location: "global" }
|
|
229
|
-
});
|
|
230
|
-
this.history = res.messages;
|
|
231
|
-
await this.dbDoc().update({
|
|
232
|
-
history: this.history,
|
|
233
|
-
modifiedAt: Timestamp.now()
|
|
234
|
-
});
|
|
235
|
-
if (options.mode === "edit") {
|
|
236
|
-
const aiResponse = res.output;
|
|
237
|
-
if (aiResponse?.data && !aiResponse.message) {
|
|
238
|
-
aiResponse.message = "";
|
|
239
|
-
}
|
|
240
|
-
return aiResponse;
|
|
241
|
-
}
|
|
242
|
-
return { message: res.text, data: null };
|
|
243
|
-
}
|
|
244
|
-
dbDoc() {
|
|
245
|
-
return this.chatClient.dbCollection().doc(this.id);
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Builds the system prompt sent to the AI, based on the `ChatMode` and
|
|
249
|
-
* supplied `SendPromptOptions`. `SendPromptOptions` may contain data or
|
|
250
|
-
* references to information needed to construct the prompt.
|
|
251
|
-
*/
|
|
252
|
-
async buildSystemPrompt(options) {
|
|
253
|
-
const serializedRootConfig = JSON.stringify(
|
|
254
|
-
this.cmsClient.rootConfig,
|
|
255
|
-
null,
|
|
256
|
-
2
|
|
257
|
-
);
|
|
258
|
-
if (options.mode === "edit") {
|
|
259
|
-
const rootDir = process.cwd();
|
|
260
|
-
const rootCmsDefsPath = path.resolve(rootDir, "root-cms.d.ts");
|
|
261
|
-
const rootCmsDefs = fs.existsSync(rootCmsDefsPath) ? fs.readFileSync(rootCmsDefsPath, {
|
|
262
|
-
encoding: "utf8"
|
|
263
|
-
}) : null;
|
|
264
|
-
const text = [(await Promise.resolve().then(() => __toESM(require_edit(), 1))).default];
|
|
265
|
-
if (rootCmsDefs) {
|
|
266
|
-
text.push(
|
|
267
|
-
"Here is the `root-cms.d.ts` file for this project:",
|
|
268
|
-
"```",
|
|
269
|
-
rootCmsDefs,
|
|
270
|
-
"```"
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
return text.join("\n");
|
|
274
|
-
}
|
|
275
|
-
if (options.mode === "altText") {
|
|
276
|
-
return (await Promise.resolve().then(() => __toESM(require_altText(), 1))).default;
|
|
277
|
-
}
|
|
278
|
-
const systemText = [
|
|
279
|
-
`You are an assistant for a headless CMS called Root CMS which is used on a website called ${this.cmsPluginOptions.name || this.cmsPluginOptions.id}. Your job is to answer questions about the docs in the system, and if requested, help suggest changes to the JSON data in the docs. If you don't know the answer, just say that you don't know, don't try to make up an answer. Be friendly and playful with your messaging.`,
|
|
280
|
-
"",
|
|
281
|
-
"Here is the root.config.ts file for the site:",
|
|
282
|
-
"```",
|
|
283
|
-
serializedRootConfig,
|
|
284
|
-
"```",
|
|
285
|
-
"",
|
|
286
|
-
"Here are the docs that exist in the system:"
|
|
287
|
-
];
|
|
288
|
-
const pages = await this.cmsClient.listDocs("Pages", { mode: "draft" });
|
|
289
|
-
pages.docs.forEach((doc) => {
|
|
290
|
-
systemText.push(JSON.stringify(doc));
|
|
291
|
-
});
|
|
292
|
-
return systemText.join("\n");
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
ChatClient = class {
|
|
296
|
-
constructor(cmsClient, user) {
|
|
297
|
-
this.cmsClient = cmsClient;
|
|
298
|
-
this.user = user;
|
|
299
|
-
}
|
|
300
|
-
async createChat() {
|
|
301
|
-
const chatId = crypto.randomUUID();
|
|
302
|
-
const docRef = this.dbCollection().doc(chatId);
|
|
303
|
-
const chat = new Chat(this, chatId);
|
|
304
|
-
await docRef.set({
|
|
305
|
-
id: chatId,
|
|
306
|
-
createdBy: this.user,
|
|
307
|
-
createdAt: Timestamp.now(),
|
|
308
|
-
modifiedAt: Timestamp.now(),
|
|
309
|
-
model: chat.model
|
|
310
|
-
});
|
|
311
|
-
return chat;
|
|
312
|
-
}
|
|
313
|
-
async getOrCreateChat(chatId) {
|
|
314
|
-
return chatId ? this.getChat(chatId) : this.createChat();
|
|
315
|
-
}
|
|
316
|
-
async getChat(chatId) {
|
|
317
|
-
const docRef = this.dbCollection().doc(chatId);
|
|
318
|
-
const chatDoc = await docRef.get();
|
|
319
|
-
if (!chatDoc.exists) {
|
|
320
|
-
throw new Error(`${chatId} does not exist`);
|
|
321
|
-
}
|
|
322
|
-
const chatData = chatDoc.data() || {};
|
|
323
|
-
return new Chat(this, chatId, {
|
|
324
|
-
history: chatData.history,
|
|
325
|
-
model: chatData.model
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
async listChats(options) {
|
|
329
|
-
const limit = options?.limit || 20;
|
|
330
|
-
const query = this.dbCollection().where("createdBy", "==", this.user).limit(limit).orderBy("createdAt", "desc");
|
|
331
|
-
const res = await query.get();
|
|
332
|
-
return res.docs.map((doc) => doc.data());
|
|
333
|
-
}
|
|
334
|
-
dbCollection() {
|
|
335
|
-
return this.cmsClient.db.collection(
|
|
336
|
-
`Projects/${this.cmsClient.projectId}/Experiments/ai/Chat`
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// cli/generate-types.ts
|
|
344
|
-
var generate_types_exports = {};
|
|
345
|
-
__export(generate_types_exports, {
|
|
346
|
-
generateSchemaDts: () => generateSchemaDts,
|
|
347
|
-
generateTypes: () => generateTypes
|
|
348
|
-
});
|
|
349
|
-
import { promises as fs3 } from "fs";
|
|
350
|
-
import path4 from "path";
|
|
351
|
-
import { fileURLToPath } from "url";
|
|
352
|
-
import { loadRootConfig, viteSsrLoadModule } from "@blinkk/root/node";
|
|
353
|
-
import * as dom from "dts-dom";
|
|
354
|
-
async function generateTypes() {
|
|
355
|
-
const rootDir = process.cwd();
|
|
356
|
-
const rootConfig = await loadRootConfig(rootDir, { command: "root-cms" });
|
|
357
|
-
const modulePath = path4.resolve(__dirname, "./project.js");
|
|
358
|
-
const project = await viteSsrLoadModule(
|
|
359
|
-
rootConfig,
|
|
360
|
-
modulePath
|
|
361
|
-
);
|
|
362
|
-
const schemas = await project.getProjectSchemas();
|
|
363
|
-
const outputPath = path4.resolve(rootDir, "root-cms.d.ts");
|
|
364
|
-
await generateSchemaDts(outputPath, schemas);
|
|
365
|
-
console.log("saved root-cms.d.ts!");
|
|
366
|
-
}
|
|
367
|
-
async function generateSchemaDts(outputPath, schemas) {
|
|
368
|
-
const dtsFormatter = new DtsFormatter();
|
|
369
|
-
for (const fileId in schemas) {
|
|
370
|
-
const schema = schemas[fileId];
|
|
371
|
-
dtsFormatter.addSchemaFile(fileId, schema);
|
|
372
|
-
}
|
|
373
|
-
const output = dtsFormatter.toString();
|
|
374
|
-
await fs3.writeFile(outputPath, output, "utf-8");
|
|
375
|
-
}
|
|
376
|
-
function fieldProperty(field, options) {
|
|
377
|
-
const prop = dom.create.property(
|
|
378
|
-
field.id,
|
|
379
|
-
fieldType(field, options),
|
|
380
|
-
dom.DeclarationFlags.Optional
|
|
381
|
-
);
|
|
382
|
-
const jsdoc = [];
|
|
383
|
-
if (field.label) {
|
|
384
|
-
if (field.help) {
|
|
385
|
-
jsdoc.push(`${field.label}. ${field.help}`);
|
|
386
|
-
} else {
|
|
387
|
-
jsdoc.push(field.label);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (field.deprecated) {
|
|
391
|
-
jsdoc.push("@deprecated");
|
|
392
|
-
}
|
|
393
|
-
if (jsdoc.length > 0) {
|
|
394
|
-
prop.jsDocComment = jsdoc.join("\n");
|
|
395
|
-
}
|
|
396
|
-
return prop;
|
|
397
|
-
}
|
|
398
|
-
function fieldType(field, options) {
|
|
399
|
-
if (field.type === "array") {
|
|
400
|
-
return dom.type.array(fieldType(field.of, options));
|
|
401
|
-
}
|
|
402
|
-
if (field.type === "boolean") {
|
|
403
|
-
return dom.type.boolean;
|
|
404
|
-
}
|
|
405
|
-
if (field.type === "date") {
|
|
406
|
-
return dom.type.string;
|
|
407
|
-
}
|
|
408
|
-
if (field.type === "datetime") {
|
|
409
|
-
return dom.type.number;
|
|
410
|
-
}
|
|
411
|
-
if (field.type === "file") {
|
|
412
|
-
const fileType = dom.create.namedTypeReference("RootCMSFile");
|
|
413
|
-
return fileType;
|
|
414
|
-
}
|
|
415
|
-
if (field.type === "image") {
|
|
416
|
-
const imageType = dom.create.namedTypeReference("RootCMSImage");
|
|
417
|
-
return imageType;
|
|
418
|
-
}
|
|
419
|
-
if (field.type === "multiselect") {
|
|
420
|
-
return dom.type.array(dom.type.string);
|
|
421
|
-
}
|
|
422
|
-
if (field.type === "oneof") {
|
|
423
|
-
const oneOf = dom.create.namedTypeReference("RootCMSOneOf");
|
|
424
|
-
if (field.types && Array.isArray(field.types)) {
|
|
425
|
-
const unionTypes = [];
|
|
426
|
-
field.types.forEach((schema) => {
|
|
427
|
-
let typeName;
|
|
428
|
-
if (typeof schema === "string") {
|
|
429
|
-
typeName = schema;
|
|
430
|
-
return;
|
|
431
|
-
} else {
|
|
432
|
-
typeName = schema.name;
|
|
433
|
-
}
|
|
434
|
-
if (!typeName) {
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
const cleanName = alphanumeric(typeName);
|
|
438
|
-
const oneOfTypeId = `${cleanName}Fields`;
|
|
439
|
-
if (typeof schema === "object" && !options.oneOfTypes[oneOfTypeId]) {
|
|
440
|
-
const oneOfTypeInterface = dom.create.interface(
|
|
441
|
-
oneOfTypeId,
|
|
442
|
-
dom.DeclarationFlags.Export
|
|
443
|
-
);
|
|
444
|
-
if (schema.description) {
|
|
445
|
-
oneOfTypeInterface.jsDocComment = schema.description;
|
|
446
|
-
}
|
|
447
|
-
const oneOfTypeFields = schema.fields || [];
|
|
448
|
-
oneOfTypeFields.forEach((f) => {
|
|
449
|
-
oneOfTypeInterface.members.push(fieldProperty(f, options));
|
|
450
|
-
});
|
|
451
|
-
options.oneOfTypes[oneOfTypeId] = oneOfTypeInterface;
|
|
452
|
-
}
|
|
453
|
-
const oneOfOption = dom.create.namedTypeReference("RootCMSOneOfOption");
|
|
454
|
-
oneOfOption.typeArguments = [
|
|
455
|
-
dom.type.stringLiteral(typeName),
|
|
456
|
-
dom.create.namedTypeReference(oneOfTypeId)
|
|
457
|
-
];
|
|
458
|
-
unionTypes.push(oneOfOption);
|
|
459
|
-
});
|
|
460
|
-
if (unionTypes.length > 0) {
|
|
461
|
-
oneOf.typeArguments = [dom.create.union(unionTypes)];
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
return oneOf;
|
|
465
|
-
}
|
|
466
|
-
if (field.type === "reference") {
|
|
467
|
-
const referenceType = dom.create.namedTypeReference("RootCMSReference");
|
|
468
|
-
return referenceType;
|
|
469
|
-
}
|
|
470
|
-
if (field.type === "references") {
|
|
471
|
-
const referenceType = dom.create.namedTypeReference("RootCMSReference");
|
|
472
|
-
return dom.type.array(referenceType);
|
|
473
|
-
}
|
|
474
|
-
if (field.type === "richtext") {
|
|
475
|
-
const richtextType = dom.create.namedTypeReference("RootCMSRichText");
|
|
476
|
-
return richtextType;
|
|
477
|
-
}
|
|
478
|
-
if (field.type === "select") {
|
|
479
|
-
return dom.type.string;
|
|
480
|
-
}
|
|
481
|
-
if (field.type === "string") {
|
|
482
|
-
return dom.type.string;
|
|
483
|
-
}
|
|
484
|
-
if (field.type === "object") {
|
|
485
|
-
const subproperties = (field.fields || []).map(
|
|
486
|
-
(f) => fieldProperty(f, options)
|
|
487
|
-
);
|
|
488
|
-
return dom.create.objectType(subproperties);
|
|
489
|
-
}
|
|
490
|
-
return dom.type.unknown;
|
|
491
|
-
}
|
|
492
|
-
function alphanumeric(input) {
|
|
493
|
-
return input.replace(/[^a-zA-Z0-9]/g, "");
|
|
494
|
-
}
|
|
495
|
-
var __dirname, TEMPLATE, DtsFormatter;
|
|
496
|
-
var init_generate_types = __esm({
|
|
497
|
-
"cli/generate-types.ts"() {
|
|
498
|
-
"use strict";
|
|
499
|
-
__dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
500
|
-
TEMPLATE = `/* eslint-disable */
|
|
501
|
-
/** Root.js CMS types. This file is autogenerated. */
|
|
502
|
-
|
|
503
|
-
export interface RootCMSFile {
|
|
504
|
-
src: string;
|
|
505
|
-
width?: number;
|
|
506
|
-
height?: number;
|
|
507
|
-
alt?: string;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
export type RootCMSImage = RootCMSFile;
|
|
511
|
-
|
|
512
|
-
export type RootCMSOneOf<T = any> = T;
|
|
513
|
-
|
|
514
|
-
export type RootCMSOneOfOption<T, Base> = Base & {_type: T};
|
|
515
|
-
|
|
516
|
-
export interface RootCMSRichTextBlock {
|
|
517
|
-
type: string;
|
|
518
|
-
data: any;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
export interface RootCMSRichText {
|
|
522
|
-
blocks: RootCMSRichTextBlock[];
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
export interface RootCMSReference {
|
|
526
|
-
/** The id of the doc, e.g. "Pages/foo-bar". */
|
|
527
|
-
id: string;
|
|
528
|
-
/** The collection id of the doc, e.g. "Pages". */
|
|
529
|
-
collection: string;
|
|
530
|
-
/** The slug of the doc, e.g. "foo-bar". */
|
|
531
|
-
slug: string;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
export interface RootCMSDoc<Fields extends {}> {
|
|
535
|
-
/** The id of the doc, e.g. "Pages/foo-bar". */
|
|
536
|
-
id: string;
|
|
537
|
-
/** The collection id of the doc, e.g. "Pages". */
|
|
538
|
-
collection: string;
|
|
539
|
-
/** The slug of the doc, e.g. "foo-bar". */
|
|
540
|
-
slug: string;
|
|
541
|
-
/** System-level metadata. */
|
|
542
|
-
sys: {
|
|
543
|
-
createdAt: number;
|
|
544
|
-
createdBy: string;
|
|
545
|
-
modifiedAt: number;
|
|
546
|
-
modifiedBy: string;
|
|
547
|
-
firstPublishedAt?: number;
|
|
548
|
-
firstPublishedBy?: string;
|
|
549
|
-
publishedAt?: number;
|
|
550
|
-
publishedBy?: string;
|
|
551
|
-
locales?: string[];
|
|
552
|
-
};
|
|
553
|
-
/** User-entered field values from the CMS. */
|
|
554
|
-
fields?: Fields;
|
|
555
|
-
}`;
|
|
556
|
-
DtsFormatter = class {
|
|
557
|
-
constructor() {
|
|
558
|
-
/**
|
|
559
|
-
* Field types defined in either `.schema.ts` files or used by `oneOf()`
|
|
560
|
-
* fields.
|
|
561
|
-
*/
|
|
562
|
-
this.fieldsTypes = {};
|
|
563
|
-
/**
|
|
564
|
-
* For `collections/*.schema.ts` files, output a corresponding "RootCMSDoc"
|
|
565
|
-
* type. For example, `collections/BlogPosts.schema.ts` would output a type
|
|
566
|
-
* called `BlogPostsDoc`.
|
|
567
|
-
*/
|
|
568
|
-
this.docTypes = {};
|
|
569
|
-
}
|
|
570
|
-
/** Adds the types for a `.schema.ts` file to the `.d.ts` file. */
|
|
571
|
-
addSchemaFile(fileId, schema) {
|
|
572
|
-
const jsdoc = `Generated from \`${fileId}\`.`;
|
|
573
|
-
const typeId = alphanumeric(path4.parse(fileId).name.split(".")[0]);
|
|
574
|
-
const oneOfTypes = {};
|
|
575
|
-
const fieldsTypeId = `${typeId}Fields`;
|
|
576
|
-
const fieldsType = dom.create.interface(
|
|
577
|
-
fieldsTypeId,
|
|
578
|
-
dom.DeclarationFlags.Export
|
|
579
|
-
);
|
|
580
|
-
fieldsType.jsDocComment = jsdoc;
|
|
581
|
-
for (const field of schema.fields) {
|
|
582
|
-
if (!field.id) {
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
585
|
-
fieldsType.members.push(fieldProperty(field, { typeId, oneOfTypes }));
|
|
586
|
-
}
|
|
587
|
-
for (const oneOfTypeId in oneOfTypes) {
|
|
588
|
-
if (!this.fieldsTypes[fieldsTypeId]) {
|
|
589
|
-
this.fieldsTypes[oneOfTypeId] = oneOfTypes[oneOfTypeId];
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
this.fieldsTypes[fieldsTypeId] = fieldsType;
|
|
593
|
-
if (fileId.startsWith("/collections/")) {
|
|
594
|
-
const docTypeId = `${typeId}Doc`;
|
|
595
|
-
const baseType = dom.create.namedTypeReference("RootCMSDoc");
|
|
596
|
-
baseType.typeArguments.push(dom.create.namedTypeReference(fieldsTypeId));
|
|
597
|
-
const docType = dom.create.alias(
|
|
598
|
-
docTypeId,
|
|
599
|
-
baseType,
|
|
600
|
-
dom.DeclarationFlags.Export
|
|
601
|
-
);
|
|
602
|
-
docType.jsDocComment = jsdoc;
|
|
603
|
-
this.docTypes[docTypeId] = docType;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
/** Generates the `.d.ts` output as a string. */
|
|
607
|
-
toString() {
|
|
608
|
-
const results = [TEMPLATE];
|
|
609
|
-
const sortedFieldsTypes = Object.keys(this.fieldsTypes).sort();
|
|
610
|
-
for (const fieldsTypeId of sortedFieldsTypes) {
|
|
611
|
-
const fieldsType = this.fieldsTypes[fieldsTypeId];
|
|
612
|
-
results.push(this.typeToString(fieldsType));
|
|
613
|
-
const docTypeId = this.fieldsTypeToDocType(fieldsTypeId);
|
|
614
|
-
const docType = this.docTypes[docTypeId];
|
|
615
|
-
if (docType) {
|
|
616
|
-
results.push(this.typeToString(docType));
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
const output = results.join("\n\n").replaceAll("/** ", "/** ").replaceAll(" */", " */").replace(/\r\n|\r|\n/g, "\n") + "\n";
|
|
620
|
-
return output;
|
|
621
|
-
}
|
|
622
|
-
/**
|
|
623
|
-
* Converts "<Name>Fields" to "<Name>Doc".
|
|
624
|
-
*/
|
|
625
|
-
fieldsTypeToDocType(fieldsTypeId) {
|
|
626
|
-
if (!fieldsTypeId.endsWith("Fields")) {
|
|
627
|
-
throw new Error(`"${fieldsTypeId}" should be suffixed with "Fields"`);
|
|
628
|
-
}
|
|
629
|
-
const name = fieldsTypeId.slice(0, -6);
|
|
630
|
-
return `${name}Doc`;
|
|
631
|
-
}
|
|
632
|
-
typeToString(type2) {
|
|
633
|
-
return this.reformatOutput(dom.emit(type2, { singleLineJsDocComments: true }));
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Formats the output to Google style conventions, e.g. 2-space indents and
|
|
637
|
-
* single quote strings.
|
|
638
|
-
*/
|
|
639
|
-
reformatOutput(input) {
|
|
640
|
-
const lines = input.trim().split("\n");
|
|
641
|
-
const results = [];
|
|
642
|
-
for (const line of lines) {
|
|
643
|
-
const convertedLine = line.replace(/ {4}/g, " ").replaceAll('"', "'");
|
|
644
|
-
results.push(convertedLine);
|
|
645
|
-
}
|
|
646
|
-
return results.join("\n");
|
|
647
|
-
}
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
// core/plugin.ts
|
|
653
|
-
import { promises as fs4 } from "fs";
|
|
654
|
-
import path5 from "path";
|
|
655
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
656
|
-
import { viteSsrLoadModule as viteSsrLoadModule2 } from "@blinkk/root/node";
|
|
657
|
-
import bodyParser from "body-parser";
|
|
658
|
-
import {
|
|
659
|
-
applicationDefault,
|
|
660
|
-
getApp,
|
|
661
|
-
initializeApp
|
|
662
|
-
} from "firebase-admin/app";
|
|
663
|
-
import { getAuth } from "firebase-admin/auth";
|
|
664
|
-
import { getFirestore } from "firebase-admin/firestore";
|
|
665
|
-
import * as jsonwebtoken from "jsonwebtoken";
|
|
666
|
-
import sirv from "sirv";
|
|
667
|
-
|
|
668
|
-
// shared/sse.ts
|
|
669
|
-
var SSEEvent = {
|
|
670
|
-
/**
|
|
671
|
-
* First connection event.
|
|
672
|
-
*/
|
|
673
|
-
CONNECTED: "connected",
|
|
674
|
-
/**
|
|
675
|
-
* Changes to any .schema.ts file.
|
|
676
|
-
*/
|
|
677
|
-
SCHEMA_CHANGED: "schemaChanged"
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
// core/api.ts
|
|
681
|
-
init_ai();
|
|
682
|
-
import { promises as fs2 } from "fs";
|
|
683
|
-
import path3 from "path";
|
|
684
|
-
import { multipartMiddleware } from "@blinkk/root/middleware";
|
|
685
|
-
|
|
686
|
-
// core/client.ts
|
|
687
|
-
import crypto2 from "crypto";
|
|
688
|
-
import {
|
|
689
|
-
FieldValue as FieldValue2,
|
|
690
|
-
Timestamp as Timestamp3
|
|
691
|
-
} from "firebase-admin/firestore";
|
|
692
|
-
|
|
693
|
-
// core/translations-manager.ts
|
|
694
|
-
import {
|
|
695
|
-
FieldValue,
|
|
696
|
-
Timestamp as Timestamp2
|
|
697
|
-
} from "firebase-admin/firestore";
|
|
698
|
-
|
|
699
|
-
// shared/strings.ts
|
|
700
|
-
import fnv from "fnv-plus";
|
|
701
|
-
function normalizeStr(str) {
|
|
702
|
-
const lines = str.trim().split("\n").map((line) => removeTrailingWhitespace(line));
|
|
703
|
-
return lines.join("\n");
|
|
704
|
-
}
|
|
705
|
-
function removeTrailingWhitespace(str) {
|
|
706
|
-
return str.trimEnd().replace(/ $/, "");
|
|
707
|
-
}
|
|
708
|
-
function hashStr(str) {
|
|
709
|
-
if (!str || typeof str !== "string") {
|
|
710
|
-
throw new Error("input string is invalid");
|
|
711
|
-
}
|
|
712
|
-
return fnv.fast1a52hex(normalizeStr(str));
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// core/translations-manager.ts
|
|
716
|
-
var TRANSLATIONS_DB_PATH_FORMAT = "/Projects/{project}/TranslationsManager/{mode}/Translations";
|
|
717
|
-
var TRANSLATIONS_LOCALE_DOC_DB_PATH_FORMAT = `${TRANSLATIONS_DB_PATH_FORMAT}/{id}:{locale}`;
|
|
718
|
-
var TranslationsManager = class {
|
|
719
|
-
constructor(cmsClient) {
|
|
720
|
-
this.cmsClient = cmsClient;
|
|
721
|
-
}
|
|
722
|
-
/**
|
|
723
|
-
* Saves draft translations for a translations doc id.
|
|
724
|
-
*
|
|
725
|
-
* Example:
|
|
726
|
-
* ```
|
|
727
|
-
* const strings = {
|
|
728
|
-
* 'one': {es: 'uno', fr: 'un'},
|
|
729
|
-
* 'two': {es: 'dos', fr: 'deux'},
|
|
730
|
-
* };
|
|
731
|
-
* await tm.saveTranslations('Pages/index', strings);
|
|
732
|
-
* ```
|
|
733
|
-
*/
|
|
734
|
-
async saveTranslations(id, strings, options) {
|
|
735
|
-
const mode = "draft";
|
|
736
|
-
const localesSet = /* @__PURE__ */ new Set();
|
|
737
|
-
Object.values(strings).forEach((entry) => {
|
|
738
|
-
Object.keys(entry).forEach((locale) => {
|
|
739
|
-
if (locale !== "source") {
|
|
740
|
-
localesSet.add(locale);
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
});
|
|
744
|
-
const batch = this.cmsClient.db.batch();
|
|
745
|
-
const locales = Array.from(localesSet);
|
|
746
|
-
locales.forEach((locale) => {
|
|
747
|
-
const updates = {
|
|
748
|
-
id,
|
|
749
|
-
locale,
|
|
750
|
-
sys: {
|
|
751
|
-
modifiedAt: Timestamp2.now(),
|
|
752
|
-
modifiedBy: options?.modifiedBy || "root-cms-client"
|
|
753
|
-
},
|
|
754
|
-
strings: {}
|
|
755
|
-
};
|
|
756
|
-
if (options?.tags && options.tags.length > 0) {
|
|
757
|
-
updates.tags = FieldValue.arrayUnion(...options.tags);
|
|
758
|
-
}
|
|
759
|
-
let numUpdates = 0;
|
|
760
|
-
const hashMap = this.toLocaleDocHashMap(strings, locale);
|
|
761
|
-
Object.entries(hashMap).forEach(([hash, translations]) => {
|
|
762
|
-
Object.entries(translations).forEach(([locale2, translation]) => {
|
|
763
|
-
if (translation) {
|
|
764
|
-
updates.strings[hash] ??= {};
|
|
765
|
-
updates.strings[hash][locale2] = translation;
|
|
766
|
-
numUpdates += 1;
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
});
|
|
770
|
-
if (numUpdates > 0) {
|
|
771
|
-
const localeDocPath = buildTranslationsLocaleDocDbPath({
|
|
772
|
-
project: this.cmsClient.projectId,
|
|
773
|
-
mode,
|
|
774
|
-
id,
|
|
775
|
-
locale
|
|
776
|
-
});
|
|
777
|
-
const localeDocRef = this.cmsClient.db.doc(localeDocPath);
|
|
778
|
-
batch.set(localeDocRef, updates, { merge: true });
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
await batch.commit();
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Publishes a translations doc.
|
|
785
|
-
*/
|
|
786
|
-
async publishTranslations(id, options) {
|
|
787
|
-
const db = this.cmsClient.db;
|
|
788
|
-
const project = this.cmsClient.projectId;
|
|
789
|
-
const draftPath = buildTranslationsDbPath({ project, mode: "draft" });
|
|
790
|
-
const query = db.collection(draftPath).where("id", "==", id);
|
|
791
|
-
const res = await query.get();
|
|
792
|
-
if (res.size === 0) {
|
|
793
|
-
console.warn(`no translations to publish for ${id}`);
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
const batch = options?.batch || db.batch();
|
|
797
|
-
res.docs.forEach((doc) => {
|
|
798
|
-
const translationsLocaleDoc = doc.data();
|
|
799
|
-
const sys = {
|
|
800
|
-
...translationsLocaleDoc.sys,
|
|
801
|
-
publishedAt: Timestamp2.now(),
|
|
802
|
-
publishedBy: options?.publishedBy || "root-cms-client"
|
|
803
|
-
};
|
|
804
|
-
batch.update(doc.ref, { sys });
|
|
805
|
-
const publishedDocPath = buildTranslationsLocaleDocDbPath({
|
|
806
|
-
project,
|
|
807
|
-
mode: "published",
|
|
808
|
-
id: translationsLocaleDoc.id,
|
|
809
|
-
locale: translationsLocaleDoc.locale
|
|
810
|
-
});
|
|
811
|
-
const publishedDocRef = db.doc(publishedDocPath);
|
|
812
|
-
batch.set(publishedDocRef, { ...translationsLocaleDoc, sys });
|
|
813
|
-
});
|
|
814
|
-
const shouldCommitBatch = !options?.batch;
|
|
815
|
-
if (shouldCommitBatch) {
|
|
816
|
-
await batch.commit();
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
/**
|
|
820
|
-
* Fetches translations from one or more translations docs in the translations
|
|
821
|
-
* manager.
|
|
822
|
-
*
|
|
823
|
-
* Example:
|
|
824
|
-
* ```
|
|
825
|
-
* await tm.loadTranslations();
|
|
826
|
-
* // =>
|
|
827
|
-
* // {
|
|
828
|
-
* // "one": {"es": "uno", "fr": "un"},
|
|
829
|
-
* // "two": {"es": "dos", "fr": "deux"}
|
|
830
|
-
* // }
|
|
831
|
-
* ```
|
|
832
|
-
*
|
|
833
|
-
* To load a specific set of translations docs by id:
|
|
834
|
-
* ```
|
|
835
|
-
* const translationsToLoad = ['Global/strings', 'Global/header', 'Global/footer', 'Pages/index'];
|
|
836
|
-
* await tm.loadTranslations({ids: translationsToLoad});
|
|
837
|
-
* // =>
|
|
838
|
-
* // {
|
|
839
|
-
* // "one": {"es": "uno", "fr": "un"},
|
|
840
|
-
* // "two": {"es": "dos", "fr": "deux"}
|
|
841
|
-
* // }
|
|
842
|
-
* ```
|
|
843
|
-
*
|
|
844
|
-
* To load a subset of locales (more performant):
|
|
845
|
-
* ```
|
|
846
|
-
* await tm.loadTranslations({locales: ['es']});
|
|
847
|
-
* // =>
|
|
848
|
-
* // {
|
|
849
|
-
* // "one": {"es": "uno"},
|
|
850
|
-
* // "two": {"es": "dos"}
|
|
851
|
-
* // }
|
|
852
|
-
* ```
|
|
853
|
-
*/
|
|
854
|
-
async loadTranslations(options) {
|
|
855
|
-
const mode = options?.mode || "published";
|
|
856
|
-
const dbPath = buildTranslationsDbPath({
|
|
857
|
-
project: this.cmsClient.projectId,
|
|
858
|
-
mode
|
|
859
|
-
});
|
|
860
|
-
let query = this.cmsClient.db.collection(dbPath);
|
|
861
|
-
if (options?.ids && options.ids.length > 0) {
|
|
862
|
-
query = query.where("id", "in", options.ids);
|
|
863
|
-
}
|
|
864
|
-
if (options?.tags && options.tags.length > 0) {
|
|
865
|
-
query = query.where("tags", "array-contains", options.tags);
|
|
866
|
-
}
|
|
867
|
-
if (options?.locales && options.locales.length > 0) {
|
|
868
|
-
query = query.where("locale", "in", options.locales);
|
|
869
|
-
}
|
|
870
|
-
const results = await query.get();
|
|
871
|
-
const strings = {};
|
|
872
|
-
results.forEach((result) => {
|
|
873
|
-
const localeDoc = result.data();
|
|
874
|
-
Object.values(localeDoc.strings || {}).forEach((item) => {
|
|
875
|
-
strings[item.source] ??= { source: item.source };
|
|
876
|
-
strings[item.source][localeDoc.locale] = item.translation;
|
|
877
|
-
});
|
|
878
|
-
});
|
|
879
|
-
return strings;
|
|
880
|
-
}
|
|
881
|
-
/**
|
|
882
|
-
* Fetches translations for a given locale, with optional fallbacks.
|
|
883
|
-
* The return value is a map of source string to translated string.
|
|
884
|
-
*
|
|
885
|
-
* Example:
|
|
886
|
-
* ```
|
|
887
|
-
* await translationsDoc.loadTranslationsForLocale('es');
|
|
888
|
-
* // =>
|
|
889
|
-
* // {
|
|
890
|
-
* // "one": "uno",
|
|
891
|
-
* // "two": "dos",
|
|
892
|
-
* // }
|
|
893
|
-
* ```
|
|
894
|
-
*/
|
|
895
|
-
async loadTranslationsForLocale(locale, options) {
|
|
896
|
-
const localeSet = /* @__PURE__ */ new Set([
|
|
897
|
-
locale,
|
|
898
|
-
...options?.fallbackLocales || []
|
|
899
|
-
]);
|
|
900
|
-
const fallbackLocales = Array.from(localeSet);
|
|
901
|
-
const multiLocaleStrings = await this.loadTranslations({
|
|
902
|
-
mode: options?.mode,
|
|
903
|
-
locales: fallbackLocales
|
|
904
|
-
});
|
|
905
|
-
return this.toSingleLocaleMap(multiLocaleStrings, fallbackLocales);
|
|
906
|
-
}
|
|
907
|
-
/**
|
|
908
|
-
* Converts a multi-locale translations map to a flat single-locale map,
|
|
909
|
-
* with optional support for fallback locales.
|
|
910
|
-
*
|
|
911
|
-
* ```
|
|
912
|
-
* const multiLocaleStrings = {
|
|
913
|
-
* 'one': {es: 'uno', fr: 'un'},
|
|
914
|
-
* 'two': {es: 'dos', fr: 'deux'}
|
|
915
|
-
* };
|
|
916
|
-
* translationsDoc.toSingleLocaleMap(multiLocaleStrings, ['es']);
|
|
917
|
-
* // =>
|
|
918
|
-
* // {
|
|
919
|
-
* // "one": "uno",
|
|
920
|
-
* // "two": "dos",
|
|
921
|
-
* // }
|
|
922
|
-
* ```
|
|
923
|
-
*/
|
|
924
|
-
toSingleLocaleMap(multiLocaleStrings, fallbackLocales) {
|
|
925
|
-
const singleLocaleStrings = {};
|
|
926
|
-
Object.entries(multiLocaleStrings).forEach(([source, translations]) => {
|
|
927
|
-
let translation = source;
|
|
928
|
-
for (const locale of fallbackLocales) {
|
|
929
|
-
if (translations[locale]) {
|
|
930
|
-
translation = translations[locale];
|
|
931
|
-
break;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
singleLocaleStrings[source] = translation;
|
|
935
|
-
});
|
|
936
|
-
return singleLocaleStrings;
|
|
937
|
-
}
|
|
938
|
-
/**
|
|
939
|
-
* Converts a multi-locale translations map to a single-locale hashed version,
|
|
940
|
-
* used for storage in in the DB.
|
|
941
|
-
*
|
|
942
|
-
* ```
|
|
943
|
-
* const multiLocaleStrings = {
|
|
944
|
-
* 'one': {es: 'uno', fr: 'un'},
|
|
945
|
-
* 'two': {es: 'dos', fr: 'deux'}
|
|
946
|
-
* };
|
|
947
|
-
* translationsDoc.toLocaleDocHashMap(multiLocaleStrings, 'es');
|
|
948
|
-
* // =>
|
|
949
|
-
* // {
|
|
950
|
-
* // "<hash1>": {"source": "one", "translation": "uno"},
|
|
951
|
-
* // "<hash2>": {"source": "two", "translation": "dos"},
|
|
952
|
-
* // }
|
|
953
|
-
* ```
|
|
954
|
-
*
|
|
955
|
-
* One reason for using hashes is because the DB has limits on the number of
|
|
956
|
-
* chars that can be used as the "key" in a object map.
|
|
957
|
-
*/
|
|
958
|
-
toLocaleDocHashMap(multiLocaleStrings, locale) {
|
|
959
|
-
const hashMap = {};
|
|
960
|
-
Object.entries(multiLocaleStrings).forEach(([source, translations]) => {
|
|
961
|
-
const translation = translations[locale];
|
|
962
|
-
if (translation) {
|
|
963
|
-
const hash = hashStr(source);
|
|
964
|
-
hashMap[hash] = { source, translation };
|
|
965
|
-
}
|
|
966
|
-
});
|
|
967
|
-
return hashMap;
|
|
968
|
-
}
|
|
969
|
-
/**
|
|
970
|
-
* Import translations from the v1 system to the TranslationsManager.
|
|
971
|
-
*/
|
|
972
|
-
async importTranslationsFromV1() {
|
|
973
|
-
const projectId = this.cmsClient.projectId;
|
|
974
|
-
const db = this.cmsClient.db;
|
|
975
|
-
const dbPath = `Projects/${projectId}/Translations`;
|
|
976
|
-
const query = db.collection(dbPath);
|
|
977
|
-
const querySnapshot = await query.get();
|
|
978
|
-
if (querySnapshot.size === 0) {
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
console.log(
|
|
982
|
-
"[root cms] importing v1 Translations to v2 TranslationsManager"
|
|
983
|
-
);
|
|
984
|
-
const translationsDocs = {};
|
|
985
|
-
querySnapshot.forEach((doc) => {
|
|
986
|
-
const translation = doc.data();
|
|
987
|
-
const source = this.cmsClient.normalizeString(translation.source);
|
|
988
|
-
delete translation.source;
|
|
989
|
-
const tags = translation.tags || [];
|
|
990
|
-
delete translation.tags;
|
|
991
|
-
for (const tag of tags) {
|
|
992
|
-
if (tag.includes("/")) {
|
|
993
|
-
const translationsId = tag;
|
|
994
|
-
translationsDocs[translationsId] ??= {
|
|
995
|
-
id: translationsId,
|
|
996
|
-
tags,
|
|
997
|
-
strings: {}
|
|
998
|
-
};
|
|
999
|
-
translationsDocs[translationsId].strings[source] = translation;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
});
|
|
1003
|
-
if (Object.keys(translationsDocs).length === 0) {
|
|
1004
|
-
console.log("[root cms] no v1 translations to save");
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
for (const docId in translationsDocs) {
|
|
1008
|
-
const [collection, slug] = docId.split("/");
|
|
1009
|
-
if (collection && slug) {
|
|
1010
|
-
const doc = await this.cmsClient.getDoc(collection, slug, {
|
|
1011
|
-
mode: "draft"
|
|
1012
|
-
});
|
|
1013
|
-
const linkedSheet = doc?.sys?.l10nSheet;
|
|
1014
|
-
if (linkedSheet) {
|
|
1015
|
-
translationsDocs[docId].sys.linkedSheet = linkedSheet;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
Object.entries(translationsDocs).forEach(([translationsId, data]) => {
|
|
1020
|
-
const len = Object.keys(data.strings).length;
|
|
1021
|
-
console.log(`[root cms] saving ${len} string(s) to ${translationsId}...`);
|
|
1022
|
-
this.saveTranslations(translationsId, data.strings, {
|
|
1023
|
-
tags: data.tags || [translationsId]
|
|
1024
|
-
});
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
|
-
};
|
|
1028
|
-
function buildTranslationsDbPath(options) {
|
|
1029
|
-
return TRANSLATIONS_DB_PATH_FORMAT.replace(
|
|
1030
|
-
"{project}",
|
|
1031
|
-
options.project
|
|
1032
|
-
).replace("{mode}", options.mode);
|
|
1033
|
-
}
|
|
1034
|
-
function buildTranslationsLocaleDocDbPath(options) {
|
|
1035
|
-
return TRANSLATIONS_LOCALE_DOC_DB_PATH_FORMAT.replace(
|
|
1036
|
-
"{project}",
|
|
1037
|
-
options.project
|
|
1038
|
-
).replace("{mode}", options.mode).replace("{id}", options.id.replaceAll("/", "--")).replace("{locale}", options.locale);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// core/client.ts
|
|
1042
|
-
var RootCMSClient = class {
|
|
1043
|
-
constructor(rootConfig) {
|
|
1044
|
-
this.rootConfig = rootConfig;
|
|
1045
|
-
this.cmsPlugin = getCmsPlugin(this.rootConfig);
|
|
1046
|
-
const cmsPluginOptions = this.cmsPlugin.getConfig();
|
|
1047
|
-
this.projectId = cmsPluginOptions.id || "default";
|
|
1048
|
-
this.app = this.cmsPlugin.getFirebaseApp();
|
|
1049
|
-
this.db = this.cmsPlugin.getFirestore();
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* Retrieves doc data from Root.js CMS.
|
|
1053
|
-
*/
|
|
1054
|
-
async getDoc(collectionId, slug, options) {
|
|
1055
|
-
const rawData = await this.getRawDoc(collectionId, slug, options);
|
|
1056
|
-
if (rawData) {
|
|
1057
|
-
return unmarshalData(rawData);
|
|
1058
|
-
}
|
|
1059
|
-
return null;
|
|
1060
|
-
}
|
|
1061
|
-
/**
|
|
1062
|
-
* Retrieves raw doc data as stored in the database. Only use this if you know
|
|
1063
|
-
* what you are doing.
|
|
1064
|
-
*/
|
|
1065
|
-
async getRawDoc(collectionId, slug, options) {
|
|
1066
|
-
const mode = options.mode;
|
|
1067
|
-
const modeCollection = mode === "draft" ? "Drafts" : "Published";
|
|
1068
|
-
slug = slug.replaceAll("/", "--");
|
|
1069
|
-
const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}/${slug}`;
|
|
1070
|
-
const docRef = this.db.doc(dbPath);
|
|
1071
|
-
const doc = await docRef.get();
|
|
1072
|
-
if (doc.exists) {
|
|
1073
|
-
return doc.data();
|
|
1074
|
-
}
|
|
1075
|
-
return null;
|
|
1076
|
-
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Firestore path for a collection.
|
|
1079
|
-
*/
|
|
1080
|
-
dbCollectionDocsPath(collectionId, options) {
|
|
1081
|
-
let modeCollection = "";
|
|
1082
|
-
if (options.mode === "draft") {
|
|
1083
|
-
modeCollection = "Drafts";
|
|
1084
|
-
} else if (options.mode === "published") {
|
|
1085
|
-
modeCollection = "Published";
|
|
1086
|
-
} else {
|
|
1087
|
-
throw new Error(`unknown mode: ${options.mode}`);
|
|
1088
|
-
}
|
|
1089
|
-
return `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}`;
|
|
1090
|
-
}
|
|
1091
|
-
/**
|
|
1092
|
-
* Firestore path for a content doc.
|
|
1093
|
-
*/
|
|
1094
|
-
dbDocPath(collectionId, slug, options) {
|
|
1095
|
-
const collectionDocsPath = this.dbCollectionDocsPath(collectionId, options);
|
|
1096
|
-
const normalizedSlug = slug.replaceAll("/", "--");
|
|
1097
|
-
return `${collectionDocsPath}/${normalizedSlug}`;
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Firestore doc ref for a content doc.
|
|
1101
|
-
*/
|
|
1102
|
-
dbDocRef(collectionId, slug, options) {
|
|
1103
|
-
const docPath = this.dbDocPath(collectionId, slug, options);
|
|
1104
|
-
return this.db.doc(docPath);
|
|
1105
|
-
}
|
|
1106
|
-
/**
|
|
1107
|
-
* Saves draft data to a doc.
|
|
1108
|
-
*
|
|
1109
|
-
* Note: this saves data to the "fields" attr of the draft doc. If you need to
|
|
1110
|
-
* modify the sys-level attributes of the doc, use `setRawDoc()`.
|
|
1111
|
-
*/
|
|
1112
|
-
async saveDraftData(docId, fieldsData, options) {
|
|
1113
|
-
const { collection, slug } = parseDocId(docId);
|
|
1114
|
-
const draftDoc = await this.getRawDoc(collection, slug, { mode: "draft" }) || {};
|
|
1115
|
-
const draftSys = draftDoc.sys || {};
|
|
1116
|
-
const modifiedBy = options?.modifiedBy || "root-cms-client";
|
|
1117
|
-
const fields = marshalData(fieldsData || {});
|
|
1118
|
-
const data = {
|
|
1119
|
-
id: docId,
|
|
1120
|
-
collection,
|
|
1121
|
-
slug,
|
|
1122
|
-
sys: {
|
|
1123
|
-
...draftSys,
|
|
1124
|
-
createdAt: draftSys.createdAt ?? Timestamp3.now(),
|
|
1125
|
-
createdBy: draftSys.createdBy ?? modifiedBy,
|
|
1126
|
-
modifiedAt: Timestamp3.now(),
|
|
1127
|
-
modifiedBy,
|
|
1128
|
-
locales: options?.locales ?? draftSys.locales ?? ["en"]
|
|
1129
|
-
},
|
|
1130
|
-
fields
|
|
1131
|
-
};
|
|
1132
|
-
await this.setRawDoc(collection, slug, data, { mode: "draft" });
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Prefer `saveDraftData('Pages/foo', data)`. Only use this if you know what
|
|
1136
|
-
* you're doing.
|
|
1137
|
-
*/
|
|
1138
|
-
async setRawDoc(collectionId, slug, data, options) {
|
|
1139
|
-
const mode = options.mode;
|
|
1140
|
-
const modeCollection = mode === "draft" ? "Drafts" : "Published";
|
|
1141
|
-
const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}/${slug}`;
|
|
1142
|
-
const docRef = this.db.doc(dbPath);
|
|
1143
|
-
await docRef.set(data);
|
|
1144
|
-
}
|
|
1145
|
-
/**
|
|
1146
|
-
* Lists docs from a Root.js CMS collection.
|
|
1147
|
-
*/
|
|
1148
|
-
async listDocs(collectionId, options) {
|
|
1149
|
-
const mode = options.mode;
|
|
1150
|
-
const modeCollection = mode === "draft" ? "Drafts" : "Published";
|
|
1151
|
-
const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}`;
|
|
1152
|
-
let query = this.db.collection(dbPath);
|
|
1153
|
-
if (options.limit) {
|
|
1154
|
-
query = query.limit(options.limit);
|
|
1155
|
-
}
|
|
1156
|
-
if (options.offset) {
|
|
1157
|
-
query = query.offset(options.offset);
|
|
1158
|
-
}
|
|
1159
|
-
if (options.orderBy) {
|
|
1160
|
-
query = query.orderBy(options.orderBy, options.orderByDirection);
|
|
1161
|
-
}
|
|
1162
|
-
if (options.query) {
|
|
1163
|
-
query = options.query(query);
|
|
1164
|
-
}
|
|
1165
|
-
const results = await query.get();
|
|
1166
|
-
const docs = [];
|
|
1167
|
-
results.forEach((result) => {
|
|
1168
|
-
if (options.raw) {
|
|
1169
|
-
const rawDoc = result.data();
|
|
1170
|
-
docs.push(rawDoc);
|
|
1171
|
-
} else {
|
|
1172
|
-
const doc = unmarshalData(result.data());
|
|
1173
|
-
docs.push(doc);
|
|
1174
|
-
}
|
|
1175
|
-
});
|
|
1176
|
-
return { docs };
|
|
1177
|
-
}
|
|
1178
|
-
/**
|
|
1179
|
-
* Returns the number of docs in a Root.js CMS collection.
|
|
1180
|
-
*/
|
|
1181
|
-
async getDocsCount(collectionId, options) {
|
|
1182
|
-
const mode = options.mode;
|
|
1183
|
-
const modeCollection = mode === "draft" ? "Drafts" : "Published";
|
|
1184
|
-
const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}`;
|
|
1185
|
-
let query = this.db.collection(dbPath);
|
|
1186
|
-
if (options.query) {
|
|
1187
|
-
query = options.query(query);
|
|
1188
|
-
}
|
|
1189
|
-
const results = await query.count().get();
|
|
1190
|
-
const count = results.data().count;
|
|
1191
|
-
return count;
|
|
1192
|
-
}
|
|
1193
|
-
/**
|
|
1194
|
-
* Batch publishes a set of docs by id.
|
|
1195
|
-
*/
|
|
1196
|
-
async publishDocs(docIds, options) {
|
|
1197
|
-
const projectCollectionsPath = `Projects/${this.projectId}/Collections`;
|
|
1198
|
-
const publishedBy = options?.publishedBy || "root-cms-client";
|
|
1199
|
-
const docRefs = docIds.map((docId) => {
|
|
1200
|
-
const [collection, slug] = docId.split("/");
|
|
1201
|
-
if (!collection || !slug) {
|
|
1202
|
-
throw new Error(`invalid doc id: ${docId}`);
|
|
1203
|
-
}
|
|
1204
|
-
const docRef = this.db.doc(
|
|
1205
|
-
`${projectCollectionsPath}/${collection}/Drafts/${slug}`
|
|
1206
|
-
);
|
|
1207
|
-
return docRef;
|
|
1208
|
-
});
|
|
1209
|
-
const docSnapshots = await this.db.getAll(...docRefs);
|
|
1210
|
-
const docs = docSnapshots.map((snapshot) => snapshot.data()).filter((d) => !!d);
|
|
1211
|
-
if (docs.length === 0) {
|
|
1212
|
-
console.log("no docs to publish");
|
|
1213
|
-
return [];
|
|
1214
|
-
}
|
|
1215
|
-
for (const doc of docs) {
|
|
1216
|
-
if (this.testPublishingLocked(doc)) {
|
|
1217
|
-
throw new Error(`publishing is locked for doc: ${doc.id}`);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
let batchCount = 0;
|
|
1221
|
-
const batch = options?.batch || this.db.batch();
|
|
1222
|
-
const versionTags = ["published"];
|
|
1223
|
-
if (options?.releaseId) {
|
|
1224
|
-
versionTags.push(`release:${options.releaseId}`);
|
|
1225
|
-
}
|
|
1226
|
-
const publishedDocs = [];
|
|
1227
|
-
for (const doc of docs) {
|
|
1228
|
-
const { id, collection, slug, sys, fields } = doc;
|
|
1229
|
-
const draftRef = this.db.doc(
|
|
1230
|
-
`${projectCollectionsPath}/${collection}/Drafts/${slug}`
|
|
1231
|
-
);
|
|
1232
|
-
const scheduledRef = this.db.doc(
|
|
1233
|
-
`${projectCollectionsPath}/${collection}/Scheduled/${slug}`
|
|
1234
|
-
);
|
|
1235
|
-
const publishedRef = this.db.doc(
|
|
1236
|
-
`${projectCollectionsPath}/${collection}/Published/${slug}`
|
|
1237
|
-
);
|
|
1238
|
-
const firstPublishedAt = sys.firstPublishedAt ?? FieldValue2.serverTimestamp();
|
|
1239
|
-
const firstPublishedBy = sys.firstPublishedBy ?? publishedBy;
|
|
1240
|
-
batch.set(publishedRef, {
|
|
1241
|
-
id,
|
|
1242
|
-
collection,
|
|
1243
|
-
slug,
|
|
1244
|
-
fields: fields || {},
|
|
1245
|
-
sys: {
|
|
1246
|
-
...sys,
|
|
1247
|
-
firstPublishedAt,
|
|
1248
|
-
firstPublishedBy,
|
|
1249
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
1250
|
-
publishedBy
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
batchCount += 1;
|
|
1254
|
-
const versionRef = this.db.doc(
|
|
1255
|
-
`${projectCollectionsPath}/${collection}/Drafts/${slug}/Versions/${Date.now()}`
|
|
1256
|
-
);
|
|
1257
|
-
const versionData = {
|
|
1258
|
-
id,
|
|
1259
|
-
collection,
|
|
1260
|
-
slug,
|
|
1261
|
-
fields: fields || {},
|
|
1262
|
-
sys: {
|
|
1263
|
-
...sys,
|
|
1264
|
-
firstPublishedAt,
|
|
1265
|
-
firstPublishedBy,
|
|
1266
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
1267
|
-
publishedBy
|
|
1268
|
-
}
|
|
1269
|
-
};
|
|
1270
|
-
if (versionTags.length) {
|
|
1271
|
-
versionData.tags = versionTags;
|
|
1272
|
-
}
|
|
1273
|
-
batch.set(versionRef, versionData);
|
|
1274
|
-
batchCount += 1;
|
|
1275
|
-
batch.delete(scheduledRef);
|
|
1276
|
-
batchCount += 1;
|
|
1277
|
-
batch.update(draftRef, {
|
|
1278
|
-
"sys.scheduledAt": FieldValue2.delete(),
|
|
1279
|
-
"sys.scheduledBy": FieldValue2.delete(),
|
|
1280
|
-
"sys.firstPublishedAt": firstPublishedAt,
|
|
1281
|
-
"sys.firstPublishedBy": firstPublishedBy,
|
|
1282
|
-
"sys.publishedAt": FieldValue2.serverTimestamp(),
|
|
1283
|
-
"sys.publishedBy": publishedBy
|
|
1284
|
-
});
|
|
1285
|
-
batchCount += 1;
|
|
1286
|
-
publishedDocs.push(doc);
|
|
1287
|
-
if (batchCount >= 400) {
|
|
1288
|
-
await batch.commit();
|
|
1289
|
-
batchCount = 0;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
if (batchCount > 0) {
|
|
1293
|
-
await batch.commit();
|
|
1294
|
-
}
|
|
1295
|
-
console.log(`published ${publishedDocs.length} docs!`);
|
|
1296
|
-
return publishedDocs;
|
|
1297
|
-
}
|
|
1298
|
-
/**
|
|
1299
|
-
* Publishes scheduled docs.
|
|
1300
|
-
*/
|
|
1301
|
-
async publishScheduledDocs() {
|
|
1302
|
-
const projectCollectionsPath = `Projects/${this.projectId}/Collections`;
|
|
1303
|
-
const now = Math.ceil((/* @__PURE__ */ new Date()).getTime());
|
|
1304
|
-
const snapshot = await this.db.collectionGroup("Scheduled").get();
|
|
1305
|
-
const docs = snapshot.docs.filter((d) => {
|
|
1306
|
-
if (!d.ref.path.startsWith(projectCollectionsPath)) {
|
|
1307
|
-
return false;
|
|
1308
|
-
}
|
|
1309
|
-
const data = d.data() || {};
|
|
1310
|
-
const scheduledAt = data.sys?.scheduledAt;
|
|
1311
|
-
return scheduledAt && scheduledAt.toMillis && scheduledAt.toMillis() <= now;
|
|
1312
|
-
}).map((d) => {
|
|
1313
|
-
const dbPath = d.ref.path;
|
|
1314
|
-
const segments = dbPath.split("/");
|
|
1315
|
-
const slug = segments.at(-1);
|
|
1316
|
-
const collection = segments.at(-3);
|
|
1317
|
-
const id = `${collection}/${slug}`;
|
|
1318
|
-
return {
|
|
1319
|
-
data: d.data(),
|
|
1320
|
-
id,
|
|
1321
|
-
collection,
|
|
1322
|
-
slug
|
|
1323
|
-
};
|
|
1324
|
-
});
|
|
1325
|
-
if (docs.length === 0) {
|
|
1326
|
-
console.log("no docs to schedule");
|
|
1327
|
-
return [];
|
|
1328
|
-
}
|
|
1329
|
-
let batchCount = 0;
|
|
1330
|
-
const batch = this.db.batch();
|
|
1331
|
-
const versionTags = ["published"];
|
|
1332
|
-
const publishedDocs = [];
|
|
1333
|
-
for (const doc of docs) {
|
|
1334
|
-
const { id, collection, slug, data } = doc;
|
|
1335
|
-
const draftRef = this.db.doc(
|
|
1336
|
-
`${projectCollectionsPath}/${collection}/Drafts/${slug}`
|
|
1337
|
-
);
|
|
1338
|
-
const scheduledRef = this.db.doc(
|
|
1339
|
-
`${projectCollectionsPath}/${collection}/Scheduled/${slug}`
|
|
1340
|
-
);
|
|
1341
|
-
const publishedRef = this.db.doc(
|
|
1342
|
-
`${projectCollectionsPath}/${collection}/Published/${slug}`
|
|
1343
|
-
);
|
|
1344
|
-
const { scheduledAt, scheduledBy, ...sys } = data.sys || {};
|
|
1345
|
-
const firstPublishedAt = sys.firstPublishedAt ?? scheduledAt;
|
|
1346
|
-
const firstPublishedBy = sys.firstPublishedBy ?? (scheduledBy || "root-cms-client");
|
|
1347
|
-
batch.set(publishedRef, {
|
|
1348
|
-
id,
|
|
1349
|
-
collection,
|
|
1350
|
-
slug,
|
|
1351
|
-
fields: data.fields || {},
|
|
1352
|
-
sys: {
|
|
1353
|
-
...sys,
|
|
1354
|
-
firstPublishedAt,
|
|
1355
|
-
firstPublishedBy,
|
|
1356
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
1357
|
-
publishedBy: scheduledBy || ""
|
|
1358
|
-
}
|
|
1359
|
-
});
|
|
1360
|
-
batchCount += 1;
|
|
1361
|
-
const versionRef = this.db.doc(
|
|
1362
|
-
`${projectCollectionsPath}/${collection}/Drafts/${slug}/Versions/${Date.now()}`
|
|
1363
|
-
);
|
|
1364
|
-
const versionData = {
|
|
1365
|
-
id,
|
|
1366
|
-
collection,
|
|
1367
|
-
slug,
|
|
1368
|
-
fields: data.fields || {},
|
|
1369
|
-
sys: {
|
|
1370
|
-
...sys,
|
|
1371
|
-
firstPublishedAt,
|
|
1372
|
-
firstPublishedBy,
|
|
1373
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
1374
|
-
publishedBy: scheduledBy || ""
|
|
1375
|
-
},
|
|
1376
|
-
tags: versionTags
|
|
1377
|
-
};
|
|
1378
|
-
batch.set(versionRef, versionData);
|
|
1379
|
-
batchCount += 1;
|
|
1380
|
-
batch.delete(scheduledRef);
|
|
1381
|
-
batchCount += 1;
|
|
1382
|
-
batch.update(draftRef, {
|
|
1383
|
-
"sys.scheduledAt": FieldValue2.delete(),
|
|
1384
|
-
"sys.scheduledBy": FieldValue2.delete(),
|
|
1385
|
-
"sys.firstPublishedAt": firstPublishedAt,
|
|
1386
|
-
"sys.firstPublishedBy": firstPublishedBy,
|
|
1387
|
-
"sys.publishedAt": FieldValue2.serverTimestamp(),
|
|
1388
|
-
"sys.publishedBy": scheduledBy || "root-cms-client"
|
|
1389
|
-
});
|
|
1390
|
-
batchCount += 1;
|
|
1391
|
-
publishedDocs.push(doc);
|
|
1392
|
-
if (batchCount >= 400) {
|
|
1393
|
-
await batch.commit();
|
|
1394
|
-
batchCount = 0;
|
|
1395
|
-
continue;
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
if (batchCount > 0) {
|
|
1399
|
-
await batch.commit();
|
|
1400
|
-
}
|
|
1401
|
-
console.log(`published ${publishedDocs.length} docs!`);
|
|
1402
|
-
return publishedDocs;
|
|
1403
|
-
}
|
|
1404
|
-
/**
|
|
1405
|
-
* Publishes docs in scheduled releases.
|
|
1406
|
-
*/
|
|
1407
|
-
async publishScheduledReleases() {
|
|
1408
|
-
const releasesPath = `Projects/${this.projectId}/Releases`;
|
|
1409
|
-
const now = Math.ceil((/* @__PURE__ */ new Date()).getTime());
|
|
1410
|
-
const query = this.db.collection(releasesPath).where("scheduledAt", "<=", Timestamp3.fromMillis(now));
|
|
1411
|
-
const querySnapshot = await query.get();
|
|
1412
|
-
for (const snapshot of querySnapshot.docs) {
|
|
1413
|
-
const release = snapshot.data();
|
|
1414
|
-
const batch = this.db.batch();
|
|
1415
|
-
const publishedBy = release.scheduledBy || "root-cms-client";
|
|
1416
|
-
batch.update(snapshot.ref, {
|
|
1417
|
-
publishedAt: Timestamp3.now(),
|
|
1418
|
-
publishedBy,
|
|
1419
|
-
scheduledAt: FieldValue2.delete(),
|
|
1420
|
-
scheduledBy: FieldValue2.delete()
|
|
1421
|
-
});
|
|
1422
|
-
const dataSourceIds = release.dataSourceIds || [];
|
|
1423
|
-
if (dataSourceIds.length > 0) {
|
|
1424
|
-
await this.publishDataSources(dataSourceIds, {
|
|
1425
|
-
publishedBy,
|
|
1426
|
-
batch,
|
|
1427
|
-
commitBatch: false
|
|
1428
|
-
});
|
|
1429
|
-
}
|
|
1430
|
-
await this.publishDocs(release.docIds || [], {
|
|
1431
|
-
publishedBy,
|
|
1432
|
-
batch,
|
|
1433
|
-
releaseId: release.id
|
|
1434
|
-
});
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
/**
|
|
1438
|
-
* Checks if a doc is currently "locked" for publishing.
|
|
1439
|
-
*/
|
|
1440
|
-
testPublishingLocked(doc) {
|
|
1441
|
-
if (doc.sys?.publishingLocked) {
|
|
1442
|
-
if (doc.sys.publishingLocked.until) {
|
|
1443
|
-
const now = Timestamp3.now().toMillis();
|
|
1444
|
-
const until = doc.sys.publishingLocked.until.toMillis();
|
|
1445
|
-
return now < until;
|
|
1446
|
-
}
|
|
1447
|
-
return true;
|
|
1448
|
-
}
|
|
1449
|
-
return false;
|
|
1450
|
-
}
|
|
1451
|
-
/**
|
|
1452
|
-
* Returns a `TranslationsManager` object for managing translations.
|
|
1453
|
-
*
|
|
1454
|
-
* To get translations:
|
|
1455
|
-
* ```
|
|
1456
|
-
* await tm.loadTranslations({
|
|
1457
|
-
* ids: ['Global/strings', 'Pages/index'],
|
|
1458
|
-
* locales: ['es'],
|
|
1459
|
-
* });
|
|
1460
|
-
* ```
|
|
1461
|
-
*
|
|
1462
|
-
* NOTE: The `TranslationsManager` is a v2 feature that will eventually
|
|
1463
|
-
* replace the v1 translations system.
|
|
1464
|
-
*/
|
|
1465
|
-
getTranslationsManager() {
|
|
1466
|
-
const cmsPluginOptions = this.cmsPlugin.getConfig();
|
|
1467
|
-
if (cmsPluginOptions.experiments?.v2TranslationsManager) {
|
|
1468
|
-
throw new Error(
|
|
1469
|
-
"`v2TranslationsManager` is not enabled. update root.config.ts and add: `{experiments: {v2TranslationsManager: true}}`"
|
|
1470
|
-
);
|
|
1471
|
-
}
|
|
1472
|
-
return new TranslationsManager(this);
|
|
1473
|
-
}
|
|
1474
|
-
/**
|
|
1475
|
-
* Loads translations saved in the translations collection, optionally
|
|
1476
|
-
* filtered by tag.
|
|
1477
|
-
*
|
|
1478
|
-
* Returns a map like:
|
|
1479
|
-
* ```
|
|
1480
|
-
* {
|
|
1481
|
-
* "<hash>": {"source": "Hello", "es": "Hola", "fr": "Bonjour"},
|
|
1482
|
-
* }
|
|
1483
|
-
* ```
|
|
1484
|
-
*/
|
|
1485
|
-
async loadTranslations(options) {
|
|
1486
|
-
const dbPath = `Projects/${this.projectId}/Translations`;
|
|
1487
|
-
let query = this.db.collection(dbPath);
|
|
1488
|
-
if (options?.tags) {
|
|
1489
|
-
query = query.where("tags", "array-contains-any", options.tags);
|
|
1490
|
-
}
|
|
1491
|
-
const querySnapshot = await query.get();
|
|
1492
|
-
const translationsMap = {};
|
|
1493
|
-
querySnapshot.forEach((doc) => {
|
|
1494
|
-
const hash = doc.id;
|
|
1495
|
-
translationsMap[hash] = doc.data();
|
|
1496
|
-
});
|
|
1497
|
-
return translationsMap;
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Saves a map of translations, e.g.:
|
|
1501
|
-
* ```
|
|
1502
|
-
* await client.saveTranslations({
|
|
1503
|
-
* "Hello": {"es": "Hola", "fr": "Bonjour"},
|
|
1504
|
-
* });
|
|
1505
|
-
* ```
|
|
1506
|
-
*/
|
|
1507
|
-
async saveTranslations(translations, tags) {
|
|
1508
|
-
const translationsPath = `Projects/${this.projectId}/Translations`;
|
|
1509
|
-
const batch = this.db.batch();
|
|
1510
|
-
let batchCount = 0;
|
|
1511
|
-
Object.entries(translations).forEach(([source, sourceTranslations]) => {
|
|
1512
|
-
const hash = this.getTranslationKey(source);
|
|
1513
|
-
const translationRef = this.db.doc(`${translationsPath}/${hash}`);
|
|
1514
|
-
const data = {
|
|
1515
|
-
...sourceTranslations,
|
|
1516
|
-
source: this.normalizeString(source)
|
|
1517
|
-
};
|
|
1518
|
-
if (tags) {
|
|
1519
|
-
data.tags = FieldValue2.arrayUnion(...tags);
|
|
1520
|
-
}
|
|
1521
|
-
batch.set(translationRef, data, { merge: true });
|
|
1522
|
-
batchCount += 1;
|
|
1523
|
-
});
|
|
1524
|
-
if (batchCount > 500) {
|
|
1525
|
-
throw new Error("up to 500 translations can be saved at a time.");
|
|
1526
|
-
}
|
|
1527
|
-
await batch.commit();
|
|
1528
|
-
}
|
|
1529
|
-
/**
|
|
1530
|
-
* Returns the "key" used for a translation as stored in the db. Translations
|
|
1531
|
-
* are stored under `Projects/<project id>/Translations/<sha1 hash>`.
|
|
1532
|
-
*/
|
|
1533
|
-
getTranslationKey(source) {
|
|
1534
|
-
const sha1 = crypto2.createHash("sha1").update(this.normalizeString(source)).digest("hex");
|
|
1535
|
-
return sha1;
|
|
1536
|
-
}
|
|
1537
|
-
/**
|
|
1538
|
-
* Cleans a string that's used for translations. Performs the following:
|
|
1539
|
-
* - Removes any leading/trailing whitespace
|
|
1540
|
-
* - Removes spaces at the end of any line
|
|
1541
|
-
*/
|
|
1542
|
-
normalizeString(str) {
|
|
1543
|
-
const lines = String(str).trim().split("\n").map((line) => line.trimEnd());
|
|
1544
|
-
return lines.join("\n");
|
|
1545
|
-
}
|
|
1546
|
-
/**
|
|
1547
|
-
* Loads translations for a particular locale.
|
|
1548
|
-
*
|
|
1549
|
-
* Returns a map like:
|
|
1550
|
-
* ```
|
|
1551
|
-
* {
|
|
1552
|
-
* "Hello": "Bonjour",
|
|
1553
|
-
* }
|
|
1554
|
-
* ```
|
|
1555
|
-
*/
|
|
1556
|
-
async loadTranslationsForLocale(locale, options) {
|
|
1557
|
-
const translationsMap = await this.loadTranslations(options);
|
|
1558
|
-
return translationsForLocale(translationsMap, locale);
|
|
1559
|
-
}
|
|
1560
|
-
/**
|
|
1561
|
-
* Firestore path for a translations file.
|
|
1562
|
-
*/
|
|
1563
|
-
dbTranslationsPath(translationsId, options) {
|
|
1564
|
-
const mode = options.mode;
|
|
1565
|
-
if (!(mode === "draft" || mode === "published")) {
|
|
1566
|
-
throw new Error(`invalid mode: ${mode}`);
|
|
1567
|
-
}
|
|
1568
|
-
const slug = translationsId.replaceAll("/", "--");
|
|
1569
|
-
const dbPath = `Projects/${this.projectId}/TranslationsManager/${mode}/Translations/${slug}`;
|
|
1570
|
-
return dbPath;
|
|
1571
|
-
}
|
|
1572
|
-
/**
|
|
1573
|
-
* Firestore doc ref for a translations file.
|
|
1574
|
-
*/
|
|
1575
|
-
dbTranslationsRef(translationsId, options) {
|
|
1576
|
-
const dbPath = this.dbTranslationsPath(translationsId, options);
|
|
1577
|
-
return this.db.doc(dbPath);
|
|
1578
|
-
}
|
|
1579
|
-
/**
|
|
1580
|
-
* Returns a data source configuration object.
|
|
1581
|
-
*/
|
|
1582
|
-
async getDataSource(dataSourceId) {
|
|
1583
|
-
const dbPath = `Projects/${this.projectId}/DataSources/${dataSourceId}`;
|
|
1584
|
-
const docRef = this.db.doc(dbPath);
|
|
1585
|
-
const doc = await docRef.get();
|
|
1586
|
-
if (doc.exists) {
|
|
1587
|
-
return doc.data();
|
|
1588
|
-
}
|
|
1589
|
-
return null;
|
|
1590
|
-
}
|
|
1591
|
-
/**
|
|
1592
|
-
* Syncs a data source to draft state.
|
|
1593
|
-
*/
|
|
1594
|
-
async syncDataSource(dataSourceId, options) {
|
|
1595
|
-
const dataSource = await this.getDataSource(dataSourceId);
|
|
1596
|
-
if (!dataSource) {
|
|
1597
|
-
throw new Error(`data source not found: ${dataSourceId}`);
|
|
1598
|
-
}
|
|
1599
|
-
const data = await this.fetchData(dataSource);
|
|
1600
|
-
const dataSourceDocRef = this.db.doc(
|
|
1601
|
-
`Projects/${this.projectId}/DataSources/${dataSourceId}`
|
|
1602
|
-
);
|
|
1603
|
-
const dataDocRef = this.db.doc(
|
|
1604
|
-
`Projects/${this.projectId}/DataSources/${dataSourceId}/Data/draft`
|
|
1605
|
-
);
|
|
1606
|
-
const syncedBy = options?.syncedBy || "root-cms-client";
|
|
1607
|
-
const updatedDataSource = {
|
|
1608
|
-
...dataSource,
|
|
1609
|
-
syncedAt: Timestamp3.now(),
|
|
1610
|
-
syncedBy
|
|
1611
|
-
};
|
|
1612
|
-
const batch = this.db.batch();
|
|
1613
|
-
batch.set(dataDocRef, {
|
|
1614
|
-
dataSource: updatedDataSource,
|
|
1615
|
-
data
|
|
1616
|
-
});
|
|
1617
|
-
batch.update(dataSourceDocRef, {
|
|
1618
|
-
syncedAt: Timestamp3.now(),
|
|
1619
|
-
syncedBy
|
|
1620
|
-
});
|
|
1621
|
-
await batch.commit();
|
|
1622
|
-
console.log(`synced data source: ${dataSourceId}`);
|
|
1623
|
-
console.log(`synced by: ${syncedBy}`);
|
|
1624
|
-
}
|
|
1625
|
-
async publishDataSource(dataSourceId, options) {
|
|
1626
|
-
const dataSource = await this.getDataSource(dataSourceId);
|
|
1627
|
-
if (!dataSource) {
|
|
1628
|
-
throw new Error(`data source not found: ${dataSourceId}`);
|
|
1629
|
-
}
|
|
1630
|
-
const dataSourceDocRef = this.db.doc(
|
|
1631
|
-
`Projects/${this.projectId}/DataSources/${dataSourceId}`
|
|
1632
|
-
);
|
|
1633
|
-
const dataDocRefDraft = this.db.doc(
|
|
1634
|
-
`Projects/${this.projectId}/DataSources/${dataSourceId}/draft`
|
|
1635
|
-
);
|
|
1636
|
-
const dataDocRefPublished = this.db.doc(
|
|
1637
|
-
`Projects/${this.projectId}/DataSources/${dataSourceId}/published`
|
|
1638
|
-
);
|
|
1639
|
-
const dataRes = await this.getFromDataSource(dataSourceId, { mode: "draft" });
|
|
1640
|
-
const publishedBy = options?.publishedBy || "root-cms-client";
|
|
1641
|
-
const updatedDataSource = {
|
|
1642
|
-
...dataSource,
|
|
1643
|
-
publishedAt: Timestamp3.now(),
|
|
1644
|
-
publishedBy
|
|
1645
|
-
};
|
|
1646
|
-
const batch = this.db.batch();
|
|
1647
|
-
batch.set(dataDocRefPublished, {
|
|
1648
|
-
dataSource: updatedDataSource,
|
|
1649
|
-
data: dataRes?.data || null,
|
|
1650
|
-
...dataRes?.headers ? { headers: dataRes.headers } : {}
|
|
1651
|
-
});
|
|
1652
|
-
batch.update(dataDocRefDraft, {
|
|
1653
|
-
dataSource: updatedDataSource
|
|
1654
|
-
});
|
|
1655
|
-
batch.update(dataSourceDocRef, {
|
|
1656
|
-
publishedAt: Timestamp3.now(),
|
|
1657
|
-
publishedBy
|
|
1658
|
-
});
|
|
1659
|
-
await batch.commit();
|
|
1660
|
-
console.log(`published data ${dataSourceId}`);
|
|
1661
|
-
console.log(`published by: ${publishedBy}`);
|
|
1662
|
-
}
|
|
1663
|
-
async publishDataSources(dataSourceIds, options) {
|
|
1664
|
-
const publishedBy = options?.publishedBy || "root-cms-client";
|
|
1665
|
-
const batch = options?.batch || this.db.batch();
|
|
1666
|
-
for (const id of dataSourceIds) {
|
|
1667
|
-
const dataSource = await this.getDataSource(id);
|
|
1668
|
-
if (!dataSource) {
|
|
1669
|
-
throw new Error(`data source not found: ${id}`);
|
|
1670
|
-
}
|
|
1671
|
-
const dataSourceDocRef = this.db.doc(
|
|
1672
|
-
`Projects/${this.projectId}/DataSources/${id}`
|
|
1673
|
-
);
|
|
1674
|
-
const dataDocRefDraft = this.db.doc(
|
|
1675
|
-
`Projects/${this.projectId}/DataSources/${id}/draft`
|
|
1676
|
-
);
|
|
1677
|
-
const dataDocRefPublished = this.db.doc(
|
|
1678
|
-
`Projects/${this.projectId}/DataSources/${id}/published`
|
|
1679
|
-
);
|
|
1680
|
-
const dataRes = await this.getFromDataSource(id, { mode: "draft" });
|
|
1681
|
-
const updatedDataSource = {
|
|
1682
|
-
...dataSource,
|
|
1683
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
1684
|
-
publishedBy
|
|
1685
|
-
};
|
|
1686
|
-
batch.set(dataDocRefPublished, {
|
|
1687
|
-
dataSource: updatedDataSource,
|
|
1688
|
-
data: dataRes?.data || null,
|
|
1689
|
-
...dataRes?.headers ? { headers: dataRes.headers } : {}
|
|
1690
|
-
});
|
|
1691
|
-
batch.update(dataDocRefDraft, { dataSource: updatedDataSource });
|
|
1692
|
-
batch.update(dataSourceDocRef, {
|
|
1693
|
-
publishedAt: FieldValue2.serverTimestamp(),
|
|
1694
|
-
publishedBy
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
if (!options?.batch || options?.commitBatch) {
|
|
1698
|
-
await batch.commit();
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
async fetchData(dataSource) {
|
|
1702
|
-
if (dataSource.type === "http") {
|
|
1703
|
-
return await this.fetchHttpData(dataSource);
|
|
1704
|
-
}
|
|
1705
|
-
throw new Error(`unsupported data source: ${dataSource.type}`);
|
|
1706
|
-
}
|
|
1707
|
-
async fetchHttpData(dataSource) {
|
|
1708
|
-
const url = dataSource.url || "";
|
|
1709
|
-
if (!url.startsWith("https://")) {
|
|
1710
|
-
throw new Error(`url not supported: ${url}`);
|
|
1711
|
-
}
|
|
1712
|
-
const res = await fetch(url, {
|
|
1713
|
-
method: dataSource.httpOptions?.method || "GET",
|
|
1714
|
-
headers: dataSource.httpOptions?.headers || [],
|
|
1715
|
-
body: dataSource.httpOptions?.body || void 0
|
|
1716
|
-
});
|
|
1717
|
-
if (res.status !== 200) {
|
|
1718
|
-
const err = await res.text();
|
|
1719
|
-
throw new Error(`req failed: ${err}`);
|
|
1720
|
-
}
|
|
1721
|
-
const contentType = String(res.headers.get("content-type"));
|
|
1722
|
-
if (contentType.includes("application/json")) {
|
|
1723
|
-
return await res.json();
|
|
1724
|
-
}
|
|
1725
|
-
return res.text();
|
|
1726
|
-
}
|
|
1727
|
-
/**
|
|
1728
|
-
* Fetches data from a data source.
|
|
1729
|
-
*/
|
|
1730
|
-
async getFromDataSource(dataSourceId, options) {
|
|
1731
|
-
const mode = options?.mode || "published";
|
|
1732
|
-
if (!(mode === "draft" || mode === "published")) {
|
|
1733
|
-
throw new Error(`invalid mode: ${mode}`);
|
|
1734
|
-
}
|
|
1735
|
-
if (!dataSourceId || dataSourceId.includes("/")) {
|
|
1736
|
-
throw new Error(`invalid data source id: ${dataSourceId}`);
|
|
1737
|
-
}
|
|
1738
|
-
const docRef = this.dbDataSourceDataRef(dataSourceId, { mode });
|
|
1739
|
-
const doc = await docRef.get();
|
|
1740
|
-
if (doc.exists) {
|
|
1741
|
-
return doc.data();
|
|
1742
|
-
}
|
|
1743
|
-
return null;
|
|
1744
|
-
}
|
|
1745
|
-
/**
|
|
1746
|
-
* Firestore path for a datasource data.
|
|
1747
|
-
*/
|
|
1748
|
-
dbDataSourceDataPath(dataSourceId, options) {
|
|
1749
|
-
if (!dataSourceId || dataSourceId.includes("/")) {
|
|
1750
|
-
throw new Error(`invalid data source id: ${dataSourceId}`);
|
|
1751
|
-
}
|
|
1752
|
-
const mode = options.mode;
|
|
1753
|
-
if (!(mode === "draft" || mode === "published")) {
|
|
1754
|
-
throw new Error(`invalid mode: ${mode}`);
|
|
1755
|
-
}
|
|
1756
|
-
const dbPath = `Projects/${this.projectId}/DataSources/${dataSourceId}/Data/${mode}`;
|
|
1757
|
-
return dbPath;
|
|
1758
|
-
}
|
|
1759
|
-
/**
|
|
1760
|
-
* Firestore doc ref for a datasource data.
|
|
1761
|
-
*/
|
|
1762
|
-
dbDataSourceDataRef(dataSourceId, options) {
|
|
1763
|
-
const dbPath = this.dbDataSourceDataPath(dataSourceId, options);
|
|
1764
|
-
return this.db.doc(dbPath);
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Gets the user's role from the project's ACL.
|
|
1768
|
-
*/
|
|
1769
|
-
async getUserRole(email) {
|
|
1770
|
-
if (!email) {
|
|
1771
|
-
return null;
|
|
1772
|
-
}
|
|
1773
|
-
const docRef = this.db.doc(`Projects/${this.projectId}`);
|
|
1774
|
-
const snapshot = await docRef.get();
|
|
1775
|
-
const data = snapshot.data() || {};
|
|
1776
|
-
const acl = data.roles || {};
|
|
1777
|
-
if (email in acl) {
|
|
1778
|
-
return acl[email];
|
|
1779
|
-
}
|
|
1780
|
-
if (!email.includes("@")) {
|
|
1781
|
-
console.warn(`invalid email: ${email}`);
|
|
1782
|
-
return null;
|
|
1783
|
-
}
|
|
1784
|
-
const domain = email.split("@").at(-1);
|
|
1785
|
-
if (domain && `*@${domain}` in acl) {
|
|
1786
|
-
return acl[`*@${domain}`];
|
|
1787
|
-
}
|
|
1788
|
-
return null;
|
|
1789
|
-
}
|
|
1790
|
-
/**
|
|
1791
|
-
* Verifies user exists in the ACL list.
|
|
1792
|
-
*/
|
|
1793
|
-
async userExistsInAcl(email) {
|
|
1794
|
-
if (!email) {
|
|
1795
|
-
return false;
|
|
1796
|
-
}
|
|
1797
|
-
const docRef = this.db.doc(`Projects/${this.projectId}`);
|
|
1798
|
-
const snapshot = await docRef.get();
|
|
1799
|
-
const data = snapshot.data() || {};
|
|
1800
|
-
const acl = data.roles || {};
|
|
1801
|
-
if (email in acl) {
|
|
1802
|
-
return true;
|
|
1803
|
-
}
|
|
1804
|
-
if (!email.includes("@")) {
|
|
1805
|
-
console.warn(`invalid email: ${email}`);
|
|
1806
|
-
return false;
|
|
1807
|
-
}
|
|
1808
|
-
const domain = email.split("@").at(-1);
|
|
1809
|
-
if (domain && `*@${domain}` in acl) {
|
|
1810
|
-
return true;
|
|
1811
|
-
}
|
|
1812
|
-
return false;
|
|
1813
|
-
}
|
|
1814
|
-
async logAction(action, options) {
|
|
1815
|
-
if (!action) {
|
|
1816
|
-
throw new Error('missing required: "action"');
|
|
1817
|
-
}
|
|
1818
|
-
const data = {
|
|
1819
|
-
action,
|
|
1820
|
-
timestamp: Timestamp3.now(),
|
|
1821
|
-
by: options?.by || "system",
|
|
1822
|
-
metadata: options?.metadata || {}
|
|
1823
|
-
};
|
|
1824
|
-
if (options?.links) {
|
|
1825
|
-
data.links = options.links;
|
|
1826
|
-
}
|
|
1827
|
-
const colRef = this.db.collection(`Projects/${this.projectId}/ActionLogs`);
|
|
1828
|
-
await colRef.add(data);
|
|
1829
|
-
const metaStr = options?.metadata ? stringifyObj(options.metadata) : "";
|
|
1830
|
-
console.log(`[${data.timestamp.toMillis()}] action: ${action} ${metaStr}`);
|
|
1831
|
-
const cmsPluginConfig = this.cmsPlugin.getConfig();
|
|
1832
|
-
if (cmsPluginConfig.onAction) {
|
|
1833
|
-
try {
|
|
1834
|
-
cmsPluginConfig.onAction(data);
|
|
1835
|
-
} catch (err) {
|
|
1836
|
-
console.error(err);
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
/**
|
|
1841
|
-
* Creates a batch request that is capable of fetching one or more docs,
|
|
1842
|
-
* corresponding translations, and dataSources.
|
|
1843
|
-
*/
|
|
1844
|
-
createBatchRequest(options) {
|
|
1845
|
-
return new BatchRequest(this, options);
|
|
1846
|
-
}
|
|
1847
|
-
};
|
|
1848
|
-
function isRichTextData(data) {
|
|
1849
|
-
return Boolean(
|
|
1850
|
-
isObject(data) && Array.isArray(data.blocks) && data.time && data.version
|
|
1851
|
-
);
|
|
1852
|
-
}
|
|
1853
|
-
function getCmsPlugin(rootConfig) {
|
|
1854
|
-
const plugins = rootConfig.plugins || [];
|
|
1855
|
-
const plugin = plugins.find((plugin2) => plugin2.name === "root-cms");
|
|
1856
|
-
if (!plugin) {
|
|
1857
|
-
throw new Error("could not find root-cms plugin config in root.config.ts");
|
|
1858
|
-
}
|
|
1859
|
-
return plugin;
|
|
1860
|
-
}
|
|
1861
|
-
function marshalData(data) {
|
|
1862
|
-
if (isRichTextData(data)) {
|
|
1863
|
-
return data;
|
|
1864
|
-
}
|
|
1865
|
-
const result = {};
|
|
1866
|
-
for (const key in data) {
|
|
1867
|
-
const val = data[key];
|
|
1868
|
-
if (isObject(val)) {
|
|
1869
|
-
result[key] = marshalData(val);
|
|
1870
|
-
} else if (Array.isArray(val)) {
|
|
1871
|
-
if (val.length > 0 && val.some((item) => isObject(item))) {
|
|
1872
|
-
const items = val.map((item) => {
|
|
1873
|
-
if (isObject(item)) {
|
|
1874
|
-
return marshalData(item);
|
|
1875
|
-
}
|
|
1876
|
-
return item;
|
|
1877
|
-
});
|
|
1878
|
-
result[key] = toArrayObject(items);
|
|
1879
|
-
} else {
|
|
1880
|
-
result[key] = val;
|
|
1881
|
-
}
|
|
1882
|
-
} else {
|
|
1883
|
-
result[key] = val;
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
return result;
|
|
1887
|
-
}
|
|
1888
|
-
function unmarshalData(data) {
|
|
1889
|
-
const result = {};
|
|
1890
|
-
for (const key in data) {
|
|
1891
|
-
const val = data[key];
|
|
1892
|
-
if (isObject(val)) {
|
|
1893
|
-
if (val.toMillis) {
|
|
1894
|
-
result[key] = val.toMillis();
|
|
1895
|
-
} else if (Object.hasOwn(val, "_array") && Array.isArray(val._array)) {
|
|
1896
|
-
const arr = val._array.map((arrayKey) => {
|
|
1897
|
-
return {
|
|
1898
|
-
...unmarshalData(val[arrayKey] || {}),
|
|
1899
|
-
_arrayKey: arrayKey
|
|
1900
|
-
};
|
|
1901
|
-
});
|
|
1902
|
-
result[key] = arr;
|
|
1903
|
-
} else {
|
|
1904
|
-
result[key] = unmarshalData(val);
|
|
1905
|
-
}
|
|
1906
|
-
} else {
|
|
1907
|
-
result[key] = val;
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
return result;
|
|
1911
|
-
}
|
|
1912
|
-
function toArrayObject(arr) {
|
|
1913
|
-
if (!Array.isArray(arr)) {
|
|
1914
|
-
return arr;
|
|
1915
|
-
}
|
|
1916
|
-
const arrObject = { _array: [] };
|
|
1917
|
-
for (const item of arr) {
|
|
1918
|
-
const key = randString(6);
|
|
1919
|
-
arrObject[key] = item;
|
|
1920
|
-
arrObject._array.push(key);
|
|
1921
|
-
}
|
|
1922
|
-
return arrObject;
|
|
1923
|
-
}
|
|
1924
|
-
function isObject(data) {
|
|
1925
|
-
return typeof data === "object" && !Array.isArray(data) && data !== null;
|
|
1926
|
-
}
|
|
1927
|
-
function randString(len) {
|
|
1928
|
-
const result = [];
|
|
1929
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
1930
|
-
for (let i = 0; i < len; i++) {
|
|
1931
|
-
const rand = Math.floor(Math.random() * chars.length);
|
|
1932
|
-
result.push(chars.charAt(rand));
|
|
1933
|
-
}
|
|
1934
|
-
return result.join("");
|
|
1935
|
-
}
|
|
1936
|
-
function translationsForLocale(translationsMap, locale) {
|
|
1937
|
-
const localeTranslations = {};
|
|
1938
|
-
Object.values(translationsMap).forEach((string) => {
|
|
1939
|
-
const source = string.source;
|
|
1940
|
-
const translation = string[locale] || string.en || string.source;
|
|
1941
|
-
localeTranslations[source] = translation;
|
|
1942
|
-
});
|
|
1943
|
-
return localeTranslations;
|
|
1944
|
-
}
|
|
1945
|
-
function stringifyObj(obj) {
|
|
1946
|
-
function format(obj2) {
|
|
1947
|
-
if (obj2 === null) {
|
|
1948
|
-
return "null";
|
|
1949
|
-
}
|
|
1950
|
-
if (typeof obj2 === "undefined") {
|
|
1951
|
-
return "undefined";
|
|
1952
|
-
}
|
|
1953
|
-
if (typeof obj2 === "string") {
|
|
1954
|
-
return `"${obj2.replaceAll('"', '\\"')}"`;
|
|
1955
|
-
}
|
|
1956
|
-
if (typeof obj2 !== "object") {
|
|
1957
|
-
return String(obj2);
|
|
1958
|
-
}
|
|
1959
|
-
if (Array.isArray(obj2)) {
|
|
1960
|
-
return `[${obj2.map(format).join(", ")}]`;
|
|
1961
|
-
}
|
|
1962
|
-
const entries = Object.entries(obj2).map(([key, value]) => {
|
|
1963
|
-
return `${key}: ${format(value)}`;
|
|
1964
|
-
});
|
|
1965
|
-
return `{${entries.join(", ")}}`;
|
|
1966
|
-
}
|
|
1967
|
-
return format(obj);
|
|
1968
|
-
}
|
|
1969
|
-
function parseDocId(docId) {
|
|
1970
|
-
const sepIndex = docId.indexOf("/");
|
|
1971
|
-
if (sepIndex <= 0) {
|
|
1972
|
-
throw new Error(`invalid doc id: ${docId}`);
|
|
1973
|
-
}
|
|
1974
|
-
const collection = docId.slice(0, sepIndex);
|
|
1975
|
-
const slug = docId.slice(sepIndex + 1).replaceAll("/", "--");
|
|
1976
|
-
if (!collection || !slug) {
|
|
1977
|
-
throw new Error(`invalid doc id: ${docId}`);
|
|
1978
|
-
}
|
|
1979
|
-
return { collection, slug };
|
|
1980
|
-
}
|
|
1981
|
-
var BatchRequest = class {
|
|
1982
|
-
constructor(cmsClient, options) {
|
|
1983
|
-
this.docIds = [];
|
|
1984
|
-
this.dataSourceIds = [];
|
|
1985
|
-
this.queries = [];
|
|
1986
|
-
this.translationsIds = [];
|
|
1987
|
-
this.cmsClient = cmsClient;
|
|
1988
|
-
this.db = cmsClient.db;
|
|
1989
|
-
this.options = options;
|
|
1990
|
-
}
|
|
1991
|
-
/**
|
|
1992
|
-
* Adds a doc to the batch request.
|
|
1993
|
-
*/
|
|
1994
|
-
addDoc(docId) {
|
|
1995
|
-
this.docIds.push(docId);
|
|
1996
|
-
}
|
|
1997
|
-
/**
|
|
1998
|
-
* Adds a data source to the batch request.
|
|
1999
|
-
*/
|
|
2000
|
-
addDataSource(dataSourceId) {
|
|
2001
|
-
this.dataSourceIds.push(dataSourceId);
|
|
2002
|
-
}
|
|
2003
|
-
/**
|
|
2004
|
-
* Adds a collection-based query to the batch request.
|
|
2005
|
-
*/
|
|
2006
|
-
addQuery(queryId, collectionId, queryOptions) {
|
|
2007
|
-
this.queries.push({
|
|
2008
|
-
queryId,
|
|
2009
|
-
collectionId,
|
|
2010
|
-
queryOptions
|
|
2011
|
-
});
|
|
2012
|
-
}
|
|
2013
|
-
/**
|
|
2014
|
-
* Adds a translation file to the request.
|
|
2015
|
-
*/
|
|
2016
|
-
addTranslations(translationsId) {
|
|
2017
|
-
this.translationsIds.push(translationsId);
|
|
2018
|
-
}
|
|
2019
|
-
/**
|
|
2020
|
-
* Fetches data from the DB.
|
|
2021
|
-
*/
|
|
2022
|
-
async fetch() {
|
|
2023
|
-
const res = new BatchResponse();
|
|
2024
|
-
const promises = [
|
|
2025
|
-
this.fetchDocs(res),
|
|
2026
|
-
this.fetchQueries(res),
|
|
2027
|
-
this.fetchDataSources(res)
|
|
2028
|
-
];
|
|
2029
|
-
if (!this.options.translate && this.translationsIds.length > 0) {
|
|
2030
|
-
promises.push(this.fetchTranslations(res));
|
|
2031
|
-
}
|
|
2032
|
-
await Promise.all(promises);
|
|
2033
|
-
if (this.translationsIds.length > 0) {
|
|
2034
|
-
await this.fetchTranslations(res);
|
|
2035
|
-
}
|
|
2036
|
-
return res;
|
|
2037
|
-
}
|
|
2038
|
-
async fetchDocs(res) {
|
|
2039
|
-
if (this.docIds.length === 0) {
|
|
2040
|
-
return;
|
|
2041
|
-
}
|
|
2042
|
-
const docRefs = this.docIds.map((docId) => {
|
|
2043
|
-
const [collectionId, slug] = docId.split("/");
|
|
2044
|
-
return this.cmsClient.dbDocRef(collectionId, slug, {
|
|
2045
|
-
mode: this.options.mode
|
|
2046
|
-
});
|
|
2047
|
-
});
|
|
2048
|
-
const docs = await this.db.getAll(...docRefs);
|
|
2049
|
-
this.docIds.forEach((docId, i) => {
|
|
2050
|
-
const doc = docs[i];
|
|
2051
|
-
if (!doc.exists) {
|
|
2052
|
-
console.warn(`doc "${docId}" does not exist`);
|
|
2053
|
-
return;
|
|
2054
|
-
}
|
|
2055
|
-
const docData = unmarshalData(doc.data());
|
|
2056
|
-
res.docs[docId] = docData;
|
|
2057
|
-
if (this.options.translate) {
|
|
2058
|
-
this.addTranslations(docId);
|
|
2059
|
-
}
|
|
2060
|
-
});
|
|
2061
|
-
}
|
|
2062
|
-
async fetchQueries(res) {
|
|
2063
|
-
if (this.queries.length === 0) {
|
|
2064
|
-
return;
|
|
2065
|
-
}
|
|
2066
|
-
const mode = this.options.mode;
|
|
2067
|
-
const handleQuery = async (queryItem) => {
|
|
2068
|
-
const docsPath = this.cmsClient.dbCollectionDocsPath(
|
|
2069
|
-
queryItem.collectionId,
|
|
2070
|
-
{ mode }
|
|
2071
|
-
);
|
|
2072
|
-
const queryOptions = queryItem.queryOptions || {};
|
|
2073
|
-
let query = this.db.collection(docsPath);
|
|
2074
|
-
if (queryOptions.limit) {
|
|
2075
|
-
query = query.limit(queryOptions.limit);
|
|
2076
|
-
}
|
|
2077
|
-
if (queryOptions.offset) {
|
|
2078
|
-
query = query.offset(queryOptions.offset);
|
|
2079
|
-
}
|
|
2080
|
-
if (queryOptions.orderBy) {
|
|
2081
|
-
query = query.orderBy(
|
|
2082
|
-
queryOptions.orderBy,
|
|
2083
|
-
queryOptions.orderByDirection
|
|
2084
|
-
);
|
|
2085
|
-
}
|
|
2086
|
-
if (queryOptions.query) {
|
|
2087
|
-
query = queryOptions.query(query);
|
|
2088
|
-
}
|
|
2089
|
-
const results = await query.get();
|
|
2090
|
-
const docs = [];
|
|
2091
|
-
results.forEach((result) => {
|
|
2092
|
-
const doc = unmarshalData(result.data());
|
|
2093
|
-
docs.push(doc);
|
|
2094
|
-
});
|
|
2095
|
-
res.queries[queryItem.queryId] = docs;
|
|
2096
|
-
};
|
|
2097
|
-
await Promise.all(this.queries.map((queryItem) => handleQuery(queryItem)));
|
|
2098
|
-
}
|
|
2099
|
-
async fetchDataSources(res) {
|
|
2100
|
-
if (this.dataSourceIds.length === 0) {
|
|
2101
|
-
return;
|
|
2102
|
-
}
|
|
2103
|
-
const docRefs = this.dataSourceIds.map((dataSourceId) => {
|
|
2104
|
-
return this.cmsClient.dbDataSourceDataRef(dataSourceId, {
|
|
2105
|
-
mode: this.options.mode
|
|
2106
|
-
});
|
|
2107
|
-
});
|
|
2108
|
-
const docs = await this.db.getAll(...docRefs);
|
|
2109
|
-
this.dataSourceIds.forEach((dataSourceId, i) => {
|
|
2110
|
-
const doc = docs[i];
|
|
2111
|
-
if (!doc.exists) {
|
|
2112
|
-
console.warn(`"data source "${dataSourceId}" does not exist`);
|
|
2113
|
-
return;
|
|
2114
|
-
}
|
|
2115
|
-
res.dataSources[dataSourceId] = doc.data();
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
async fetchTranslations(res) {
|
|
2119
|
-
if (this.translationsIds.length === 0) {
|
|
2120
|
-
return;
|
|
2121
|
-
}
|
|
2122
|
-
const docRefs = this.translationsIds.map((translationsId) => {
|
|
2123
|
-
return this.cmsClient.dbTranslationsRef(translationsId, {
|
|
2124
|
-
mode: this.options.mode
|
|
2125
|
-
});
|
|
2126
|
-
});
|
|
2127
|
-
const docs = await this.db.getAll(...docRefs);
|
|
2128
|
-
this.translationsIds.forEach((translationsId, i) => {
|
|
2129
|
-
const doc = docs[i];
|
|
2130
|
-
if (!doc.exists) {
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
res.translations[translationsId] = doc.data();
|
|
2134
|
-
});
|
|
2135
|
-
}
|
|
2136
|
-
};
|
|
2137
|
-
var BatchResponse = class {
|
|
2138
|
-
constructor() {
|
|
2139
|
-
this.docs = {};
|
|
2140
|
-
this.queries = {};
|
|
2141
|
-
this.dataSources = {};
|
|
2142
|
-
this.translations = {};
|
|
2143
|
-
}
|
|
2144
|
-
/**
|
|
2145
|
-
* Returns a map of translations for a given locale or locale fallbacks.
|
|
2146
|
-
*
|
|
2147
|
-
* The input is either a single locale (e.g. "de") or an array of locales
|
|
2148
|
-
* representing the fallback tree, e.g. ["en-CA", "en-GB", "en"].
|
|
2149
|
-
*
|
|
2150
|
-
* TODO(stevenle): support the locale fallback tree.
|
|
2151
|
-
*
|
|
2152
|
-
* The returned value is a flat map of source string to translated string,
|
|
2153
|
-
* e.g.:
|
|
2154
|
-
* {"<source>": "<translation>"}
|
|
2155
|
-
*/
|
|
2156
|
-
getTranslations(locale) {
|
|
2157
|
-
const translationsMap = this.getTranslationsMap();
|
|
2158
|
-
const translations = translationsForLocale(translationsMap, locale);
|
|
2159
|
-
return translations;
|
|
2160
|
-
}
|
|
2161
|
-
/**
|
|
2162
|
-
* Merges the strings from all translations files retrieved in the request.
|
|
2163
|
-
* The returned value is a map of string to translations, e.g.:
|
|
2164
|
-
*
|
|
2165
|
-
* {"<hash>": {"source": "<source>", "<locale>": "<translation>"}}
|
|
2166
|
-
*/
|
|
2167
|
-
getTranslationsMap() {
|
|
2168
|
-
const translationsDocs = Object.values(this.translations).reverse();
|
|
2169
|
-
const translationsMap = {};
|
|
2170
|
-
for (const translationsDoc of translationsDocs) {
|
|
2171
|
-
const strings = translationsDoc.strings || {};
|
|
2172
|
-
for (const hash in strings) {
|
|
2173
|
-
const translations = strings[hash];
|
|
2174
|
-
translationsMap[hash] ??= { source: translations.source };
|
|
2175
|
-
for (const locale in translations) {
|
|
2176
|
-
if (locale !== "source" && translations[locale]) {
|
|
2177
|
-
translationsMap[hash][locale] = translations[locale];
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
return translationsMap;
|
|
2183
|
-
}
|
|
2184
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
ChatClient,
|
|
3
|
+
summarizeDiff
|
|
4
|
+
} from "./chunk-ZOEPOKGE.js";
|
|
5
|
+
import {
|
|
6
|
+
getServerVersion
|
|
7
|
+
} from "./chunk-Y65VGJLE.js";
|
|
8
|
+
import {
|
|
9
|
+
runCronJobs
|
|
10
|
+
} from "./chunk-SI44FG3H.js";
|
|
11
|
+
import {
|
|
12
|
+
RootCMSClient,
|
|
13
|
+
parseDocId,
|
|
14
|
+
unmarshalData
|
|
15
|
+
} from "./chunk-62EVNFXB.js";
|
|
16
|
+
import "./chunk-MLKGABMK.js";
|
|
2185
17
|
|
|
2186
|
-
// core/
|
|
18
|
+
// core/plugin.ts
|
|
19
|
+
import { promises as fs2 } from "fs";
|
|
2187
20
|
import path2 from "path";
|
|
2188
|
-
import {
|
|
2189
|
-
import
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
async saveVersions() {
|
|
2204
|
-
const lastRun = await this.getLastRun();
|
|
2205
|
-
const lastRunWithOffset = lastRun === 0 ? lastRun : lastRun - DOCUMENT_SAVE_OFFSET;
|
|
2206
|
-
const changedDocs = await this.getDocsModifiedAfter(lastRunWithOffset);
|
|
2207
|
-
const now = Timestamp4.now().toMillis();
|
|
2208
|
-
const versions = changedDocs.filter((doc) => {
|
|
2209
|
-
const modifiedAt = doc.sys.modifiedAt.toMillis();
|
|
2210
|
-
return modifiedAt <= now - DOCUMENT_SAVE_OFFSET;
|
|
2211
|
-
});
|
|
2212
|
-
if (versions.length > 0) {
|
|
2213
|
-
this.saveVersionsToFirestore(versions);
|
|
2214
|
-
}
|
|
2215
|
-
this.saveLastRun(now);
|
|
2216
|
-
}
|
|
2217
|
-
async saveVersionsToFirestore(versions) {
|
|
2218
|
-
const batch = this.db.batch();
|
|
2219
|
-
versions.forEach((version) => {
|
|
2220
|
-
if (!version.collection || !version.slug || !version.sys?.modifiedAt) {
|
|
2221
|
-
return;
|
|
2222
|
-
}
|
|
2223
|
-
const modifiedAtMillis = version.sys.modifiedAt.toMillis();
|
|
2224
|
-
const versionPath = `Projects/${this.projectId}/Collections/${version.collection}/Drafts/${version.slug}/Versions/${modifiedAtMillis}`;
|
|
2225
|
-
console.log(versionPath);
|
|
2226
|
-
const versionRef = this.db.doc(versionPath);
|
|
2227
|
-
batch.set(versionRef, version);
|
|
2228
|
-
});
|
|
2229
|
-
await batch.commit();
|
|
2230
|
-
console.log(`versions: saved ${versions.length} versions`);
|
|
2231
|
-
}
|
|
2232
|
-
/**
|
|
2233
|
-
* Returns the last time (in millis) saveVersions() was run, or 0 if has
|
|
2234
|
-
* never been run.
|
|
2235
|
-
*/
|
|
2236
|
-
async getLastRun() {
|
|
2237
|
-
const projectDocRef = this.db.collection("Projects").doc(this.projectId);
|
|
2238
|
-
const projectDoc = await projectDocRef.get();
|
|
2239
|
-
if (projectDoc.exists) {
|
|
2240
|
-
const data = projectDoc.data() || {};
|
|
2241
|
-
const ts = data.versionsLastRun;
|
|
2242
|
-
if (ts) {
|
|
2243
|
-
return ts.toMillis();
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
return 0;
|
|
2247
|
-
}
|
|
2248
|
-
/**
|
|
2249
|
-
* Saves {versionLastRun: <timestamp>} to the Projects/<projectId> doc.
|
|
2250
|
-
*/
|
|
2251
|
-
async saveLastRun(millis) {
|
|
2252
|
-
const ts = Timestamp4.fromMillis(millis);
|
|
2253
|
-
const projectDocRef = this.db.collection("Projects").doc(this.projectId);
|
|
2254
|
-
await projectDocRef.set({ versionsLastRun: ts }, { merge: true });
|
|
2255
|
-
}
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { viteSsrLoadModule } from "@blinkk/root/node";
|
|
23
|
+
import bodyParser from "body-parser";
|
|
24
|
+
import {
|
|
25
|
+
applicationDefault,
|
|
26
|
+
getApp,
|
|
27
|
+
initializeApp
|
|
28
|
+
} from "firebase-admin/app";
|
|
29
|
+
import { getAuth } from "firebase-admin/auth";
|
|
30
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
31
|
+
import * as jsonwebtoken from "jsonwebtoken";
|
|
32
|
+
import sirv from "sirv";
|
|
33
|
+
|
|
34
|
+
// shared/sse.ts
|
|
35
|
+
var SSEEvent = {
|
|
2256
36
|
/**
|
|
2257
|
-
*
|
|
37
|
+
* First connection event.
|
|
2258
38
|
*/
|
|
2259
|
-
|
|
2260
|
-
const ts = Timestamp4.fromMillis(millis);
|
|
2261
|
-
const results = [];
|
|
2262
|
-
const collectionIds = await this.listCollections();
|
|
2263
|
-
for (const collectionId of collectionIds) {
|
|
2264
|
-
const collectionPath = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
|
|
2265
|
-
const query = this.db.collection(collectionPath).where("sys.modifiedAt", ">=", ts);
|
|
2266
|
-
const querySnapshot = await query.get();
|
|
2267
|
-
querySnapshot.forEach((doc) => {
|
|
2268
|
-
results.push(doc.data());
|
|
2269
|
-
});
|
|
2270
|
-
}
|
|
2271
|
-
return results;
|
|
2272
|
-
}
|
|
39
|
+
CONNECTED: "connected",
|
|
2273
40
|
/**
|
|
2274
|
-
*
|
|
41
|
+
* Changes to any .schema.ts file.
|
|
2275
42
|
*/
|
|
2276
|
-
|
|
2277
|
-
const collectionIds = [];
|
|
2278
|
-
const collectionFileNames = await glob("*.schema.ts", {
|
|
2279
|
-
cwd: path2.join(this.rootConfig.rootDir, "collections")
|
|
2280
|
-
});
|
|
2281
|
-
collectionFileNames.forEach((filename) => {
|
|
2282
|
-
collectionIds.push(filename.slice(0, -10));
|
|
2283
|
-
});
|
|
2284
|
-
return collectionIds;
|
|
2285
|
-
}
|
|
43
|
+
SCHEMA_CHANGED: "schemaChanged"
|
|
2286
44
|
};
|
|
2287
45
|
|
|
2288
|
-
// core/
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
runCronJob("saveVersions", runSaveVersions, rootConfig)
|
|
2293
|
-
]);
|
|
2294
|
-
}
|
|
2295
|
-
async function runCronJob(name, fn, ...args) {
|
|
2296
|
-
try {
|
|
2297
|
-
await fn(...args);
|
|
2298
|
-
} catch (err) {
|
|
2299
|
-
console.log(`cron failed: ${name}`);
|
|
2300
|
-
console.error(String(err.stack || err));
|
|
2301
|
-
throw err;
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
async function runPublishScheduledDocs(rootConfig) {
|
|
2305
|
-
const cmsClient = new RootCMSClient(rootConfig);
|
|
2306
|
-
await cmsClient.publishScheduledDocs();
|
|
2307
|
-
await cmsClient.publishScheduledReleases();
|
|
2308
|
-
}
|
|
2309
|
-
async function runSaveVersions(rootConfig) {
|
|
2310
|
-
const service = new VersionsService(rootConfig);
|
|
2311
|
-
await service.saveVersions();
|
|
2312
|
-
}
|
|
46
|
+
// core/api.ts
|
|
47
|
+
import { promises as fs } from "fs";
|
|
48
|
+
import path from "path";
|
|
49
|
+
import { multipartMiddleware } from "@blinkk/root/middleware";
|
|
2313
50
|
|
|
2314
51
|
// core/csv.ts
|
|
2315
52
|
import { parse as csvParse } from "csv-parse/sync";
|
|
@@ -2379,13 +116,13 @@ function api(server, options) {
|
|
|
2379
116
|
return await app.getCollection(collectionId);
|
|
2380
117
|
}
|
|
2381
118
|
try {
|
|
2382
|
-
const schemaPath =
|
|
119
|
+
const schemaPath = path.join(
|
|
2383
120
|
req.rootConfig.rootDir,
|
|
2384
121
|
"dist",
|
|
2385
122
|
"collections",
|
|
2386
123
|
`${collectionId}.schema.json`
|
|
2387
124
|
);
|
|
2388
|
-
const contents = await
|
|
125
|
+
const contents = await fs.readFile(schemaPath, "utf8");
|
|
2389
126
|
return JSON.parse(contents);
|
|
2390
127
|
} catch (err) {
|
|
2391
128
|
if (err && err.code === "ENOENT") {
|
|
@@ -2636,8 +373,8 @@ function api(server, options) {
|
|
|
2636
373
|
}
|
|
2637
374
|
try {
|
|
2638
375
|
const cmsClient = new RootCMSClient(req.rootConfig);
|
|
2639
|
-
const { generateImage
|
|
2640
|
-
const imageUrl = await
|
|
376
|
+
const { generateImage } = await import("./ai-DRQJXU4N.js");
|
|
377
|
+
const imageUrl = await generateImage(cmsClient, {
|
|
2641
378
|
prompt,
|
|
2642
379
|
aspectRatio
|
|
2643
380
|
});
|
|
@@ -2650,196 +387,6 @@ function api(server, options) {
|
|
|
2650
387
|
);
|
|
2651
388
|
}
|
|
2652
389
|
|
|
2653
|
-
// package.json
|
|
2654
|
-
var package_default = {
|
|
2655
|
-
name: "@blinkk/root-cms",
|
|
2656
|
-
version: "2.4.8",
|
|
2657
|
-
author: "s@blinkk.com",
|
|
2658
|
-
license: "MIT",
|
|
2659
|
-
engines: {
|
|
2660
|
-
node: ">=16.0.0"
|
|
2661
|
-
},
|
|
2662
|
-
repository: {
|
|
2663
|
-
type: "git",
|
|
2664
|
-
url: "git+https://github.com/blinkk/rootjs.git",
|
|
2665
|
-
directory: "packages/root-cms"
|
|
2666
|
-
},
|
|
2667
|
-
files: [
|
|
2668
|
-
"dist/**/*"
|
|
2669
|
-
],
|
|
2670
|
-
bin: {
|
|
2671
|
-
"root-cms": "./bin/root-cms.js"
|
|
2672
|
-
},
|
|
2673
|
-
type: "module",
|
|
2674
|
-
module: "./dist/index.js",
|
|
2675
|
-
types: "./dist/index.d.ts",
|
|
2676
|
-
exports: {
|
|
2677
|
-
".": {
|
|
2678
|
-
types: "./dist/core.d.ts",
|
|
2679
|
-
import: "./dist/core.js"
|
|
2680
|
-
},
|
|
2681
|
-
"./client": {
|
|
2682
|
-
types: "./dist/client.d.ts",
|
|
2683
|
-
import: "./dist/client.js"
|
|
2684
|
-
},
|
|
2685
|
-
"./core": {
|
|
2686
|
-
types: "./dist/core.d.ts",
|
|
2687
|
-
import: "./dist/core.js"
|
|
2688
|
-
},
|
|
2689
|
-
"./functions": {
|
|
2690
|
-
types: "./dist/functions.d.ts",
|
|
2691
|
-
import: "./dist/functions.js"
|
|
2692
|
-
},
|
|
2693
|
-
"./plugin": {
|
|
2694
|
-
types: "./dist/plugin.d.ts",
|
|
2695
|
-
import: "./dist/plugin.js"
|
|
2696
|
-
},
|
|
2697
|
-
"./project": {
|
|
2698
|
-
types: "./dist/project.d.ts",
|
|
2699
|
-
import: "./dist/project.js"
|
|
2700
|
-
},
|
|
2701
|
-
"./richtext": {
|
|
2702
|
-
types: "./dist/richtext.d.ts",
|
|
2703
|
-
import: "./dist/richtext.js"
|
|
2704
|
-
}
|
|
2705
|
-
},
|
|
2706
|
-
scripts: {
|
|
2707
|
-
build: 'rm -rf dist && concurrently -n "core,signin,ui" npm:build:core npm:build:signin npm:build:ui',
|
|
2708
|
-
"build:core": "tsup-node --config=./core/tsup.config.ts",
|
|
2709
|
-
"//": "NOTE: esbuild is used here because tsup doesn't currently support aliases.",
|
|
2710
|
-
"build:ui": "esbuild ui/ui.tsx --bundle --minify --alias:react=@preact/compat --alias:react-dom=@preact/compat --tsconfig=ui/tsconfig.json --outdir=dist/ui --legal-comments=external",
|
|
2711
|
-
"build:signin": "esbuild signin/signin.tsx --bundle --minify --tsconfig=signin/tsconfig.json --outdir=dist/ui --legal-comments=external",
|
|
2712
|
-
dev: 'rm -rf dist && concurrently -k -n "core,ui" npm:dev:core npm:dev:signin npm:dev:ui',
|
|
2713
|
-
"dev:core": "pnpm build:core --watch",
|
|
2714
|
-
"dev:signin": "pnpm build:signin --watch",
|
|
2715
|
-
"dev:ui": "pnpm build:ui --watch",
|
|
2716
|
-
test: "pnpm build && firebase emulators:exec 'vitest run --exclude=**/*.visual.test.tsx'",
|
|
2717
|
-
"test:visual": "vitest run --config=vitest.config.visual.ts",
|
|
2718
|
-
"test:watch": "pnpm build && firebase emulators:exec 'vitest'"
|
|
2719
|
-
},
|
|
2720
|
-
dependencies: {
|
|
2721
|
-
"@ag-grid-community/client-side-row-model": "32.3.9",
|
|
2722
|
-
"@ag-grid-community/core": "32.3.9",
|
|
2723
|
-
"@ag-grid-community/react": "32.3.9",
|
|
2724
|
-
"@ag-grid-community/styles": "32.3.9",
|
|
2725
|
-
"@genkit-ai/ai": "1.26.0",
|
|
2726
|
-
"@genkit-ai/core": "1.26.0",
|
|
2727
|
-
"@genkit-ai/google-genai": "1.26.0",
|
|
2728
|
-
"@google-cloud/firestore": "7.11.3",
|
|
2729
|
-
"@hello-pangea/dnd": "18.0.1",
|
|
2730
|
-
"@types/cli-progress": "3.11.6",
|
|
2731
|
-
"body-parser": "1.20.2",
|
|
2732
|
-
"cli-progress": "3.12.0",
|
|
2733
|
-
commander: "11.0.0",
|
|
2734
|
-
"csv-parse": "5.5.2",
|
|
2735
|
-
"csv-stringify": "6.4.4",
|
|
2736
|
-
"date-fns": "4.1.0",
|
|
2737
|
-
"date-fns-tz": "3.2.0",
|
|
2738
|
-
diff: "8.0.2",
|
|
2739
|
-
"dts-dom": "3.7.0",
|
|
2740
|
-
"fnv-plus": "1.3.1",
|
|
2741
|
-
genkit: "1.26.0",
|
|
2742
|
-
jsonwebtoken: "9.0.2",
|
|
2743
|
-
kleur: "4.1.5",
|
|
2744
|
-
sirv: "2.0.3",
|
|
2745
|
-
"tiny-glob": "0.2.9",
|
|
2746
|
-
zod: "3.23.8"
|
|
2747
|
-
},
|
|
2748
|
-
"//": "NOTE(stevenle): due to compat issues with mantine and preact, mantine is pinned to v4.2.12",
|
|
2749
|
-
devDependencies: {
|
|
2750
|
-
"@babel/core": "7.17.9",
|
|
2751
|
-
"@blinkk/root": "workspace:*",
|
|
2752
|
-
"@editorjs/editorjs": "2.30.8",
|
|
2753
|
-
"@editorjs/header": "2.8.8",
|
|
2754
|
-
"@editorjs/image": "2.10.2",
|
|
2755
|
-
"@editorjs/list": "2.0.6",
|
|
2756
|
-
"@editorjs/nested-list": "1.4.3",
|
|
2757
|
-
"@editorjs/raw": "2.5.1",
|
|
2758
|
-
"@editorjs/table": "2.4.4",
|
|
2759
|
-
"@editorjs/underline": "1.2.1",
|
|
2760
|
-
"@emotion/react": "11.10.5",
|
|
2761
|
-
"@firebase/app-compat": "0.5.2",
|
|
2762
|
-
"@firebase/app-types": "0.9.3",
|
|
2763
|
-
"@firebase/rules-unit-testing": "5.0.0",
|
|
2764
|
-
"@lexical/code": "0.33.1",
|
|
2765
|
-
"@lexical/html": "0.33.1",
|
|
2766
|
-
"@lexical/link": "0.33.1",
|
|
2767
|
-
"@lexical/list": "0.33.1",
|
|
2768
|
-
"@lexical/markdown": "0.33.1",
|
|
2769
|
-
"@lexical/react": "0.33.1",
|
|
2770
|
-
"@lexical/rich-text": "0.33.1",
|
|
2771
|
-
"@lexical/selection": "0.33.1",
|
|
2772
|
-
"@lexical/table": "0.33.1",
|
|
2773
|
-
"@lexical/utils": "0.33.1",
|
|
2774
|
-
"@mantine/core": "4.2.12",
|
|
2775
|
-
"@mantine/hooks": "4.2.12",
|
|
2776
|
-
"@mantine/modals": "4.2.12",
|
|
2777
|
-
"@mantine/notifications": "4.2.12",
|
|
2778
|
-
"@mantine/spotlight": "4.2.12",
|
|
2779
|
-
"@preact/compat": "18.3.1",
|
|
2780
|
-
"@tabler/icons-preact": "3.35.0",
|
|
2781
|
-
"@testing-library/preact": "3.2.4",
|
|
2782
|
-
"@testing-library/user-event": "14.6.1",
|
|
2783
|
-
"@types/body-parser": "1.19.3",
|
|
2784
|
-
"@types/fnv-plus": "1.3.2",
|
|
2785
|
-
"@types/gapi": "0.0.47",
|
|
2786
|
-
"@types/gapi.client.drive-v3": "0.0.4",
|
|
2787
|
-
"@types/gapi.client.sheets-v4": "0.0.4",
|
|
2788
|
-
"@types/google.accounts": "0.0.14",
|
|
2789
|
-
"@types/jsonwebtoken": "9.0.1",
|
|
2790
|
-
"@types/node": "24.3.1",
|
|
2791
|
-
"@vitest/browser": "4.0.10",
|
|
2792
|
-
"@vitest/browser-playwright": "4.0.10",
|
|
2793
|
-
concurrently: "7.6.0",
|
|
2794
|
-
esbuild: "0.25.9",
|
|
2795
|
-
firebase: "12.2.1",
|
|
2796
|
-
"firebase-admin": "13.5.0",
|
|
2797
|
-
"firebase-functions": "6.4.0",
|
|
2798
|
-
"firebase-tools": "14.15.2",
|
|
2799
|
-
"highlight.js": "11.6.0",
|
|
2800
|
-
jsdom: "27.2.0",
|
|
2801
|
-
"json-diff-kit": "1.0.29",
|
|
2802
|
-
lexical: "0.33.1",
|
|
2803
|
-
marked: "9.1.1",
|
|
2804
|
-
"mdast-util-from-markdown": "2.0.1",
|
|
2805
|
-
"mdast-util-gfm": "3.0.0",
|
|
2806
|
-
"micromark-extension-gfm": "3.0.0",
|
|
2807
|
-
playwright: "1.56.1",
|
|
2808
|
-
preact: "10.27.1",
|
|
2809
|
-
"preact-render-to-string": "6.6.1",
|
|
2810
|
-
"preact-router": "4.1.2",
|
|
2811
|
-
react: "npm:@preact/compat@18.3.1",
|
|
2812
|
-
"react-dom": "npm:@preact/compat@18.3.1",
|
|
2813
|
-
"react-json-view-compare": "2.0.2",
|
|
2814
|
-
tsup: "8.5.0",
|
|
2815
|
-
typescript: "5.9.2",
|
|
2816
|
-
vite: "7.1.4",
|
|
2817
|
-
vitest: "4.0.10",
|
|
2818
|
-
yjs: "13.6.27"
|
|
2819
|
-
},
|
|
2820
|
-
peerDependencies: {
|
|
2821
|
-
"@blinkk/root": "2.4.8",
|
|
2822
|
-
"firebase-admin": ">=11",
|
|
2823
|
-
"firebase-functions": ">=4",
|
|
2824
|
-
preact: ">=10",
|
|
2825
|
-
"preact-render-to-string": ">=5"
|
|
2826
|
-
},
|
|
2827
|
-
peerDependenciesMeta: {
|
|
2828
|
-
"firebase-functions": {
|
|
2829
|
-
optional: true
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
};
|
|
2833
|
-
|
|
2834
|
-
// core/server-version.ts
|
|
2835
|
-
var SERVER_STARTUP_TS = String(Math.floor((/* @__PURE__ */ new Date()).getTime() / 1e3));
|
|
2836
|
-
function getServerVersion() {
|
|
2837
|
-
if (process.env.NODE_ENV === "development") {
|
|
2838
|
-
return SERVER_STARTUP_TS;
|
|
2839
|
-
}
|
|
2840
|
-
return package_default?.version || "root-3.0.0";
|
|
2841
|
-
}
|
|
2842
|
-
|
|
2843
390
|
// core/sse.ts
|
|
2844
391
|
function sse(server) {
|
|
2845
392
|
const sseClients = /* @__PURE__ */ new Set();
|
|
@@ -2854,6 +401,10 @@ function sse(server) {
|
|
|
2854
401
|
});
|
|
2855
402
|
};
|
|
2856
403
|
server.use("/cms/api/sse.connect", async (req, res) => {
|
|
404
|
+
if (!req.user) {
|
|
405
|
+
res.status(401).json({ success: false, error: "NOT_AUTHORIZED" });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
2857
408
|
res.writeHead(200, {
|
|
2858
409
|
"Content-Type": "text/event-stream",
|
|
2859
410
|
"Cache-Control": "no-cache",
|
|
@@ -2888,29 +439,29 @@ function formatMessage(event, data) {
|
|
|
2888
439
|
}
|
|
2889
440
|
|
|
2890
441
|
// core/plugin.ts
|
|
2891
|
-
var
|
|
442
|
+
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
2892
443
|
async function writeCollectionSchemasToJson(rootConfig) {
|
|
2893
|
-
const modulePath =
|
|
2894
|
-
const project = await
|
|
444
|
+
const modulePath = path2.resolve(__dirname, "./project.js");
|
|
445
|
+
const project = await viteSsrLoadModule(
|
|
2895
446
|
rootConfig,
|
|
2896
447
|
modulePath
|
|
2897
448
|
);
|
|
2898
|
-
const outDir =
|
|
2899
|
-
await
|
|
449
|
+
const outDir = path2.join(rootConfig.rootDir, "dist", "collections");
|
|
450
|
+
await fs2.mkdir(outDir, { recursive: true });
|
|
2900
451
|
const fileIds = Object.keys(project.SCHEMA_MODULES);
|
|
2901
452
|
for (const fileId of fileIds) {
|
|
2902
453
|
if (!fileId.startsWith("/collections/")) {
|
|
2903
454
|
continue;
|
|
2904
455
|
}
|
|
2905
|
-
const collectionId =
|
|
456
|
+
const collectionId = path2.basename(fileId).split(".")[0];
|
|
2906
457
|
const schema = await project.getCollectionSchema(collectionId);
|
|
2907
458
|
if (!schema) {
|
|
2908
459
|
console.warn(`collection is missing a default export: ${fileId}`);
|
|
2909
460
|
continue;
|
|
2910
461
|
}
|
|
2911
|
-
const jsonPath =
|
|
462
|
+
const jsonPath = path2.join(outDir, `${collectionId}.schema.json`);
|
|
2912
463
|
const data = JSON.stringify(schema, null, 2);
|
|
2913
|
-
await
|
|
464
|
+
await fs2.writeFile(jsonPath, data, "utf-8");
|
|
2914
465
|
}
|
|
2915
466
|
}
|
|
2916
467
|
var SESSION_COOKIE_AUTH = "root-cms-auth";
|
|
@@ -3128,7 +679,7 @@ function cmsPlugin(options) {
|
|
|
3128
679
|
*/
|
|
3129
680
|
ssrInput: () => {
|
|
3130
681
|
return {
|
|
3131
|
-
cms:
|
|
682
|
+
cms: path2.resolve(__dirname, "./app.js")
|
|
3132
683
|
};
|
|
3133
684
|
},
|
|
3134
685
|
/**
|
|
@@ -3138,13 +689,13 @@ function cmsPlugin(options) {
|
|
|
3138
689
|
server.use(bodyParser.json());
|
|
3139
690
|
async function getRenderer(req) {
|
|
3140
691
|
if (serverOptions.type === "dev") {
|
|
3141
|
-
const appFilePath =
|
|
692
|
+
const appFilePath = path2.resolve(__dirname, "./app.js");
|
|
3142
693
|
const app3 = await req.viteServer.ssrLoadModule(
|
|
3143
694
|
appFilePath
|
|
3144
695
|
);
|
|
3145
696
|
return app3;
|
|
3146
697
|
}
|
|
3147
|
-
const appImportPath =
|
|
698
|
+
const appImportPath = path2.resolve(
|
|
3148
699
|
req.rootConfig.rootDir,
|
|
3149
700
|
"dist/server/cms.js"
|
|
3150
701
|
);
|
|
@@ -3216,7 +767,7 @@ function cmsPlugin(options) {
|
|
|
3216
767
|
}
|
|
3217
768
|
next();
|
|
3218
769
|
});
|
|
3219
|
-
const staticDir =
|
|
770
|
+
const staticDir = path2.resolve(__dirname, "ui");
|
|
3220
771
|
server.use(
|
|
3221
772
|
"/cms/static",
|
|
3222
773
|
sirv(staticDir, { dev: process.env.NODE_ENV === "development" })
|
|
@@ -3263,13 +814,13 @@ function cmsPlugin(options) {
|
|
|
3263
814
|
if (process.env.NODE_ENV === "development" && options.watch !== false) {
|
|
3264
815
|
plugin.onFileChange = (eventName, filepath) => {
|
|
3265
816
|
if (filepath.endsWith(".schema.ts")) {
|
|
3266
|
-
|
|
3267
|
-
const
|
|
3268
|
-
|
|
817
|
+
import("./generate-types-TQBCE2SG.js").then((generateTypesModule) => {
|
|
818
|
+
const generateTypes = generateTypesModule.generateTypes;
|
|
819
|
+
generateTypes();
|
|
3269
820
|
});
|
|
3270
821
|
if (sseBroadcast) {
|
|
3271
822
|
const eventData = {
|
|
3272
|
-
file:
|
|
823
|
+
file: path2.basename(filepath)
|
|
3273
824
|
};
|
|
3274
825
|
sseBroadcast(SSEEvent.SCHEMA_CHANGED, eventData);
|
|
3275
826
|
}
|
|
@@ -3279,7 +830,7 @@ function cmsPlugin(options) {
|
|
|
3279
830
|
return plugin;
|
|
3280
831
|
}
|
|
3281
832
|
function fileExists(filepath) {
|
|
3282
|
-
return
|
|
833
|
+
return fs2.access(filepath).then(() => true).catch(() => false);
|
|
3283
834
|
}
|
|
3284
835
|
export {
|
|
3285
836
|
cmsPlugin
|