@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/dist/cli.js CHANGED
@@ -1,3 +1,11 @@
1
+ import {
2
+ generateTypes
3
+ } from "./chunk-RYF3UTHQ.js";
4
+ import {
5
+ getCmsPlugin
6
+ } from "./chunk-62EVNFXB.js";
7
+ import "./chunk-MLKGABMK.js";
8
+
1
9
  // cli/cli.ts
2
10
  import { Command } from "commander";
3
11
  import { bgGreen, black } from "kleur/colors";
@@ -7,36 +15,6 @@ import fs from "fs";
7
15
  import path from "path";
8
16
  import { loadRootConfig } from "@blinkk/root/node";
9
17
 
10
- // core/client.ts
11
- import crypto from "crypto";
12
- import {
13
- FieldValue as FieldValue2,
14
- Timestamp as Timestamp2
15
- } from "firebase-admin/firestore";
16
-
17
- // core/translations-manager.ts
18
- import {
19
- FieldValue,
20
- Timestamp
21
- } from "firebase-admin/firestore";
22
-
23
- // shared/strings.ts
24
- import fnv from "fnv-plus";
25
-
26
- // core/translations-manager.ts
27
- var TRANSLATIONS_DB_PATH_FORMAT = "/Projects/{project}/TranslationsManager/{mode}/Translations";
28
- var TRANSLATIONS_LOCALE_DOC_DB_PATH_FORMAT = `${TRANSLATIONS_DB_PATH_FORMAT}/{id}:{locale}`;
29
-
30
- // core/client.ts
31
- function getCmsPlugin(rootConfig) {
32
- const plugins = rootConfig.plugins || [];
33
- const plugin = plugins.find((plugin2) => plugin2.name === "root-cms");
34
- if (!plugin) {
35
- throw new Error("could not find root-cms plugin config in root.config.ts");
36
- }
37
- return plugin;
38
- }
39
-
40
18
  // cli/utils.ts
41
19
  function parseFilters(filterStr) {
42
20
  if (!filterStr) {
@@ -47,9 +25,9 @@ function parseFilters(filterStr) {
47
25
  const excludes = parts.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
48
26
  return { includes, excludes };
49
27
  }
50
- function getPathStatus(path4, includes, excludes) {
28
+ function getPathStatus(path3, includes, excludes) {
51
29
  for (const pattern of excludes) {
52
- if (globMatch(path4, pattern)) {
30
+ if (globMatch(path3, pattern)) {
53
31
  return "EXCLUDE";
54
32
  }
55
33
  }
@@ -59,7 +37,7 @@ function getPathStatus(path4, includes, excludes) {
59
37
  let isIncluded = false;
60
38
  let shouldTraverse = false;
61
39
  for (const pattern of includes) {
62
- const match = checkMatch(path4, pattern);
40
+ const match = checkMatch(path3, pattern);
63
41
  if (match === "FULL") {
64
42
  isIncluded = true;
65
43
  } else if (match === "PARTIAL") {
@@ -74,11 +52,11 @@ function getPathStatus(path4, includes, excludes) {
74
52
  }
75
53
  return "SKIP";
76
54
  }
77
- function globMatch(path4, pattern) {
78
- return checkMatch(path4, pattern) === "FULL";
55
+ function globMatch(path3, pattern) {
56
+ return checkMatch(path3, pattern) === "FULL";
79
57
  }
80
- function checkMatch(path4, pattern) {
81
- const pathParts = path4.split("/");
58
+ function checkMatch(path3, pattern) {
59
+ const pathParts = path3.split("/");
82
60
  const patternParts = pattern.split("/");
83
61
  if (matchSegments(pathParts, patternParts)) {
84
62
  return "FULL";
@@ -342,311 +320,13 @@ function convertForExport(obj) {
342
320
  return obj;
343
321
  }
344
322
 
345
- // cli/generate-types.ts
346
- import { promises as fs2 } from "fs";
347
- import path2 from "path";
348
- import { fileURLToPath } from "url";
349
- import { loadRootConfig as loadRootConfig2, viteSsrLoadModule } from "@blinkk/root/node";
350
- import * as dom from "dts-dom";
351
- var __dirname = path2.dirname(fileURLToPath(import.meta.url));
352
- async function generateTypes() {
353
- const rootDir = process.cwd();
354
- const rootConfig = await loadRootConfig2(rootDir, { command: "root-cms" });
355
- const modulePath = path2.resolve(__dirname, "./project.js");
356
- const project = await viteSsrLoadModule(
357
- rootConfig,
358
- modulePath
359
- );
360
- const schemas = await project.getProjectSchemas();
361
- const outputPath = path2.resolve(rootDir, "root-cms.d.ts");
362
- await generateSchemaDts(outputPath, schemas);
363
- console.log("saved root-cms.d.ts!");
364
- }
365
- var TEMPLATE = `/* eslint-disable */
366
- /** Root.js CMS types. This file is autogenerated. */
367
-
368
- export interface RootCMSFile {
369
- src: string;
370
- width?: number;
371
- height?: number;
372
- alt?: string;
373
- }
374
-
375
- export type RootCMSImage = RootCMSFile;
376
-
377
- export type RootCMSOneOf<T = any> = T;
378
-
379
- export type RootCMSOneOfOption<T, Base> = Base & {_type: T};
380
-
381
- export interface RootCMSRichTextBlock {
382
- type: string;
383
- data: any;
384
- }
385
-
386
- export interface RootCMSRichText {
387
- blocks: RootCMSRichTextBlock[];
388
- }
389
-
390
- export interface RootCMSReference {
391
- /** The id of the doc, e.g. "Pages/foo-bar". */
392
- id: string;
393
- /** The collection id of the doc, e.g. "Pages". */
394
- collection: string;
395
- /** The slug of the doc, e.g. "foo-bar". */
396
- slug: string;
397
- }
398
-
399
- export interface RootCMSDoc<Fields extends {}> {
400
- /** The id of the doc, e.g. "Pages/foo-bar". */
401
- id: string;
402
- /** The collection id of the doc, e.g. "Pages". */
403
- collection: string;
404
- /** The slug of the doc, e.g. "foo-bar". */
405
- slug: string;
406
- /** System-level metadata. */
407
- sys: {
408
- createdAt: number;
409
- createdBy: string;
410
- modifiedAt: number;
411
- modifiedBy: string;
412
- firstPublishedAt?: number;
413
- firstPublishedBy?: string;
414
- publishedAt?: number;
415
- publishedBy?: string;
416
- locales?: string[];
417
- };
418
- /** User-entered field values from the CMS. */
419
- fields?: Fields;
420
- }`;
421
- var DtsFormatter = class {
422
- constructor() {
423
- /**
424
- * Field types defined in either `.schema.ts` files or used by `oneOf()`
425
- * fields.
426
- */
427
- this.fieldsTypes = {};
428
- /**
429
- * For `collections/*.schema.ts` files, output a corresponding "RootCMSDoc"
430
- * type. For example, `collections/BlogPosts.schema.ts` would output a type
431
- * called `BlogPostsDoc`.
432
- */
433
- this.docTypes = {};
434
- }
435
- /** Adds the types for a `.schema.ts` file to the `.d.ts` file. */
436
- addSchemaFile(fileId, schema) {
437
- const jsdoc = `Generated from \`${fileId}\`.`;
438
- const typeId = alphanumeric(path2.parse(fileId).name.split(".")[0]);
439
- const oneOfTypes = {};
440
- const fieldsTypeId = `${typeId}Fields`;
441
- const fieldsType = dom.create.interface(
442
- fieldsTypeId,
443
- dom.DeclarationFlags.Export
444
- );
445
- fieldsType.jsDocComment = jsdoc;
446
- for (const field of schema.fields) {
447
- if (!field.id) {
448
- continue;
449
- }
450
- fieldsType.members.push(fieldProperty(field, { typeId, oneOfTypes }));
451
- }
452
- for (const oneOfTypeId in oneOfTypes) {
453
- if (!this.fieldsTypes[fieldsTypeId]) {
454
- this.fieldsTypes[oneOfTypeId] = oneOfTypes[oneOfTypeId];
455
- }
456
- }
457
- this.fieldsTypes[fieldsTypeId] = fieldsType;
458
- if (fileId.startsWith("/collections/")) {
459
- const docTypeId = `${typeId}Doc`;
460
- const baseType = dom.create.namedTypeReference("RootCMSDoc");
461
- baseType.typeArguments.push(dom.create.namedTypeReference(fieldsTypeId));
462
- const docType = dom.create.alias(
463
- docTypeId,
464
- baseType,
465
- dom.DeclarationFlags.Export
466
- );
467
- docType.jsDocComment = jsdoc;
468
- this.docTypes[docTypeId] = docType;
469
- }
470
- }
471
- /** Generates the `.d.ts` output as a string. */
472
- toString() {
473
- const results = [TEMPLATE];
474
- const sortedFieldsTypes = Object.keys(this.fieldsTypes).sort();
475
- for (const fieldsTypeId of sortedFieldsTypes) {
476
- const fieldsType = this.fieldsTypes[fieldsTypeId];
477
- results.push(this.typeToString(fieldsType));
478
- const docTypeId = this.fieldsTypeToDocType(fieldsTypeId);
479
- const docType = this.docTypes[docTypeId];
480
- if (docType) {
481
- results.push(this.typeToString(docType));
482
- }
483
- }
484
- const output = results.join("\n\n").replaceAll("/** ", "/** ").replaceAll(" */", " */").replace(/\r\n|\r|\n/g, "\n") + "\n";
485
- return output;
486
- }
487
- /**
488
- * Converts "<Name>Fields" to "<Name>Doc".
489
- */
490
- fieldsTypeToDocType(fieldsTypeId) {
491
- if (!fieldsTypeId.endsWith("Fields")) {
492
- throw new Error(`"${fieldsTypeId}" should be suffixed with "Fields"`);
493
- }
494
- const name = fieldsTypeId.slice(0, -6);
495
- return `${name}Doc`;
496
- }
497
- typeToString(type2) {
498
- return this.reformatOutput(dom.emit(type2, { singleLineJsDocComments: true }));
499
- }
500
- /**
501
- * Formats the output to Google style conventions, e.g. 2-space indents and
502
- * single quote strings.
503
- */
504
- reformatOutput(input) {
505
- const lines = input.trim().split("\n");
506
- const results = [];
507
- for (const line of lines) {
508
- const convertedLine = line.replace(/ {4}/g, " ").replaceAll('"', "'");
509
- results.push(convertedLine);
510
- }
511
- return results.join("\n");
512
- }
513
- };
514
- async function generateSchemaDts(outputPath, schemas) {
515
- const dtsFormatter = new DtsFormatter();
516
- for (const fileId in schemas) {
517
- const schema = schemas[fileId];
518
- dtsFormatter.addSchemaFile(fileId, schema);
519
- }
520
- const output = dtsFormatter.toString();
521
- await fs2.writeFile(outputPath, output, "utf-8");
522
- }
523
- function fieldProperty(field, options) {
524
- const prop = dom.create.property(
525
- field.id,
526
- fieldType(field, options),
527
- dom.DeclarationFlags.Optional
528
- );
529
- const jsdoc = [];
530
- if (field.label) {
531
- if (field.help) {
532
- jsdoc.push(`${field.label}. ${field.help}`);
533
- } else {
534
- jsdoc.push(field.label);
535
- }
536
- }
537
- if (field.deprecated) {
538
- jsdoc.push("@deprecated");
539
- }
540
- if (jsdoc.length > 0) {
541
- prop.jsDocComment = jsdoc.join("\n");
542
- }
543
- return prop;
544
- }
545
- function fieldType(field, options) {
546
- if (field.type === "array") {
547
- return dom.type.array(fieldType(field.of, options));
548
- }
549
- if (field.type === "boolean") {
550
- return dom.type.boolean;
551
- }
552
- if (field.type === "date") {
553
- return dom.type.string;
554
- }
555
- if (field.type === "datetime") {
556
- return dom.type.number;
557
- }
558
- if (field.type === "file") {
559
- const fileType = dom.create.namedTypeReference("RootCMSFile");
560
- return fileType;
561
- }
562
- if (field.type === "image") {
563
- const imageType = dom.create.namedTypeReference("RootCMSImage");
564
- return imageType;
565
- }
566
- if (field.type === "multiselect") {
567
- return dom.type.array(dom.type.string);
568
- }
569
- if (field.type === "oneof") {
570
- const oneOf = dom.create.namedTypeReference("RootCMSOneOf");
571
- if (field.types && Array.isArray(field.types)) {
572
- const unionTypes = [];
573
- field.types.forEach((schema) => {
574
- let typeName;
575
- if (typeof schema === "string") {
576
- typeName = schema;
577
- return;
578
- } else {
579
- typeName = schema.name;
580
- }
581
- if (!typeName) {
582
- return;
583
- }
584
- const cleanName = alphanumeric(typeName);
585
- const oneOfTypeId = `${cleanName}Fields`;
586
- if (typeof schema === "object" && !options.oneOfTypes[oneOfTypeId]) {
587
- const oneOfTypeInterface = dom.create.interface(
588
- oneOfTypeId,
589
- dom.DeclarationFlags.Export
590
- );
591
- if (schema.description) {
592
- oneOfTypeInterface.jsDocComment = schema.description;
593
- }
594
- const oneOfTypeFields = schema.fields || [];
595
- oneOfTypeFields.forEach((f) => {
596
- oneOfTypeInterface.members.push(fieldProperty(f, options));
597
- });
598
- options.oneOfTypes[oneOfTypeId] = oneOfTypeInterface;
599
- }
600
- const oneOfOption = dom.create.namedTypeReference("RootCMSOneOfOption");
601
- oneOfOption.typeArguments = [
602
- dom.type.stringLiteral(typeName),
603
- dom.create.namedTypeReference(oneOfTypeId)
604
- ];
605
- unionTypes.push(oneOfOption);
606
- });
607
- if (unionTypes.length > 0) {
608
- oneOf.typeArguments = [dom.create.union(unionTypes)];
609
- }
610
- }
611
- return oneOf;
612
- }
613
- if (field.type === "reference") {
614
- const referenceType = dom.create.namedTypeReference("RootCMSReference");
615
- return referenceType;
616
- }
617
- if (field.type === "references") {
618
- const referenceType = dom.create.namedTypeReference("RootCMSReference");
619
- return dom.type.array(referenceType);
620
- }
621
- if (field.type === "richtext") {
622
- const richtextType = dom.create.namedTypeReference("RootCMSRichText");
623
- return richtextType;
624
- }
625
- if (field.type === "select") {
626
- return dom.type.string;
627
- }
628
- if (field.type === "string") {
629
- return dom.type.string;
630
- }
631
- if (field.type === "object") {
632
- const subproperties = (field.fields || []).map(
633
- (f) => fieldProperty(f, options)
634
- );
635
- return dom.create.objectType(subproperties);
636
- }
637
- return dom.type.unknown;
638
- }
639
- function alphanumeric(input) {
640
- return input.replace(/[^a-zA-Z0-9]/g, "");
641
- }
642
-
643
323
  // cli/import.ts
644
- import fs3 from "fs";
645
- import path3 from "path";
324
+ import fs2 from "fs";
325
+ import path2 from "path";
646
326
  import * as readline from "readline";
647
- import { loadRootConfig as loadRootConfig3 } from "@blinkk/root/node";
327
+ import { loadRootConfig as loadRootConfig2 } from "@blinkk/root/node";
648
328
  import cliProgress from "cli-progress";
649
- import { Timestamp as Timestamp3, GeoPoint } from "firebase-admin/firestore";
329
+ import { Timestamp, GeoPoint } from "firebase-admin/firestore";
650
330
  async function importData(options) {
651
331
  if (!options.dir) {
652
332
  throw new Error(
@@ -654,18 +334,18 @@ async function importData(options) {
654
334
  );
655
335
  }
656
336
  const importDir = options.dir;
657
- if (!fs3.existsSync(importDir)) {
337
+ if (!fs2.existsSync(importDir)) {
658
338
  throw new Error(`Error: Directory not found: ${importDir}`);
659
339
  }
660
340
  const rootDir = process.cwd();
661
- const rootConfig = await loadRootConfig3(rootDir, { command: "root-cms" });
341
+ const rootConfig = await loadRootConfig2(rootDir, { command: "root-cms" });
662
342
  const cmsPlugin = getCmsPlugin(rootConfig);
663
343
  const cmsPluginOptions = cmsPlugin.getConfig();
664
344
  const siteId = options.site || cmsPluginOptions.id || "default";
665
345
  const databaseId = options.database || "(default)";
666
346
  const gcpProjectId = options.project || cmsPluginOptions.firebaseConfig?.projectId || "unknown";
667
347
  const db = cmsPlugin.getFirestore({ databaseId });
668
- const entries = fs3.readdirSync(importDir, { withFileTypes: true });
348
+ const entries = fs2.readdirSync(importDir, { withFileTypes: true });
669
349
  const availableCollections = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
670
350
  const { includes, excludes } = parseFilters(options.filter);
671
351
  const collectionsToImport = availableCollections.filter((id) => {
@@ -681,8 +361,8 @@ async function importData(options) {
681
361
  }
682
362
  const summary = [];
683
363
  for (const collectionType of collectionsToImport) {
684
- const collectionDir = path3.join(importDir, collectionType);
685
- if (fs3.existsSync(collectionDir)) {
364
+ const collectionDir = path2.join(importDir, collectionType);
365
+ if (fs2.existsSync(collectionDir)) {
686
366
  const count = countJsonFilesRecursive(
687
367
  collectionDir,
688
368
  collectionType,
@@ -721,7 +401,7 @@ async function importData(options) {
721
401
  const limit = pLimit(10);
722
402
  for (const item of summary) {
723
403
  const collectionPath = `Projects/${siteId}/${item.collectionType}`;
724
- const collectionDir = path3.join(importDir, item.collectionType);
404
+ const collectionDir = path2.join(importDir, item.collectionType);
725
405
  await importCollection(
726
406
  db,
727
407
  collectionPath,
@@ -734,10 +414,10 @@ async function importData(options) {
734
414
  limit
735
415
  );
736
416
  }
737
- const projectFilePath = path3.join(importDir, "__data.json");
738
- if (fs3.existsSync(projectFilePath)) {
417
+ const projectFilePath = path2.join(importDir, "__data.json");
418
+ if (fs2.existsSync(projectFilePath)) {
739
419
  console.log("Importing project document...");
740
- const rawData = JSON.parse(fs3.readFileSync(projectFilePath, "utf-8"));
420
+ const rawData = JSON.parse(fs2.readFileSync(projectFilePath, "utf-8"));
741
421
  const projectData = convertFirestoreTypes(rawData, db);
742
422
  const projectPath = `Projects/${siteId}`;
743
423
  await db.doc(projectPath).set(projectData, { merge: true });
@@ -747,14 +427,14 @@ async function importData(options) {
747
427
  }
748
428
  function countJsonFilesRecursive(dir, relativePath, includes, excludes) {
749
429
  let count = 0;
750
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
430
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
751
431
  for (const entry of entries) {
752
432
  if (entry.isDirectory()) {
753
433
  const subRelativePath = `${relativePath}/${entry.name}`;
754
434
  const status = getPathStatus(subRelativePath, includes, excludes);
755
435
  if (status !== "SKIP" && status !== "EXCLUDE") {
756
436
  count += countJsonFilesRecursive(
757
- path3.join(dir, entry.name),
437
+ path2.join(dir, entry.name),
758
438
  subRelativePath,
759
439
  includes,
760
440
  excludes
@@ -768,7 +448,7 @@ function countJsonFilesRecursive(dir, relativePath, includes, excludes) {
768
448
  }
769
449
  continue;
770
450
  }
771
- const docId = path3.basename(entry.name, ".json");
451
+ const docId = path2.basename(entry.name, ".json");
772
452
  const docRelativePath = `${relativePath}/${docId}`;
773
453
  const status = getPathStatus(docRelativePath, includes, excludes);
774
454
  if (status !== "SKIP" && status !== "EXCLUDE") {
@@ -805,7 +485,7 @@ function convertFirestoreTypes(obj, db) {
805
485
  return obj;
806
486
  }
807
487
  if (typeof obj === "object" && "_seconds" in obj && "_nanoseconds" in obj && Object.keys(obj).length === 2) {
808
- return new Timestamp3(obj._seconds, obj._nanoseconds);
488
+ return new Timestamp(obj._seconds, obj._nanoseconds);
809
489
  }
810
490
  if (typeof obj === "object" && "_latitude" in obj && "_longitude" in obj && Object.keys(obj).length === 2) {
811
491
  return new GeoPoint(obj._latitude, obj._longitude);
@@ -826,15 +506,15 @@ function convertFirestoreTypes(obj, db) {
826
506
  return obj;
827
507
  }
828
508
  async function importCollectionRecursive(db, collectionPath, inputDir, progressBar, relativePath, includes, excludes, limit) {
829
- if (!fs3.existsSync(inputDir)) {
509
+ if (!fs2.existsSync(inputDir)) {
830
510
  return;
831
511
  }
832
- const entries = fs3.readdirSync(inputDir, { withFileTypes: true });
512
+ const entries = fs2.readdirSync(inputDir, { withFileTypes: true });
833
513
  await Promise.all(
834
514
  entries.map(async (entry) => {
835
515
  if (entry.isFile() && entry.name.endsWith(".json")) {
836
516
  const rawData = JSON.parse(
837
- fs3.readFileSync(path3.join(inputDir, entry.name), "utf-8")
517
+ fs2.readFileSync(path2.join(inputDir, entry.name), "utf-8")
838
518
  );
839
519
  const docData = convertFirestoreTypes(rawData, db);
840
520
  if (entry.name === "__data.json") {
@@ -849,7 +529,7 @@ async function importCollectionRecursive(db, collectionPath, inputDir, progressB
849
529
  }
850
530
  }
851
531
  } else {
852
- const docId = path3.basename(entry.name, ".json");
532
+ const docId = path2.basename(entry.name, ".json");
853
533
  const docRelativePath = relativePath ? `${relativePath}/${docId}` : docId;
854
534
  const status = includes && excludes ? getPathStatus(docRelativePath, includes, excludes) : "INCLUDE";
855
535
  if (status !== "SKIP" && status !== "EXCLUDE") {
@@ -867,7 +547,7 @@ async function importCollectionRecursive(db, collectionPath, inputDir, progressB
867
547
  }
868
548
  } else if (entry.isDirectory()) {
869
549
  const subCollectionPath = `${collectionPath}/${entry.name}`;
870
- const subCollectionDir = path3.join(inputDir, entry.name);
550
+ const subCollectionDir = path2.join(inputDir, entry.name);
871
551
  const subRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
872
552
  const status = includes && excludes ? getPathStatus(subRelativePath, includes, excludes) : "INCLUDE";
873
553
  if (status !== "SKIP" && status !== "EXCLUDE") {
@@ -901,7 +581,7 @@ async function promptYesNo(question) {
901
581
  }
902
582
 
903
583
  // cli/init-firebase.ts
904
- import { loadRootConfig as loadRootConfig4 } from "@blinkk/root/node";
584
+ import { loadRootConfig as loadRootConfig3 } from "@blinkk/root/node";
905
585
 
906
586
  // core/security.ts
907
587
  import { initializeApp } from "firebase-admin/app";
@@ -921,11 +601,16 @@ service cloud.firestore {
921
601
 
922
602
  match /{collection}/{document=**} {
923
603
  allow write:
924
- if isSignedIn() && userCanWrite();
604
+ if isSignedIn() && userCanPublish();
925
605
  allow read:
926
606
  if isSignedIn() && userCanRead();
927
607
  }
928
608
 
609
+ match /Collections/{collectionId}/Drafts/{document=**} {
610
+ allow write:
611
+ if isSignedIn() && userCanEdit();
612
+ }
613
+
929
614
  function isSignedIn() {
930
615
  return request.auth != null;
931
616
  }
@@ -938,16 +623,23 @@ service cloud.firestore {
938
623
  let roles = getRoles();
939
624
  let email = request.auth.token.email;
940
625
  let domain = '*@' + email.split('@')[1];
941
- return (roles[email] in ['ADMIN', 'EDITOR', 'VIEWER']) || (roles[domain] in ['ADMIN', 'EDITOR', 'VIEWER']);
626
+ return (roles[email] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR', 'VIEWER']) || (roles[domain] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR', 'VIEWER']);
942
627
  }
943
628
 
944
- function userCanWrite() {
629
+ function userCanPublish() {
945
630
  let roles = getRoles();
946
631
  let email = request.auth.token.email;
947
632
  let domain = '*@' + email.split('@')[1];
948
633
  return (roles[email] in ['ADMIN', 'EDITOR']) || (roles[domain] in ['ADMIN', 'EDITOR']);
949
634
  }
950
635
 
636
+ function userCanEdit() {
637
+ let roles = getRoles();
638
+ let email = request.auth.token.email;
639
+ let domain = '*@' + email.split('@')[1];
640
+ return (roles[email] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR']) || (roles[domain] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR']);
641
+ }
642
+
951
643
  function userIsAdmin() {
952
644
  let roles = getRoles();
953
645
  let email = request.auth.token.email;
@@ -967,7 +659,7 @@ async function applySecurityRules(projectId) {
967
659
  // cli/init-firebase.ts
968
660
  async function initFirebase(options) {
969
661
  const rootDir = process.cwd();
970
- const rootConfig = await loadRootConfig4(rootDir, { command: "root-cms" });
662
+ const rootConfig = await loadRootConfig3(rootDir, { command: "root-cms" });
971
663
  const cmsPlugin = getCmsPlugin(rootConfig);
972
664
  const cmsPluginOptions = cmsPlugin.getConfig();
973
665
  const gcpProjectId = options.project || cmsPluginOptions.firebaseConfig.projectId;
@@ -1,6 +1,7 @@
1
1
  import { Plugin, Request, RootConfig } from '@blinkk/root';
2
2
  import { App } from 'firebase-admin/app';
3
3
  import { Firestore, WriteBatch, Timestamp, Query } from 'firebase-admin/firestore';
4
+ import { C as Collection } from './schema-Bux4PrV2.js';
4
5
 
5
6
  /**
6
7
  * Supported Root AI models. Defaults to 'gemini-3-flash-preview'.
@@ -385,7 +386,7 @@ interface Doc<Fields = any> {
385
386
  fields: Fields;
386
387
  }
387
388
  type DocMode = 'draft' | 'published';
388
- type UserRole = 'ADMIN' | 'EDITOR' | 'VIEWER';
389
+ type UserRole = 'ADMIN' | 'EDITOR' | 'CONTRIBUTOR' | 'VIEWER';
389
390
  type HttpMethod = 'GET' | 'POST';
390
391
  interface DataSource {
391
392
  id: string;
@@ -436,6 +437,18 @@ interface SaveDraftOptions {
436
437
  * Email of user modifying the doc. If blank, defaults to `root-cms-client`.
437
438
  */
438
439
  modifiedBy?: string;
440
+ /**
441
+ * Whether to validate fieldsData against the collection schema before saving.
442
+ * If validation fails, an error will be thrown with details about the validation errors.
443
+ */
444
+ validate?: boolean;
445
+ }
446
+ interface UpdateDraftOptions {
447
+ /**
448
+ * Whether to validate the updated field against the collection schema.
449
+ * If validation fails, an error will be thrown with details about the validation errors.
450
+ */
451
+ validate?: boolean;
439
452
  }
440
453
  interface ListDocsOptions {
441
454
  mode: DocMode;
@@ -553,6 +566,11 @@ declare class RootCMSClient {
553
566
  dbDocRef(collectionId: string, slug: string, options: {
554
567
  mode: 'draft' | 'published';
555
568
  }): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
569
+ /**
570
+ * Returns a collection's schema definition as defined in
571
+ * `/collections/<id>.schema.ts`.
572
+ */
573
+ getCollection(collectionId: string): Promise<Collection | null>;
556
574
  /**
557
575
  * Saves draft data to a doc.
558
576
  *
@@ -560,6 +578,18 @@ declare class RootCMSClient {
560
578
  * modify the sys-level attributes of the doc, use `setRawDoc()`.
561
579
  */
562
580
  saveDraftData(docId: string, fieldsData: any, options?: SaveDraftOptions): Promise<void>;
581
+ /**
582
+ * Updates a specific field path in a draft doc.
583
+ *
584
+ * This allows partial updates to nested fields without replacing the entire document.
585
+ * For example: `updateDraftData('Pages/home', 'hero.title', 'New Title')`
586
+ *
587
+ * @param docId - The document ID (e.g., 'Pages/home')
588
+ * @param path - JSON path to the field (e.g., 'hero.title' or 'content.0.text')
589
+ * @param fieldValue - The value to set at the specified path
590
+ * @param options - Update options including validation
591
+ */
592
+ updateDraftData(docId: string, path: string, fieldValue: any, options?: UpdateDraftOptions): Promise<void>;
563
593
  /**
564
594
  * Prefer `saveDraftData('Pages/foo', data)`. Only use this if you know what
565
595
  * you're doing.
@@ -896,4 +926,4 @@ declare class BatchResponse {
896
926
  private getTranslationsMap;
897
927
  }
898
928
 
899
- export { type CMSPlugin as $, type Action as A, type BatchRequestOptions as B, BatchResponse as C, type DocMode as D, type Locale as E, type SourceString as F, type GetDocOptions as G, type HttpMethod as H, type TranslatedString as I, type TranslationsDocMode as J, type TranslationsLocaleDocHashMap as K, type LoadTranslationsOptions as L, type TranslationsLocaleDocEntry as M, type MultiLocaleTranslationsMap as N, type SingleLocaleTranslationsMap as O, TranslationsManager as P, buildTranslationsDbPath as Q, RootCMSClient as R, type SetDocOptions as S, type TranslationsMap as T, type UserRole as U, buildTranslationsLocaleDocDbPath as V, type CMSBuiltInSidebarTool as W, type CMSUser as X, type CMSAIConfig as Y, type CMSSidebarTool as Z, type CMSPluginOptions as _, type LocaleTranslations as a, cmsPlugin as a0, type Doc as b, type DataSource as c, type DataSourceData as d, type DataSourceMode as e, type SaveDraftOptions as f, type ListDocsOptions as g, type GetCountOptions as h, type Translation as i, type Release as j, type ListActionsOptions as k, isRichTextData as l, getCmsPlugin as m, marshalData as n, normalizeData as o, type ArrayObject as p, marshalArray as q, unmarshalArray as r, translationsForLocale as s, toArrayObject as t, unmarshalData as u, parseDocId as v, type BatchRequestQuery as w, type BatchRequestQueryOptions as x, type TranslationsDoc as y, BatchRequest as z };
929
+ export { type CMSPluginOptions as $, type Action as A, type BatchRequestOptions as B, BatchRequest as C, type DocMode as D, BatchResponse as E, type Locale as F, type GetDocOptions as G, type HttpMethod as H, type SourceString as I, type TranslatedString as J, type TranslationsDocMode as K, type LoadTranslationsOptions as L, type TranslationsLocaleDocHashMap as M, type TranslationsLocaleDocEntry as N, type MultiLocaleTranslationsMap as O, type SingleLocaleTranslationsMap as P, TranslationsManager as Q, RootCMSClient as R, type SetDocOptions as S, type TranslationsMap as T, type UserRole as U, buildTranslationsDbPath as V, buildTranslationsLocaleDocDbPath as W, type CMSBuiltInSidebarTool as X, type CMSUser as Y, type CMSAIConfig as Z, type CMSSidebarTool as _, type LocaleTranslations as a, type CMSPlugin as a0, cmsPlugin as a1, type Doc as b, type DataSource as c, type DataSourceData as d, type DataSourceMode as e, type SaveDraftOptions as f, type UpdateDraftOptions as g, type ListDocsOptions as h, type GetCountOptions as i, type Translation as j, type Release as k, type ListActionsOptions as l, isRichTextData as m, getCmsPlugin as n, marshalData as o, normalizeData as p, type ArrayObject as q, marshalArray as r, unmarshalArray as s, toArrayObject as t, unmarshalData as u, translationsForLocale as v, parseDocId as w, type BatchRequestQuery as x, type BatchRequestQueryOptions as y, type TranslationsDoc as z };
package/dist/client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import '@blinkk/root';
2
2
  import 'firebase-admin/app';
3
3
  import 'firebase-admin/firestore';
4
- export { A as Action, p as ArrayObject, z as BatchRequest, B as BatchRequestOptions, w as BatchRequestQuery, x as BatchRequestQueryOptions, C as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, b as Doc, D as DocMode, h as GetCountOptions, G as GetDocOptions, H as HttpMethod, k as ListActionsOptions, g as ListDocsOptions, L as LoadTranslationsOptions, a as LocaleTranslations, j as Release, R as RootCMSClient, f as SaveDraftOptions, S as SetDocOptions, i as Translation, y as TranslationsDoc, T as TranslationsMap, U as UserRole, m as getCmsPlugin, l as isRichTextData, q as marshalArray, n as marshalData, o as normalizeData, v as parseDocId, t as toArrayObject, s as translationsForLocale, r as unmarshalArray, u as unmarshalData } from './client-pSzji9ZN.js';
4
+ export { A as Action, q as ArrayObject, C as BatchRequest, B as BatchRequestOptions, x as BatchRequestQuery, y as BatchRequestQueryOptions, E as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, b as Doc, D as DocMode, i as GetCountOptions, G as GetDocOptions, H as HttpMethod, l as ListActionsOptions, h as ListDocsOptions, L as LoadTranslationsOptions, a as LocaleTranslations, k as Release, R as RootCMSClient, f as SaveDraftOptions, S as SetDocOptions, j as Translation, z as TranslationsDoc, T as TranslationsMap, g as UpdateDraftOptions, U as UserRole, n as getCmsPlugin, m as isRichTextData, r as marshalArray, o as marshalData, p as normalizeData, w as parseDocId, t as toArrayObject, v as translationsForLocale, s as unmarshalArray, u as unmarshalData } from './client-PhodvL2Q.js';
5
+ import './schema-Bux4PrV2.js';