@blinkk/root-cms 3.0.1-beta.3 → 3.0.1-beta.4

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/dist/app.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  getServerVersion,
3
3
  serializeAiConfig
4
- } from "./chunk-NXEXOHBD.js";
5
- import "./chunk-CRK7N6RR.js";
4
+ } from "./chunk-MVS2NLZM.js";
5
+ import "./chunk-C245C557.js";
6
+ import "./chunk-2BSW7SIH.js";
6
7
  import {
7
8
  getCollectionSchema,
8
9
  getProjectSchemas
@@ -1,7 +1,3 @@
1
- import {
2
- validateFields
3
- } from "./chunk-CRK7N6RR.js";
4
-
5
1
  // core/client.ts
6
2
  import crypto from "crypto";
7
3
  import {
@@ -362,6 +358,301 @@ function buildTranslationsLocaleDocDbPath(options) {
362
358
  ).replace("{mode}", options.mode).replace("{id}", normalizeSlug(options.id)).replace("{locale}", options.locale);
363
359
  }
364
360
 
361
+ // core/validation.ts
362
+ function validateFields(fieldsData, schema) {
363
+ if (fieldsData === null || fieldsData === void 0) {
364
+ return [];
365
+ }
366
+ if (typeof fieldsData !== "object" || Array.isArray(fieldsData)) {
367
+ return [
368
+ {
369
+ path: "",
370
+ message: "Expected object for fields data",
371
+ expected: "object",
372
+ received: getType(fieldsData)
373
+ }
374
+ ];
375
+ }
376
+ const errors = [];
377
+ for (const field of schema.fields) {
378
+ if (!field.id) {
379
+ continue;
380
+ }
381
+ if (!(field.id in fieldsData)) {
382
+ continue;
383
+ }
384
+ const value = fieldsData[field.id];
385
+ errors.push(...validateValue(value, field, field.id));
386
+ }
387
+ return errors;
388
+ }
389
+ function validateValue(value, field, path) {
390
+ if (value === void 0) {
391
+ return [];
392
+ }
393
+ switch (field.type) {
394
+ case "string":
395
+ case "select":
396
+ if (typeof value !== "string") {
397
+ return [createError(path, "string", value)];
398
+ }
399
+ return [];
400
+ case "number":
401
+ if (typeof value !== "number") {
402
+ return [createError(path, "number", value)];
403
+ }
404
+ if (isNaN(value)) {
405
+ return [createError(path, "number", value)];
406
+ }
407
+ return [];
408
+ case "boolean":
409
+ if (typeof value !== "boolean") {
410
+ return [createError(path, "boolean", value)];
411
+ }
412
+ return [];
413
+ case "date":
414
+ case "datetime": {
415
+ if (typeof value !== "object" || Array.isArray(value)) {
416
+ return [createError(path, "object", value)];
417
+ }
418
+ const errors = [];
419
+ const seconds = value.seconds;
420
+ const nanoseconds = value.nanoseconds;
421
+ if (seconds === void 0) {
422
+ errors.push({
423
+ path: `${path}.seconds`,
424
+ message: "Required",
425
+ expected: "number",
426
+ received: "undefined"
427
+ });
428
+ } else if (typeof seconds !== "number") {
429
+ errors.push(createError(`${path}.seconds`, "number", seconds));
430
+ }
431
+ if (nanoseconds === void 0) {
432
+ errors.push({
433
+ path: `${path}.nanoseconds`,
434
+ message: "Required",
435
+ expected: "number",
436
+ received: "undefined"
437
+ });
438
+ } else if (typeof nanoseconds !== "number") {
439
+ errors.push(createError(`${path}.nanoseconds`, "number", nanoseconds));
440
+ }
441
+ return errors;
442
+ }
443
+ case "multiselect":
444
+ if (!Array.isArray(value)) {
445
+ return [createError(path, "array", value)];
446
+ }
447
+ return value.flatMap((item, index) => {
448
+ if (typeof item !== "string") {
449
+ return [createError(`${path}.${index}`, "string", item)];
450
+ }
451
+ return [];
452
+ });
453
+ case "image":
454
+ case "file": {
455
+ if (typeof value !== "object" || Array.isArray(value)) {
456
+ return [createError(path, "object", value)];
457
+ }
458
+ const errors = [];
459
+ if (value.src === void 0) {
460
+ errors.push({
461
+ path: `${path}.src`,
462
+ message: "Required",
463
+ expected: "string",
464
+ received: "undefined"
465
+ });
466
+ } else if (typeof value.src !== "string") {
467
+ errors.push(createError(`${path}.src`, "string", value.src));
468
+ }
469
+ if (value.alt !== void 0 && typeof value.alt !== "string") {
470
+ errors.push(createError(`${path}.alt`, "string", value.alt));
471
+ }
472
+ return errors;
473
+ }
474
+ case "object": {
475
+ if (typeof value !== "object" || Array.isArray(value)) {
476
+ return [createError(path, "object", value)];
477
+ }
478
+ const objectField = field;
479
+ const errors = [];
480
+ for (const nestedField of objectField.fields) {
481
+ if (!nestedField.id || !(nestedField.id in value)) {
482
+ continue;
483
+ }
484
+ errors.push(
485
+ ...validateValue(
486
+ value[nestedField.id],
487
+ nestedField,
488
+ `${path}.${nestedField.id}`
489
+ )
490
+ );
491
+ }
492
+ return errors;
493
+ }
494
+ case "array": {
495
+ if (!Array.isArray(value)) {
496
+ return [createError(path, "array", value)];
497
+ }
498
+ const arrayField = field;
499
+ const itemField = arrayField.of;
500
+ return value.flatMap((item, index) => {
501
+ return validateValue(item, itemField, `${path}.${index}`);
502
+ });
503
+ }
504
+ case "oneof": {
505
+ if (typeof value !== "object" || Array.isArray(value)) {
506
+ return [createError(path, "object", value)];
507
+ }
508
+ const oneOfField = field;
509
+ const typeName = value._type;
510
+ const typeMap = /* @__PURE__ */ new Map();
511
+ const typeNames = [];
512
+ for (const type of oneOfField.types) {
513
+ if (typeof type === "string") {
514
+ typeNames.push(type);
515
+ continue;
516
+ }
517
+ typeMap.set(type.name, type);
518
+ typeNames.push(type.name);
519
+ }
520
+ if (!typeNames.includes(typeName)) {
521
+ const expectedStr = typeNames.map((t) => `'${t}'`).join(" | ");
522
+ return [
523
+ {
524
+ path: `${path}._type`,
525
+ message: `Invalid discriminator value. Expected ${expectedStr}`,
526
+ expected: "valid discriminator value",
527
+ received: typeName
528
+ }
529
+ ];
530
+ }
531
+ const matchedSchema = typeMap.get(typeName);
532
+ if (!matchedSchema) {
533
+ return [];
534
+ }
535
+ const errors = [];
536
+ for (const nestedField of matchedSchema.fields) {
537
+ if (!nestedField.id || !(nestedField.id in value)) {
538
+ continue;
539
+ }
540
+ errors.push(
541
+ ...validateValue(
542
+ value[nestedField.id],
543
+ nestedField,
544
+ `${path}.${nestedField.id}`
545
+ )
546
+ );
547
+ }
548
+ return errors;
549
+ }
550
+ case "richtext": {
551
+ if (typeof value !== "object" || Array.isArray(value)) {
552
+ return [createError(path, "object", value)];
553
+ }
554
+ const errors = [];
555
+ if (value.blocks === void 0) {
556
+ errors.push({
557
+ path: `${path}.blocks`,
558
+ message: "Required",
559
+ expected: "array",
560
+ received: "undefined"
561
+ });
562
+ } else if (!Array.isArray(value.blocks)) {
563
+ errors.push(createError(`${path}.blocks`, "array", value.blocks));
564
+ } else {
565
+ value.blocks.forEach((block, index) => {
566
+ if (typeof block !== "object" || block === null) {
567
+ errors.push(
568
+ createError(`${path}.blocks.${index}`, "object", block)
569
+ );
570
+ return;
571
+ }
572
+ if (block.type === void 0) {
573
+ errors.push({
574
+ path: `${path}.blocks.${index}.type`,
575
+ message: "Required",
576
+ expected: "string",
577
+ received: "undefined"
578
+ });
579
+ } else if (typeof block.type !== "string") {
580
+ errors.push(
581
+ createError(`${path}.blocks.${index}.type`, "string", block.type)
582
+ );
583
+ }
584
+ });
585
+ }
586
+ return errors;
587
+ }
588
+ case "reference": {
589
+ if (typeof value !== "object" || Array.isArray(value)) {
590
+ return [createError(path, "object", value)];
591
+ }
592
+ const errors = [];
593
+ const requiredFields = ["id", "collection", "slug"];
594
+ for (const req of requiredFields) {
595
+ if (value[req] === void 0) {
596
+ errors.push({
597
+ path: `${path}.${req}`,
598
+ message: "Required",
599
+ expected: "string",
600
+ received: "undefined"
601
+ });
602
+ } else if (typeof value[req] !== "string") {
603
+ errors.push(createError(`${path}.${req}`, "string", value[req]));
604
+ }
605
+ }
606
+ return errors;
607
+ }
608
+ case "references": {
609
+ if (!Array.isArray(value)) {
610
+ return [createError(path, "array", value)];
611
+ }
612
+ return value.flatMap((item, index) => {
613
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
614
+ return [createError(`${path}.${index}`, "object", item)];
615
+ }
616
+ const errors = [];
617
+ const requiredFields = ["id", "collection", "slug"];
618
+ for (const req of requiredFields) {
619
+ if (item[req] === void 0) {
620
+ errors.push({
621
+ path: `${path}.${index}.${req}`,
622
+ message: "Required",
623
+ expected: "string",
624
+ received: "undefined"
625
+ });
626
+ } else if (typeof item[req] !== "string") {
627
+ errors.push(
628
+ createError(`${path}.${index}.${req}`, "string", item[req])
629
+ );
630
+ }
631
+ }
632
+ return errors;
633
+ });
634
+ }
635
+ default:
636
+ console.warn(`Unknown field type: ${field.type}`);
637
+ return [];
638
+ }
639
+ }
640
+ function getType(value) {
641
+ if (value === null) return "null";
642
+ if (Array.isArray(value)) return "array";
643
+ if (typeof value === "number" && Number.isNaN(value)) return "nan";
644
+ return typeof value;
645
+ }
646
+ function createError(path, expected, receivedValue) {
647
+ const received = getType(receivedValue);
648
+ return {
649
+ path,
650
+ message: `Expected ${expected}, received ${received}`,
651
+ expected,
652
+ received
653
+ };
654
+ }
655
+
365
656
  // core/values.ts
366
657
  function setValueAtPath(obj, path, value) {
367
658
  const keys = path.split(".");
@@ -0,0 +1,187 @@
1
+ import {
2
+ SearchIndexService
3
+ } from "./chunk-C245C557.js";
4
+ import {
5
+ RootCMSClient,
6
+ getCmsPlugin
7
+ } from "./chunk-2BSW7SIH.js";
8
+
9
+ // core/cron.ts
10
+ import { Timestamp as Timestamp2 } from "firebase-admin/firestore";
11
+
12
+ // core/versions.ts
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import { Timestamp } from "firebase-admin/firestore";
16
+ import glob from "tiny-glob";
17
+ var DOCUMENT_SAVE_OFFSET = 5 * 60 * 1e3;
18
+ var VersionsService = class {
19
+ constructor(rootConfig) {
20
+ this.rootConfig = rootConfig;
21
+ const cmsPlugin = getCmsPlugin(rootConfig);
22
+ const cmsPluginOptions = cmsPlugin.getConfig();
23
+ const projectId = cmsPluginOptions.id || "default";
24
+ this.projectId = projectId;
25
+ this.db = cmsPlugin.getFirestore();
26
+ }
27
+ /**
28
+ * Saves a version of all documents that have been edited since the last run.
29
+ */
30
+ async saveVersions() {
31
+ const lastRun = await this.getLastRun();
32
+ const lastRunWithOffset = lastRun === 0 ? lastRun : lastRun - DOCUMENT_SAVE_OFFSET;
33
+ const changedDocs = await this.getDocsModifiedAfter(lastRunWithOffset);
34
+ const now = Timestamp.now().toMillis();
35
+ const versions = changedDocs.filter((doc) => {
36
+ const modifiedAt = doc.sys.modifiedAt.toMillis();
37
+ if (modifiedAt > now - DOCUMENT_SAVE_OFFSET) {
38
+ return false;
39
+ }
40
+ const publishedAt = doc.sys.publishedAt?.toMillis?.();
41
+ if (publishedAt && Math.abs(publishedAt - modifiedAt) < 5e3) {
42
+ return false;
43
+ }
44
+ return true;
45
+ });
46
+ if (versions.length > 0) {
47
+ this.saveVersionsToFirestore(versions);
48
+ }
49
+ this.saveLastRun(now);
50
+ }
51
+ async saveVersionsToFirestore(versions) {
52
+ const batch = this.db.batch();
53
+ versions.forEach((version) => {
54
+ if (!version.collection || !version.slug || !version.sys?.modifiedAt) {
55
+ return;
56
+ }
57
+ const modifiedAtMillis = version.sys.modifiedAt.toMillis();
58
+ const versionPath = `Projects/${this.projectId}/Collections/${version.collection}/Drafts/${version.slug}/Versions/${modifiedAtMillis}`;
59
+ console.log(versionPath);
60
+ const versionRef = this.db.doc(versionPath);
61
+ batch.set(versionRef, version);
62
+ });
63
+ await batch.commit();
64
+ console.log(`versions: saved ${versions.length} versions`);
65
+ }
66
+ /**
67
+ * Returns the last time (in millis) saveVersions() was run, or 0 if has
68
+ * never been run.
69
+ */
70
+ async getLastRun() {
71
+ const projectDocRef = this.db.collection("Projects").doc(this.projectId);
72
+ const projectDoc = await projectDocRef.get();
73
+ if (projectDoc.exists) {
74
+ const data = projectDoc.data() || {};
75
+ const ts = data.versionsLastRun;
76
+ if (ts) {
77
+ return ts.toMillis();
78
+ }
79
+ }
80
+ return 0;
81
+ }
82
+ /**
83
+ * Saves {versionLastRun: <timestamp>} to the Projects/<projectId> doc.
84
+ */
85
+ async saveLastRun(millis) {
86
+ const ts = Timestamp.fromMillis(millis);
87
+ const projectDocRef = this.db.collection("Projects").doc(this.projectId);
88
+ await projectDocRef.set({ versionsLastRun: ts }, { merge: true });
89
+ }
90
+ /**
91
+ * Returns a list of all docs that were edited after a certain time.
92
+ */
93
+ async getDocsModifiedAfter(millis) {
94
+ const ts = Timestamp.fromMillis(millis);
95
+ const results = [];
96
+ const collectionIds = await this.listCollections();
97
+ for (const collectionId of collectionIds) {
98
+ const collectionPath = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
99
+ const query = this.db.collection(collectionPath).where("sys.modifiedAt", ">=", ts);
100
+ const querySnapshot = await query.get();
101
+ querySnapshot.forEach((doc) => {
102
+ results.push(doc.data());
103
+ });
104
+ }
105
+ return results;
106
+ }
107
+ /**
108
+ * Returns a list of collection ids for the Root project.
109
+ */
110
+ async listCollections() {
111
+ const collectionsDir = path.join(this.rootConfig.rootDir, "collections");
112
+ if (!fs.existsSync(collectionsDir)) {
113
+ return [];
114
+ }
115
+ const collectionIds = [];
116
+ const collectionFileNames = await glob("*.schema.ts", {
117
+ cwd: collectionsDir
118
+ });
119
+ collectionFileNames.forEach((filename) => {
120
+ collectionIds.push(filename.slice(0, -10));
121
+ });
122
+ return collectionIds;
123
+ }
124
+ };
125
+
126
+ // core/cron.ts
127
+ var SEARCH_INDEX_MIN_INTERVAL_MS = 5 * 60 * 1e3;
128
+ async function runCronJobs(rootConfig, options = {}) {
129
+ await Promise.all([
130
+ runCronJob("publishScheduledDocs", runPublishScheduledDocs, rootConfig),
131
+ runCronJob(
132
+ "syncScheduledDataSources",
133
+ runSyncScheduledDataSources,
134
+ rootConfig
135
+ ),
136
+ runCronJob("saveVersions", runSaveVersions, rootConfig),
137
+ runCronJob(
138
+ "incrementalSearchIndex",
139
+ runIncrementalSearchIndex,
140
+ rootConfig,
141
+ options.loadSchema
142
+ )
143
+ ]);
144
+ }
145
+ async function runCronJob(name, fn, ...args) {
146
+ try {
147
+ await fn(...args);
148
+ } catch (err) {
149
+ console.log(`cron failed: ${name}`);
150
+ console.error(String(err.stack || err));
151
+ throw err;
152
+ }
153
+ }
154
+ async function runPublishScheduledDocs(rootConfig) {
155
+ const cmsClient = new RootCMSClient(rootConfig);
156
+ await cmsClient.publishScheduledDocs();
157
+ await cmsClient.publishScheduledReleases();
158
+ }
159
+ async function runSyncScheduledDataSources(rootConfig) {
160
+ const cmsClient = new RootCMSClient(rootConfig);
161
+ await cmsClient.syncScheduledDataSources();
162
+ }
163
+ async function runSaveVersions(rootConfig) {
164
+ const service = new VersionsService(rootConfig);
165
+ await service.saveVersions();
166
+ }
167
+ async function runIncrementalSearchIndex(rootConfig, loadSchema) {
168
+ const service = new SearchIndexService(rootConfig, loadSchema);
169
+ const status = await service.getStatus();
170
+ if (status.lastRun !== null) {
171
+ const elapsed = Date.now() - status.lastRun;
172
+ if (elapsed < SEARCH_INDEX_MIN_INTERVAL_MS) {
173
+ return;
174
+ }
175
+ const hasChanges = await service.hasChangesSince(
176
+ Timestamp2.fromMillis(status.lastRun)
177
+ );
178
+ if (!hasChanges) {
179
+ return;
180
+ }
181
+ }
182
+ await service.rebuildIndex({ force: false });
183
+ }
184
+
185
+ export {
186
+ runCronJobs
187
+ };