@blinkk/root-cms 3.0.1-alpha.1 → 3.0.1-beta.2

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  RootCMSClient,
3
3
  getCmsPlugin
4
- } from "./chunk-SNZ4S4IC.js";
4
+ } from "./chunk-F4SODS5S.js";
5
5
 
6
6
  // core/cron.ts
7
7
  import { Timestamp as Timestamp3 } from "firebase-admin/firestore";
@@ -264,6 +264,181 @@ function extractDocRecords(schema, options) {
264
264
  return records;
265
265
  }
266
266
 
267
+ // core/search-query.ts
268
+ var SMART_QUOTES = /[“”„«»]/g;
269
+ function parseQuery(input) {
270
+ const out = {
271
+ phrases: [],
272
+ terms: [],
273
+ excluded: [],
274
+ excludedPhrases: []
275
+ };
276
+ if (!input) {
277
+ return out;
278
+ }
279
+ const s = input.replace(SMART_QUOTES, '"');
280
+ let i = 0;
281
+ while (i < s.length) {
282
+ while (i < s.length && isSpace(s[i])) {
283
+ i++;
284
+ }
285
+ if (i >= s.length) {
286
+ break;
287
+ }
288
+ let negative = false;
289
+ if (s[i] === "-" && i + 1 < s.length && !isSpace(s[i + 1])) {
290
+ negative = true;
291
+ i++;
292
+ }
293
+ if (s[i] === '"') {
294
+ i++;
295
+ const start = i;
296
+ while (i < s.length && s[i] !== '"') {
297
+ i++;
298
+ }
299
+ const phrase = s.slice(start, i).trim();
300
+ if (i < s.length) {
301
+ i++;
302
+ }
303
+ if (phrase) {
304
+ if (negative) {
305
+ out.excludedPhrases.push(phrase);
306
+ } else {
307
+ out.phrases.push(phrase);
308
+ }
309
+ }
310
+ } else {
311
+ const start = i;
312
+ while (i < s.length && !isSpace(s[i]) && s[i] !== '"') {
313
+ i++;
314
+ }
315
+ const term = s.slice(start, i);
316
+ if (term && term.replace(/^-+$/, "")) {
317
+ if (negative) {
318
+ out.excluded.push(term);
319
+ } else {
320
+ out.terms.push(term);
321
+ }
322
+ }
323
+ }
324
+ }
325
+ return out;
326
+ }
327
+ function isSpace(ch) {
328
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r";
329
+ }
330
+ function tokenize(s) {
331
+ return s.toLowerCase().split(/[\s\p{P}]+/u).filter(Boolean);
332
+ }
333
+ function containsPhrase(text, phrase) {
334
+ const phraseTokens = tokenize(phrase);
335
+ if (phraseTokens.length === 0) {
336
+ return true;
337
+ }
338
+ const textTokens = tokenize(text);
339
+ if (textTokens.length < phraseTokens.length) {
340
+ return false;
341
+ }
342
+ outer: for (let i = 0; i <= textTokens.length - phraseTokens.length; i++) {
343
+ for (let j = 0; j < phraseTokens.length; j++) {
344
+ if (textTokens[i + j] !== phraseTokens[j]) {
345
+ continue outer;
346
+ }
347
+ }
348
+ return true;
349
+ }
350
+ return false;
351
+ }
352
+ function isEmptyQuery(parsed) {
353
+ return parsed.phrases.length === 0 && parsed.terms.length === 0;
354
+ }
355
+ function executeQuery(index, parsed) {
356
+ if (isEmptyQuery(parsed)) {
357
+ return [];
358
+ }
359
+ let acc = null;
360
+ for (const phrase of parsed.phrases) {
361
+ const candidates = index.search(phrase, {
362
+ combineWith: "AND",
363
+ fuzzy: false,
364
+ prefix: false
365
+ });
366
+ const matched = candidates.filter(
367
+ (r) => containsPhrase(String(r.text || ""), phrase) || containsPhrase(String(r.fieldLabel || ""), phrase)
368
+ );
369
+ acc = mergeAnd(acc, matched);
370
+ if (acc.size === 0) {
371
+ return [];
372
+ }
373
+ }
374
+ if (parsed.terms.length > 0) {
375
+ const q = parsed.terms.join(" ");
376
+ const results = index.search(q, { combineWith: "AND" });
377
+ acc = mergeAnd(acc, results);
378
+ }
379
+ if (!acc || acc.size === 0) {
380
+ return [];
381
+ }
382
+ for (const term of parsed.excluded) {
383
+ for (const r of index.search(term)) {
384
+ acc.delete(String(r.id));
385
+ }
386
+ }
387
+ for (const phrase of parsed.excludedPhrases) {
388
+ const candidates = index.search(phrase, {
389
+ combineWith: "AND",
390
+ fuzzy: false,
391
+ prefix: false
392
+ });
393
+ for (const r of candidates) {
394
+ if (containsPhrase(String(r.text || ""), phrase) || containsPhrase(String(r.fieldLabel || ""), phrase)) {
395
+ acc.delete(String(r.id));
396
+ }
397
+ }
398
+ }
399
+ return Array.from(acc.values()).sort((a, b) => b.score - a.score).map((entry) => ({
400
+ ...entry.record,
401
+ score: entry.score,
402
+ terms: Array.from(entry.terms)
403
+ }));
404
+ }
405
+ function mergeAnd(acc, results) {
406
+ if (acc === null) {
407
+ const next2 = /* @__PURE__ */ new Map();
408
+ for (const r of results) {
409
+ next2.set(String(r.id), {
410
+ score: r.score,
411
+ record: r,
412
+ terms: new Set(Array.isArray(r.terms) ? r.terms : [])
413
+ });
414
+ }
415
+ return next2;
416
+ }
417
+ const byId = /* @__PURE__ */ new Map();
418
+ for (const r of results) {
419
+ byId.set(String(r.id), r);
420
+ }
421
+ const next = /* @__PURE__ */ new Map();
422
+ for (const [id, entry] of acc) {
423
+ const r = byId.get(id);
424
+ if (!r) {
425
+ continue;
426
+ }
427
+ const terms = new Set(entry.terms);
428
+ if (Array.isArray(r.terms)) {
429
+ for (const t of r.terms) {
430
+ terms.add(t);
431
+ }
432
+ }
433
+ next.set(id, {
434
+ score: entry.score + r.score,
435
+ record: entry.record,
436
+ terms
437
+ });
438
+ }
439
+ return next;
440
+ }
441
+
267
442
  // core/search-index.ts
268
443
  var SHARD_MAX_CHARS = 5e5;
269
444
  var MAX_INDEX_SIZE_BYTES = 50 * 1024 * 1024;
@@ -290,6 +465,46 @@ var MINISEARCH_OPTIONS = {
290
465
  fuzzy: 0.2
291
466
  }
292
467
  };
468
+ function toSetOrNull(values) {
469
+ if (!values || values.length === 0) {
470
+ return null;
471
+ }
472
+ return new Set(values);
473
+ }
474
+ function resolveSearchIndexFilters(config) {
475
+ return {
476
+ includeCollections: toSetOrNull(config?.includeCollections),
477
+ excludeCollections: toSetOrNull(config?.excludeCollections),
478
+ includeDocIds: toSetOrNull(config?.includeDocIds),
479
+ excludeDocIds: toSetOrNull(config?.excludeDocIds)
480
+ };
481
+ }
482
+ function isCollectionIndexable(filters, collectionId) {
483
+ if (filters.includeCollections && !filters.includeCollections.has(collectionId)) {
484
+ return false;
485
+ }
486
+ if (filters.excludeCollections?.has(collectionId)) {
487
+ return false;
488
+ }
489
+ return true;
490
+ }
491
+ function isDocIndexable(filters, docId) {
492
+ const slashIndex = docId.indexOf("/");
493
+ if (slashIndex < 0) {
494
+ return false;
495
+ }
496
+ const collectionId = docId.slice(0, slashIndex);
497
+ if (!isCollectionIndexable(filters, collectionId)) {
498
+ return false;
499
+ }
500
+ if (filters.includeDocIds && !filters.includeDocIds.has(docId)) {
501
+ return false;
502
+ }
503
+ if (filters.excludeDocIds?.has(docId)) {
504
+ return false;
505
+ }
506
+ return true;
507
+ }
293
508
  function isTitleyDeepKey(deepKey) {
294
509
  const last = deepKey.split(".").pop() || "";
295
510
  return TITLEY_KEYS.has(last.toLowerCase());
@@ -298,7 +513,7 @@ function withWeight(rec) {
298
513
  return { ...rec, weight: isTitleyDeepKey(rec.deepKey) ? 2 : 1 };
299
514
  }
300
515
  var SearchIndexService = class {
301
- constructor(rootConfig, loadSchema) {
516
+ constructor(rootConfig, loadSchema, filtersOverride) {
302
517
  this.cached = null;
303
518
  this.rootConfig = rootConfig;
304
519
  const cmsPlugin = getCmsPlugin(rootConfig);
@@ -306,6 +521,20 @@ var SearchIndexService = class {
306
521
  this.projectId = cmsPluginOptions.id || "default";
307
522
  this.db = cmsPlugin.getFirestore();
308
523
  this.loadSchema = loadSchema || ((id) => this.loadCollectionSchemaFromDist(id));
524
+ this.filters = resolveSearchIndexFilters(
525
+ filtersOverride ?? cmsPluginOptions.searchIndex
526
+ );
527
+ }
528
+ /** Returns true if the given collection passes the include/exclude filter. */
529
+ isCollectionIndexable(collectionId) {
530
+ return isCollectionIndexable(this.filters, collectionId);
531
+ }
532
+ /**
533
+ * Returns true if the given doc id passes the include/exclude filter (and
534
+ * its collection is itself indexable). Doc ids are `<collectionId>/<slug>`.
535
+ */
536
+ isDocIndexable(docId) {
537
+ return isDocIndexable(this.filters, docId);
309
538
  }
310
539
  /** Returns the MiniSearch options used by both writers and readers. */
311
540
  static getMiniSearchOptions() {
@@ -317,14 +546,17 @@ var SearchIndexService = class {
317
546
  indexCollectionPath() {
318
547
  return `Projects/${this.projectId}/${SEARCH_INDEX_SUBCOLLECTION}`;
319
548
  }
320
- /** Lists collection ids by globbing `<rootDir>/collections/*.schema.ts`. */
549
+ /**
550
+ * Lists collection ids by globbing `<rootDir>/collections/*.schema.ts`,
551
+ * filtered by the include/exclude config.
552
+ */
321
553
  async listCollectionIds() {
322
554
  const collectionsDir = path.join(this.rootConfig.rootDir, "collections");
323
555
  if (!fs.existsSync(collectionsDir)) {
324
556
  return [];
325
557
  }
326
558
  const fileNames = await glob("*.schema.ts", { cwd: collectionsDir });
327
- return fileNames.map((f) => f.slice(0, -10));
559
+ return fileNames.map((f) => f.slice(0, -10)).filter((id) => this.isCollectionIndexable(id));
328
560
  }
329
561
  /**
330
562
  * Default schema loader: reads `dist/collections/<id>.schema.json`. These
@@ -438,23 +670,33 @@ var SearchIndexService = class {
438
670
  snap.forEach((d) => batch.delete(d.ref));
439
671
  await batch.commit();
440
672
  }
441
- /** Lists the full set of live (collection, slug, doc) tuples for `Drafts`. */
673
+ /**
674
+ * Lists the full set of live (collection, slug, doc) tuples for `Drafts`,
675
+ * filtered by the include/exclude config.
676
+ */
442
677
  async listAllDocs(collectionIds) {
443
678
  const all = [];
444
679
  for (const collectionId of collectionIds) {
445
680
  const path3 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
446
681
  const snap = await this.db.collection(path3).get();
447
682
  snap.forEach((d) => {
683
+ const docId = `${collectionId}/${d.id}`;
684
+ if (!this.isDocIndexable(docId)) {
685
+ return;
686
+ }
448
687
  const data = d.data() || {};
449
688
  if (!data.collection) data.collection = collectionId;
450
689
  if (!data.slug) data.slug = d.id;
451
- if (!data.id) data.id = `${collectionId}/${d.id}`;
690
+ if (!data.id) data.id = docId;
452
691
  all.push(data);
453
692
  });
454
693
  }
455
694
  return all;
456
695
  }
457
- /** Lists docs modified since `lastRun` across every collection. */
696
+ /**
697
+ * Lists docs modified since `lastRun` across every collection, filtered by
698
+ * the include/exclude config.
699
+ */
458
700
  async listChangedDocs(collectionIds, lastRun) {
459
701
  const all = [];
460
702
  for (const collectionId of collectionIds) {
@@ -462,10 +704,14 @@ var SearchIndexService = class {
462
704
  const q = this.db.collection(path3).where("sys.modifiedAt", ">=", lastRun);
463
705
  const snap = await q.get();
464
706
  snap.forEach((d) => {
707
+ const docId = `${collectionId}/${d.id}`;
708
+ if (!this.isDocIndexable(docId)) {
709
+ return;
710
+ }
465
711
  const data = d.data() || {};
466
712
  if (!data.collection) data.collection = collectionId;
467
713
  if (!data.slug) data.slug = d.id;
468
- if (!data.id) data.id = `${collectionId}/${d.id}`;
714
+ if (!data.id) data.id = docId;
469
715
  all.push(data);
470
716
  });
471
717
  }
@@ -495,7 +741,12 @@ var SearchIndexService = class {
495
741
  for (const collectionId of collectionIds) {
496
742
  const path3 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
497
743
  const snap = await this.db.collection(path3).select().get();
498
- snap.forEach((d) => liveIds.add(`${collectionId}/${d.id}`));
744
+ snap.forEach((d) => {
745
+ const docId = `${collectionId}/${d.id}`;
746
+ if (this.isDocIndexable(docId)) {
747
+ liveIds.add(docId);
748
+ }
749
+ });
499
750
  }
500
751
  for (const id of knownIds) {
501
752
  if (!liveIds.has(id)) {
@@ -561,6 +812,9 @@ var SearchIndexService = class {
561
812
  snap.forEach((d) => {
562
813
  const data = d.data() || {};
563
814
  const slug = data.slug || d.id;
815
+ if (!this.isDocIndexable(`${collectionId}/${slug}`)) {
816
+ return;
817
+ }
564
818
  const records = extractDocRecords(schema, {
565
819
  collection: collectionId,
566
820
  slug,
@@ -632,7 +886,12 @@ var SearchIndexService = class {
632
886
  for (const collectionId of collectionIds) {
633
887
  const path3 = `Projects/${this.projectId}/Collections/${collectionId}/Drafts`;
634
888
  const snap = await this.db.collection(path3).select().get();
635
- snap.forEach((d) => liveIds.add(`${collectionId}/${d.id}`));
889
+ snap.forEach((d) => {
890
+ const docId = `${collectionId}/${d.id}`;
891
+ if (this.isDocIndexable(docId)) {
892
+ liveIds.add(docId);
893
+ }
894
+ });
636
895
  }
637
896
  let deletions = 0;
638
897
  for (const docId of Object.keys(docMap)) {
@@ -712,7 +971,7 @@ var SearchIndexService = class {
712
971
  if (!cached || !q.trim()) {
713
972
  return { hits: [], meta: status };
714
973
  }
715
- const raw = cached.index.search(q.trim());
974
+ const raw = executeQuery(cached.index, parseQuery(q));
716
975
  const hits = raw.slice(0, limit).map((r) => ({
717
976
  id: String(r.id || ""),
718
977
  docId: String(r.docId || ""),
@@ -222,6 +222,9 @@ function fieldType(field, options) {
222
222
  if (field.type === "multiselect") {
223
223
  return dom.type.array(dom.type.string);
224
224
  }
225
+ if (field.type === "number") {
226
+ return dom.type.number;
227
+ }
225
228
  if (field.type === "oneof") {
226
229
  const oneOf = dom.create.namedTypeReference("RootCMSOneOf");
227
230
  if (field.types && Array.isArray(field.types)) {
package/dist/cli.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  generateTypes
3
- } from "./chunk-KDXHFMIH.js";
3
+ } from "./chunk-XLX37FRL.js";
4
4
  import {
5
5
  RootCMSClient,
6
6
  getCmsPlugin,
7
7
  unmarshalData
8
- } from "./chunk-SNZ4S4IC.js";
8
+ } from "./chunk-F4SODS5S.js";
9
+ import "./chunk-CRK7N6RR.js";
9
10
  import "./chunk-MLKGABMK.js";
10
11
 
11
12
  // cli/cli.ts