@dyrected/core 2.15.0 → 2.17.0

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/index.js CHANGED
@@ -1845,6 +1845,21 @@ var DyrectedAIError = class _DyrectedAIError extends Error {
1845
1845
  }
1846
1846
  };
1847
1847
 
1848
+ // src/types/navigation.ts
1849
+ function defineWorkspace(options) {
1850
+ return options;
1851
+ }
1852
+ var CURRENT_NAV_PREFERENCES_VERSION = 1;
1853
+ var DEFAULT_USER_NAV_PREFERENCES = {
1854
+ _version: CURRENT_NAV_PREFERENCES_VERSION,
1855
+ pinned: [],
1856
+ hidden: [],
1857
+ groupOrder: [],
1858
+ itemOrder: {},
1859
+ groups: [],
1860
+ items: []
1861
+ };
1862
+
1848
1863
  // src/auth/token.ts
1849
1864
  import { SignJWT, jwtVerify, decodeJwt } from "jose";
1850
1865
 
@@ -3363,6 +3378,134 @@ function assertValidDeclarativeAccessInConfig(config, source = "config") {
3363
3378
  throw new ConfigValidationError(source, diagnostics);
3364
3379
  }
3365
3380
 
3381
+ // src/utils/id.ts
3382
+ import { randomBytes, randomUUID } from "crypto";
3383
+ var NANOID_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-";
3384
+ var BASE62_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
3385
+ var CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
3386
+ var COMMON_SLUG_PREFIXES = {
3387
+ users: "usr",
3388
+ members: "mem",
3389
+ accounts: "acc",
3390
+ applications: "app",
3391
+ customers: "cus",
3392
+ products: "prd",
3393
+ orders: "ord",
3394
+ reports: "rpt",
3395
+ sessions: "ses",
3396
+ transactions: "txn",
3397
+ wallets: "wal",
3398
+ media: "med",
3399
+ files: "fil",
3400
+ posts: "post",
3401
+ pages: "page",
3402
+ tags: "tag",
3403
+ categories: "cat",
3404
+ comments: "com"
3405
+ };
3406
+ function nanoid(size = 21, alphabet = NANOID_ALPHABET) {
3407
+ const mask = (2 << 31 - Math.clz32(alphabet.length - 1 | 1)) - 1;
3408
+ const step = Math.ceil(1.6 * mask * size / alphabet.length);
3409
+ let id = "";
3410
+ while (true) {
3411
+ const bytes = randomBytes(step);
3412
+ for (let i = 0; i < step; i++) {
3413
+ const byte = bytes[i] & mask;
3414
+ if (alphabet[byte]) {
3415
+ id += alphabet[byte];
3416
+ if (id.length === size) return id;
3417
+ }
3418
+ }
3419
+ }
3420
+ }
3421
+ function encodeUlidTime(now2, len = 10) {
3422
+ let str = "";
3423
+ let time = Math.floor(now2);
3424
+ for (let i = len - 1; i >= 0; i--) {
3425
+ const mod = time % 32;
3426
+ str = CROCKFORD_BASE32[mod] + str;
3427
+ time = (time - mod) / 32;
3428
+ }
3429
+ return str;
3430
+ }
3431
+ function encodeUlidRandom(bytes) {
3432
+ let str = "";
3433
+ let bitBuffer = 0;
3434
+ let bitsInBuffer = 0;
3435
+ for (let i = 0; i < bytes.length; i++) {
3436
+ bitBuffer = bitBuffer << 8 | bytes[i];
3437
+ bitsInBuffer += 8;
3438
+ while (bitsInBuffer >= 5) {
3439
+ bitsInBuffer -= 5;
3440
+ const index = bitBuffer >> bitsInBuffer & 31;
3441
+ str += CROCKFORD_BASE32[index];
3442
+ }
3443
+ }
3444
+ return str;
3445
+ }
3446
+ function ulid(seedTime = Date.now()) {
3447
+ const timePart = encodeUlidTime(seedTime);
3448
+ const randPart = encodeUlidRandom(randomBytes(10));
3449
+ return timePart + randPart;
3450
+ }
3451
+ function uuid() {
3452
+ return randomUUID();
3453
+ }
3454
+ function resolvePrefix(slug, explicitPrefix) {
3455
+ if (explicitPrefix && typeof explicitPrefix === "string" && explicitPrefix.trim().length > 0) {
3456
+ const cleaned = explicitPrefix.trim().toLowerCase().replace(/[^a-z0-9_]/g, "");
3457
+ if (cleaned.length > 0) return cleaned;
3458
+ }
3459
+ if (!slug || typeof slug !== "string") return "col";
3460
+ const s = slug.trim().toLowerCase();
3461
+ if (COMMON_SLUG_PREFIXES[s]) return COMMON_SLUG_PREFIXES[s];
3462
+ const pieces = s.split(/[-_]/).filter(Boolean);
3463
+ if (pieces.length > 1 && pieces[0].length >= 2) {
3464
+ const first2 = pieces[0].replace(/[^a-z0-9]/g, "");
3465
+ if (first2.length >= 2) return first2.slice(0, 4);
3466
+ }
3467
+ const cleanWord = s.replace(/[^a-z0-9]/g, "");
3468
+ if (cleanWord.length <= 4) return cleanWord || "col";
3469
+ return cleanWord.slice(0, 3) || "col";
3470
+ }
3471
+ function prefixedId(prefix = "col", size = 16) {
3472
+ const cleanPrefix = (prefix || "col").toLowerCase().replace(/[^a-z0-9_]/g, "") || "col";
3473
+ return `${cleanPrefix}_${nanoid(size, BASE62_ALPHABET)}`;
3474
+ }
3475
+ function generateId(options) {
3476
+ if (typeof options === "string") {
3477
+ return prefixedId(resolvePrefix(options));
3478
+ }
3479
+ const opts = options || {};
3480
+ const type = opts.type || "prefixed-nanoid";
3481
+ switch (type) {
3482
+ case "uuid":
3483
+ return uuid();
3484
+ case "ulid":
3485
+ return ulid();
3486
+ case "nanoid":
3487
+ return nanoid(opts.size || 21);
3488
+ case "prefixed-nanoid":
3489
+ default:
3490
+ return prefixedId(opts.prefix || "col", opts.size || 16);
3491
+ }
3492
+ }
3493
+ function generateDocumentId(collectionOrSlug) {
3494
+ if (!collectionOrSlug) {
3495
+ return prefixedId("col");
3496
+ }
3497
+ if (typeof collectionOrSlug === "string") {
3498
+ const prefix2 = resolvePrefix(collectionOrSlug);
3499
+ return prefixedId(prefix2);
3500
+ }
3501
+ if (typeof collectionOrSlug.idGenerator === "function") {
3502
+ return collectionOrSlug.idGenerator(collectionOrSlug.slug);
3503
+ }
3504
+ const type = collectionOrSlug.idType || "prefixed-nanoid";
3505
+ const prefix = resolvePrefix(collectionOrSlug.slug, collectionOrSlug.idPrefix);
3506
+ return generateId({ prefix, type });
3507
+ }
3508
+
3366
3509
  // src/utils/config.ts
3367
3510
  var AUDIT_COLLECTION_SLUG = "__audit";
3368
3511
  var SYSTEM_FIELDS = [
@@ -3593,7 +3736,33 @@ function normalizeConfig(config) {
3593
3736
  }
3594
3737
  ];
3595
3738
  }
3739
+ if (!existingFieldNames.has("__preferences")) {
3740
+ fields = [
3741
+ ...fields,
3742
+ {
3743
+ name: "__preferences",
3744
+ type: "json",
3745
+ label: "Preferences",
3746
+ promoted: true,
3747
+ admin: {
3748
+ hidden: true,
3749
+ readOnly: true
3750
+ }
3751
+ }
3752
+ ];
3753
+ }
3596
3754
  fields = fields.map((field) => {
3755
+ if (field.name === "__preferences") {
3756
+ return {
3757
+ ...field,
3758
+ promoted: true,
3759
+ admin: {
3760
+ ...field.admin || {},
3761
+ hidden: true,
3762
+ readOnly: true
3763
+ }
3764
+ };
3765
+ }
3597
3766
  if (field.name === "email") {
3598
3767
  return {
3599
3768
  ...field,
@@ -3684,11 +3853,31 @@ function normalizeConfig(config) {
3684
3853
  }
3685
3854
  const updatedFieldNames = new Set(fields.map((f) => f.name));
3686
3855
  const fieldsToInject = SYSTEM_FIELDS.filter((f) => !updatedFieldNames.has(f.name));
3856
+ const indexedFieldNames = /* @__PURE__ */ new Set();
3857
+ if (col.indexes) {
3858
+ for (const idx of col.indexes) {
3859
+ if (Array.isArray(idx.fields)) {
3860
+ for (const f of idx.fields) {
3861
+ indexedFieldNames.add(f);
3862
+ }
3863
+ }
3864
+ }
3865
+ }
3866
+ const allFields = [...fields, ...fieldsToInject].map((field) => {
3867
+ if (field.unique || field.name && indexedFieldNames.has(field.name)) {
3868
+ return { ...field, promoted: true };
3869
+ }
3870
+ return field;
3871
+ });
3687
3872
  const workflow = col.workflow || (col.drafts ? simplePublishingWorkflow() : void 0);
3873
+ const idPrefix = col.idPrefix || resolvePrefix(col.slug);
3874
+ const idType = col.idType || "prefixed-nanoid";
3688
3875
  return {
3689
3876
  ...col,
3877
+ idPrefix,
3878
+ idType,
3690
3879
  workflow,
3691
- fields: [...fields, ...fieldsToInject]
3880
+ fields: allFields
3692
3881
  };
3693
3882
  });
3694
3883
  const hasAuditCollection = normalizedCollections.some((col) => col.slug === AUDIT_COLLECTION_SLUG);
@@ -3748,6 +3937,17 @@ var ConflictError = class _ConflictError extends DyrectedError {
3748
3937
  Object.setPrototypeOf(this, _ConflictError.prototype);
3749
3938
  }
3750
3939
  };
3940
+ var DuplicateKeyError = class _DuplicateKeyError extends ConflictError {
3941
+ field;
3942
+ value;
3943
+ constructor(message, options) {
3944
+ super(message);
3945
+ this.name = "DuplicateKeyError";
3946
+ this.field = options?.field;
3947
+ this.value = options?.value;
3948
+ Object.setPrototypeOf(this, _DuplicateKeyError.prototype);
3949
+ }
3950
+ };
3751
3951
 
3752
3952
  // src/utils/parse-where.ts
3753
3953
  function assertNever(op, context) {
@@ -6369,6 +6569,525 @@ function sanitizeDocForAI(options) {
6369
6569
  return result;
6370
6570
  }
6371
6571
 
6572
+ // src/utils/navigation-reconciler.ts
6573
+ var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
6574
+ function reconcileNavigation(baseTree, prefs, schemas, options) {
6575
+ if (!baseTree) {
6576
+ return {
6577
+ groups: [],
6578
+ ungrouped: [],
6579
+ pinnedItems: []
6580
+ };
6581
+ }
6582
+ const includeHidden = options?.includeHidden ?? false;
6583
+ const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
6584
+ const hiddenSet = new Set(effectivePrefs.hidden || []);
6585
+ const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
6586
+ const knownGlobalSlugs = new Set((schemas?.globals || []).map((g) => g.slug));
6587
+ const groupsList = [];
6588
+ const findGroup = (query) => {
6589
+ const qNorm = normalizeKey(query);
6590
+ return groupsList.find(
6591
+ (g) => g.id === query || g.slug === query || g.name === query || normalizeKey(g.id) === qNorm || normalizeKey(g.slug) === qNorm || normalizeKey(g.name) === qNorm
6592
+ );
6593
+ };
6594
+ for (const g of baseTree.groups || []) {
6595
+ groupsList.push({
6596
+ ...g,
6597
+ items: (g.items || []).map((item) => ({ ...item }))
6598
+ });
6599
+ }
6600
+ for (const ug of effectivePrefs.groups || []) {
6601
+ const existing = findGroup(ug.slug || ug.name);
6602
+ if (!existing) {
6603
+ groupsList.push({
6604
+ id: ug.slug ? `group_${ug.slug}` : `group_${normalizeKey(ug.name)}`,
6605
+ name: ug.name,
6606
+ slug: ug.slug || normalizeKey(ug.name),
6607
+ icon: ug.icon || "Folder",
6608
+ defaultExpanded: ug.defaultExpanded ?? true,
6609
+ order: ug.order ?? 100,
6610
+ items: []
6611
+ });
6612
+ }
6613
+ }
6614
+ for (const itemOpt of effectivePrefs.items || []) {
6615
+ const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group;
6616
+ const groupKey = targetGroup || "Custom";
6617
+ let group = findGroup(groupKey);
6618
+ if (!group) {
6619
+ group = {
6620
+ id: `group_${normalizeKey(groupKey)}`,
6621
+ name: groupKey,
6622
+ slug: normalizeKey(groupKey),
6623
+ icon: "Folder",
6624
+ defaultExpanded: true,
6625
+ order: 99,
6626
+ items: []
6627
+ };
6628
+ groupsList.push(group);
6629
+ }
6630
+ const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
6631
+ for (const otherGroup of groupsList) {
6632
+ if (otherGroup !== group) {
6633
+ otherGroup.items = otherGroup.items.filter((i) => i.slug !== itemOpt.slug && i.id !== itemId);
6634
+ }
6635
+ }
6636
+ const existingIndex = group.items.findIndex((i) => i.id === itemId || i.slug === itemOpt.slug);
6637
+ const existingItem = existingIndex >= 0 ? group.items[existingIndex] : void 0;
6638
+ const col = schemas?.collections?.find((c) => c.slug === itemOpt.collection);
6639
+ const colViews = col?.views || [];
6640
+ const baseViews = existingItem?.views || [];
6641
+ const userViews = itemOpt.views || [];
6642
+ const userViewSlugs = new Set(userViews.map((v) => v.slug));
6643
+ let mergedViews = [
6644
+ ...baseViews.filter((v) => !userViewSlugs.has(v.slug)),
6645
+ ...userViews
6646
+ ];
6647
+ if (mergedViews.length === 0 && itemOpt.collection) {
6648
+ if (colViews.length > 0) {
6649
+ mergedViews = [...colViews];
6650
+ } else {
6651
+ mergedViews = [
6652
+ {
6653
+ slug: "default",
6654
+ label: col?.label || col?.labels?.plural || itemOpt.label || "All Records",
6655
+ layout: "table",
6656
+ collection: itemOpt.collection
6657
+ }
6658
+ ];
6659
+ }
6660
+ }
6661
+ mergedViews = mergedViews.map((v) => ({
6662
+ ...v,
6663
+ collection: v.collection || itemOpt.collection || existingItem?.collection
6664
+ }));
6665
+ const compiledItem = {
6666
+ id: itemId,
6667
+ type: itemOpt.slug ? "workspace" : itemOpt.collection ? "collection" : itemOpt.global ? "global" : "workspace",
6668
+ slug: itemOpt.slug || itemOpt.collection || itemOpt.global || "item",
6669
+ label: itemOpt.label || itemOpt.slug || "Item",
6670
+ icon: itemOpt.icon || existingItem?.icon || col?.admin?.icon || "Briefcase",
6671
+ group: group.name,
6672
+ order: itemOpt.order ?? existingItem?.order ?? 100,
6673
+ views: mergedViews,
6674
+ collection: itemOpt.collection || existingItem?.collection,
6675
+ global: itemOpt.global || existingItem?.global,
6676
+ badge: itemOpt.badge || existingItem?.badge,
6677
+ access: itemOpt.access || existingItem?.access
6678
+ };
6679
+ if (existingIndex >= 0) {
6680
+ group.items[existingIndex] = compiledItem;
6681
+ } else {
6682
+ group.items.push(compiledItem);
6683
+ }
6684
+ }
6685
+ const allAvailableItems = [];
6686
+ const reconciledGroups = [];
6687
+ for (const group of groupsList) {
6688
+ const isGroupHidden = hiddenSet.has(group.id) || !!group.slug && hiddenSet.has(group.slug) || hiddenSet.has(group.name);
6689
+ if (!includeHidden && isGroupHidden) {
6690
+ continue;
6691
+ }
6692
+ const visibleItems = [];
6693
+ for (const item of group.items) {
6694
+ const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
6695
+ if (!includeHidden && isItemHidden) {
6696
+ continue;
6697
+ }
6698
+ const allViews = item.views || [];
6699
+ const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
6700
+ const compoundKey = `${item.slug}_${v.slug}`;
6701
+ return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
6702
+ });
6703
+ let isTombstone = false;
6704
+ if (schemas && schemas.collections && schemas.collections.length > 0) {
6705
+ if (item.collection && !knownCollectionSlugs.has(item.collection)) {
6706
+ isTombstone = true;
6707
+ }
6708
+ }
6709
+ if (schemas && schemas.globals && schemas.globals.length > 0) {
6710
+ if (item.global && !knownGlobalSlugs.has(item.global)) {
6711
+ isTombstone = true;
6712
+ }
6713
+ }
6714
+ const processedItem = {
6715
+ ...item,
6716
+ views: visibleViews,
6717
+ ...isTombstone ? { isTombstone: true } : {}
6718
+ };
6719
+ visibleItems.push(processedItem);
6720
+ allAvailableItems.push(processedItem);
6721
+ }
6722
+ const groupKey = group.slug || group.name;
6723
+ const itemOrderList = effectivePrefs.itemOrder?.[group.id] || effectivePrefs.itemOrder?.[groupKey];
6724
+ if (itemOrderList && itemOrderList.length > 0) {
6725
+ visibleItems.sort((a, b) => {
6726
+ const aIdx = itemOrderList.indexOf(a.id) >= 0 ? itemOrderList.indexOf(a.id) : a.slug ? itemOrderList.indexOf(a.slug) : -1;
6727
+ const bIdx = itemOrderList.indexOf(b.id) >= 0 ? itemOrderList.indexOf(b.id) : b.slug ? itemOrderList.indexOf(b.slug) : -1;
6728
+ if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
6729
+ if (aIdx >= 0) return -1;
6730
+ if (bIdx >= 0) return 1;
6731
+ return (a.order ?? 100) - (b.order ?? 100);
6732
+ });
6733
+ } else {
6734
+ visibleItems.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
6735
+ }
6736
+ reconciledGroups.push({
6737
+ ...group,
6738
+ items: visibleItems
6739
+ });
6740
+ }
6741
+ if (effectivePrefs.groupOrder && effectivePrefs.groupOrder.length > 0) {
6742
+ reconciledGroups.sort((a, b) => {
6743
+ const aIdx = effectivePrefs.groupOrder.indexOf(a.id) >= 0 ? effectivePrefs.groupOrder.indexOf(a.id) : a.slug ? effectivePrefs.groupOrder.indexOf(a.slug) : effectivePrefs.groupOrder.indexOf(a.name);
6744
+ const bIdx = effectivePrefs.groupOrder.indexOf(b.id) >= 0 ? effectivePrefs.groupOrder.indexOf(b.id) : b.slug ? effectivePrefs.groupOrder.indexOf(b.slug) : effectivePrefs.groupOrder.indexOf(b.name);
6745
+ if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
6746
+ if (aIdx >= 0) return -1;
6747
+ if (bIdx >= 0) return 1;
6748
+ return (a.order ?? 100) - (b.order ?? 100);
6749
+ });
6750
+ } else {
6751
+ reconciledGroups.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
6752
+ }
6753
+ const reconciledUngrouped = (baseTree.ungrouped || []).filter((item) => {
6754
+ const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
6755
+ return includeHidden || !isItemHidden;
6756
+ }).map((item) => {
6757
+ const allViews = item.views || [];
6758
+ const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
6759
+ const compoundKey = `${item.slug}_${v.slug}`;
6760
+ return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
6761
+ });
6762
+ const processed = { ...item, views: visibleViews };
6763
+ allAvailableItems.push(processed);
6764
+ return processed;
6765
+ });
6766
+ const pinnedItems = [];
6767
+ for (const pin of effectivePrefs.pinned || []) {
6768
+ const match = allAvailableItems.find((i) => i.slug === pin.slug || i.id === pin.slug);
6769
+ if (match) {
6770
+ pinnedItems.push({
6771
+ ...match,
6772
+ id: `pin_${match.id}`,
6773
+ label: pin.label || match.label,
6774
+ icon: pin.icon || match.icon
6775
+ });
6776
+ } else {
6777
+ pinnedItems.push({
6778
+ id: `pin_${pin.slug}`,
6779
+ type: pin.type,
6780
+ slug: pin.slug,
6781
+ label: pin.label || pin.slug,
6782
+ icon: pin.icon || "Star",
6783
+ order: 0,
6784
+ views: []
6785
+ });
6786
+ }
6787
+ }
6788
+ return {
6789
+ groups: reconciledGroups,
6790
+ ungrouped: reconciledUngrouped,
6791
+ pinnedItems
6792
+ };
6793
+ }
6794
+
6795
+ // src/utils/navigation.ts
6796
+ var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
6797
+ "collections",
6798
+ "globals",
6799
+ "setup",
6800
+ "api",
6801
+ "login",
6802
+ "logout",
6803
+ "settings",
6804
+ "admin",
6805
+ "preferences",
6806
+ "auth"
6807
+ ]);
6808
+ function assertValidNavigationSlugs(navigation) {
6809
+ for (const item of navigation) {
6810
+ if (item.slug) {
6811
+ const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
6812
+ if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
6813
+ throw new Error(
6814
+ `[Dyrected Config] The slug '${item.slug}' in navigation item '${item.label || item.slug}' is a reserved system path and cannot be used as an operational workspace slug.`
6815
+ );
6816
+ }
6817
+ }
6818
+ }
6819
+ }
6820
+ function resolveGroupMeta(groupProp, defaultGroupName = "Collections") {
6821
+ if (!groupProp) {
6822
+ return { name: defaultGroupName };
6823
+ }
6824
+ if (typeof groupProp === "string") {
6825
+ return { name: groupProp };
6826
+ }
6827
+ return { name: groupProp.name, meta: groupProp };
6828
+ }
6829
+ function compileNavigation(config) {
6830
+ const collections = config.collections || [];
6831
+ const globals = config.globals || [];
6832
+ const explicitNav = config.admin?.navigation || [];
6833
+ assertValidNavigationSlugs(explicitNav);
6834
+ const collectionsBySlug = /* @__PURE__ */ new Map();
6835
+ for (const col of collections) {
6836
+ collectionsBySlug.set(col.slug, col);
6837
+ }
6838
+ const globalsBySlug = /* @__PURE__ */ new Map();
6839
+ for (const gl of globals) {
6840
+ globalsBySlug.set(gl.slug, gl);
6841
+ }
6842
+ const mentionedCollections = /* @__PURE__ */ new Set();
6843
+ const mentionedGlobals = /* @__PURE__ */ new Set();
6844
+ const injectedViewsByCollection = /* @__PURE__ */ new Map();
6845
+ for (const item of explicitNav) {
6846
+ if (item.addToCollection) {
6847
+ const existing = injectedViewsByCollection.get(item.addToCollection) || [];
6848
+ if (item.views && item.views.length > 0) {
6849
+ injectedViewsByCollection.set(item.addToCollection, [...existing, ...item.views]);
6850
+ }
6851
+ }
6852
+ if (item.collection) mentionedCollections.add(item.collection);
6853
+ if (item.global) mentionedGlobals.add(item.global);
6854
+ }
6855
+ const groupMetadataMap = /* @__PURE__ */ new Map();
6856
+ const itemsByGroup = /* @__PURE__ */ new Map();
6857
+ const ungroupedItems = [];
6858
+ const registerItem = (groupName, item) => {
6859
+ if (!groupName) {
6860
+ ungroupedItems.push(item);
6861
+ } else {
6862
+ const list = itemsByGroup.get(groupName) || [];
6863
+ list.push(item);
6864
+ itemsByGroup.set(groupName, list);
6865
+ }
6866
+ };
6867
+ for (const item of explicitNav) {
6868
+ if (item.addToCollection && !item.slug && !item.collection && !item.global && !item.dashboard && !item.href) {
6869
+ continue;
6870
+ }
6871
+ const { name: groupName, meta: groupMeta } = resolveGroupMeta(item.group, void 0);
6872
+ if (groupName && groupMeta) {
6873
+ groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
6874
+ }
6875
+ if (item.dashboard) {
6876
+ registerItem(groupName, {
6877
+ id: "dashboard",
6878
+ type: "dashboard",
6879
+ slug: "dashboard",
6880
+ label: item.label || "Dashboard",
6881
+ icon: item.icon || "LayoutDashboard",
6882
+ group: groupName,
6883
+ order: item.order ?? 0,
6884
+ views: [],
6885
+ access: item.access
6886
+ });
6887
+ continue;
6888
+ }
6889
+ if (item.collection) {
6890
+ const col = collectionsBySlug.get(item.collection);
6891
+ const combinedViews = [
6892
+ ...col?.views || [],
6893
+ ...injectedViewsByCollection.get(item.collection) || [],
6894
+ ...item.views || []
6895
+ ];
6896
+ registerItem(groupName, {
6897
+ id: item.slug || `collection_${item.collection}`,
6898
+ type: item.slug ? "workspace" : "collection",
6899
+ slug: item.slug || item.collection,
6900
+ collection: item.collection,
6901
+ label: item.label || col?.labels?.plural || item.collection,
6902
+ icon: item.icon || col?.admin?.icon || "Folder",
6903
+ group: groupName,
6904
+ order: item.order ?? col?.admin?.order ?? 100,
6905
+ views: combinedViews,
6906
+ badge: item.badge,
6907
+ access: item.access || col?.access?.read
6908
+ });
6909
+ continue;
6910
+ }
6911
+ if (item.global) {
6912
+ const gl = globalsBySlug.get(item.global);
6913
+ registerItem(groupName, {
6914
+ id: item.slug || `global_${item.global}`,
6915
+ type: "global",
6916
+ slug: item.slug || item.global,
6917
+ global: item.global,
6918
+ label: item.label || gl?.label || item.global,
6919
+ icon: item.icon || "Settings",
6920
+ group: groupName,
6921
+ order: item.order ?? 100,
6922
+ views: [],
6923
+ badge: item.badge,
6924
+ access: item.access
6925
+ });
6926
+ continue;
6927
+ }
6928
+ if (item.href) {
6929
+ registerItem(groupName, {
6930
+ id: `link_${item.slug || item.href}`,
6931
+ type: "link",
6932
+ slug: item.slug || "link",
6933
+ label: item.label || "Link",
6934
+ href: item.href,
6935
+ icon: item.icon || "ExternalLink",
6936
+ group: groupName,
6937
+ order: item.order ?? 100,
6938
+ views: [],
6939
+ badge: item.badge,
6940
+ access: item.access
6941
+ });
6942
+ continue;
6943
+ }
6944
+ const workspaceSlug = item.slug || "workspace";
6945
+ registerItem(groupName, {
6946
+ id: `workspace_${workspaceSlug}`,
6947
+ type: "workspace",
6948
+ slug: workspaceSlug,
6949
+ label: item.label || workspaceSlug,
6950
+ icon: item.icon || "Briefcase",
6951
+ group: groupName,
6952
+ order: item.order ?? 100,
6953
+ views: item.views || [],
6954
+ badge: item.badge,
6955
+ access: item.access
6956
+ });
6957
+ }
6958
+ for (const col of collections) {
6959
+ if (mentionedCollections.has(col.slug)) continue;
6960
+ if (col.admin?.hidden) continue;
6961
+ const groupProp = col.admin?.group;
6962
+ const { name: groupName, meta: groupMeta } = resolveGroupMeta(groupProp, "Collections");
6963
+ if (groupMeta) {
6964
+ groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
6965
+ }
6966
+ const combinedViews = [
6967
+ ...col.views || [],
6968
+ ...injectedViewsByCollection.get(col.slug) || []
6969
+ ];
6970
+ registerItem(groupName, {
6971
+ id: `collection_${col.slug}`,
6972
+ type: "collection",
6973
+ slug: col.slug,
6974
+ collection: col.slug,
6975
+ label: col.labels?.plural || col.slug,
6976
+ icon: col.admin?.icon || "Folder",
6977
+ group: groupName,
6978
+ order: col?.admin?.order ?? 100,
6979
+ views: combinedViews,
6980
+ access: col.access?.read
6981
+ });
6982
+ }
6983
+ for (const gl of globals) {
6984
+ if (mentionedGlobals.has(gl.slug)) continue;
6985
+ const groupName = "Configuration";
6986
+ registerItem(groupName, {
6987
+ id: `global_${gl.slug}`,
6988
+ type: "global",
6989
+ slug: gl.slug,
6990
+ global: gl.slug,
6991
+ label: gl.label || gl.slug,
6992
+ icon: "Settings",
6993
+ group: groupName,
6994
+ order: 100,
6995
+ views: []
6996
+ });
6997
+ }
6998
+ const sortAndSpliceItems = (items) => {
6999
+ const sorted = [...items].sort((a, b) => a.order - b.order);
7000
+ const explicitMap = /* @__PURE__ */ new Map();
7001
+ for (const opt of explicitNav) {
7002
+ if (opt.slug) explicitMap.set(opt.slug, opt);
7003
+ if (opt.collection) explicitMap.set(opt.collection, opt);
7004
+ if (opt.global) explicitMap.set(opt.global, opt);
7005
+ }
7006
+ for (const opt of explicitNav) {
7007
+ const itemKey = opt.slug || opt.collection || opt.global;
7008
+ if (!itemKey) continue;
7009
+ if (opt.after) {
7010
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7011
+ const targetIdx = sorted.findIndex((i) => i.slug === opt.after);
7012
+ if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
7013
+ const [moved] = sorted.splice(itemIdx, 1);
7014
+ const newTargetIdx = sorted.findIndex((i) => i.slug === opt.after);
7015
+ sorted.splice(newTargetIdx + 1, 0, moved);
7016
+ }
7017
+ } else if (opt.before) {
7018
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7019
+ const targetIdx = sorted.findIndex((i) => i.slug === opt.before);
7020
+ if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
7021
+ const [moved] = sorted.splice(itemIdx, 1);
7022
+ const newTargetIdx = sorted.findIndex((i) => i.slug === opt.before);
7023
+ sorted.splice(newTargetIdx, 0, moved);
7024
+ }
7025
+ } else if (opt.position === "first") {
7026
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7027
+ if (itemIdx > 0) {
7028
+ const [moved] = sorted.splice(itemIdx, 1);
7029
+ sorted.unshift(moved);
7030
+ }
7031
+ } else if (opt.position === "last") {
7032
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7033
+ if (itemIdx !== -1 && itemIdx < sorted.length - 1) {
7034
+ const [moved] = sorted.splice(itemIdx, 1);
7035
+ sorted.push(moved);
7036
+ }
7037
+ }
7038
+ }
7039
+ return sorted;
7040
+ };
7041
+ const compiledGroups = [];
7042
+ for (const [groupName, rawItems] of itemsByGroup.entries()) {
7043
+ const meta = groupMetadataMap.get(groupName);
7044
+ const sortedItems = sortAndSpliceItems(rawItems);
7045
+ compiledGroups.push({
7046
+ id: `group_${groupName.toLowerCase().replace(/\s+/g, "_")}`,
7047
+ name: groupName,
7048
+ slug: meta?.slug || groupName.toLowerCase().replace(/\s+/g, "-"),
7049
+ icon: meta?.icon,
7050
+ defaultExpanded: meta?.defaultExpanded ?? true,
7051
+ order: meta?.order ?? 100,
7052
+ items: sortedItems
7053
+ });
7054
+ }
7055
+ compiledGroups.sort((a, b) => a.order - b.order);
7056
+ return {
7057
+ groups: compiledGroups,
7058
+ ungrouped: sortAndSpliceItems(ungroupedItems)
7059
+ };
7060
+ }
7061
+ function pruneNavigationForUser(tree, user) {
7062
+ const userRoles = /* @__PURE__ */ new Set();
7063
+ if (user?.role) userRoles.add(user.role);
7064
+ if (Array.isArray(user?.roles)) {
7065
+ for (const r of user.roles) userRoles.add(r);
7066
+ }
7067
+ const isAllowed = (item) => {
7068
+ if (!item.access) return true;
7069
+ if (Array.isArray(item.access)) {
7070
+ if (item.access.length === 0) return true;
7071
+ return item.access.some((role) => userRoles.has(role));
7072
+ }
7073
+ return true;
7074
+ };
7075
+ const prunedGroups = [];
7076
+ for (const group of tree.groups) {
7077
+ const visibleItems = group.items.filter(isAllowed);
7078
+ if (visibleItems.length > 0) {
7079
+ prunedGroups.push({
7080
+ ...group,
7081
+ items: visibleItems
7082
+ });
7083
+ }
7084
+ }
7085
+ return {
7086
+ groups: prunedGroups,
7087
+ ungrouped: tree.ungrouped.filter(isAllowed)
7088
+ };
7089
+ }
7090
+
6372
7091
  // src/index.ts
6373
7092
  import { z } from "zod";
6374
7093
  function defineCollection(config) {
@@ -6432,9 +7151,14 @@ export {
6432
7151
  AI_CHUNKS_COLLECTION,
6433
7152
  AI_MESSAGES_COLLECTION,
6434
7153
  AI_THREADS_COLLECTION,
7154
+ BASE62_ALPHABET,
6435
7155
  BUILTIN_JEXL_HELPERS,
7156
+ CROCKFORD_BASE32,
7157
+ CURRENT_NAV_PREFERENCES_VERSION,
6436
7158
  ConfigValidationError,
6437
7159
  ConflictError,
7160
+ DEFAULT_USER_NAV_PREFERENCES,
7161
+ DuplicateKeyError,
6438
7162
  DyrectedAIError,
6439
7163
  DyrectedError,
6440
7164
  FieldConditionBuilder,
@@ -6442,9 +7166,11 @@ export {
6442
7166
  LIFECYCLE_EVENTS_COLLECTION,
6443
7167
  LIFECYCLE_EVENT_NAMES,
6444
7168
  MatchBuilder,
7169
+ NANOID_ALPHABET,
6445
7170
  NON_NEGOTIABLE_CREDENTIAL_FIELDS,
6446
7171
  NotFoundError,
6447
7172
  PII_REGEX_PATTERNS,
7173
+ RESERVED_NAVIGATION_SLUGS,
6448
7174
  ValidationError,
6449
7175
  WORKFLOW_HISTORY_COLLECTION,
6450
7176
  abs,
@@ -6455,6 +7181,7 @@ export {
6455
7181
  assertValidDeclarativeHookExpression,
6456
7182
  assertValidDeclarativeHookResult,
6457
7183
  assertValidDeclarativeHooksInConfig,
7184
+ assertValidNavigationSlugs,
6458
7185
  assertValidPreviewUrlsInConfig,
6459
7186
  availableWorkflowTransitions,
6460
7187
  buildSqlOrderBy,
@@ -6469,6 +7196,7 @@ export {
6469
7196
  collectDeclarativeHookValidationIssues,
6470
7197
  collectPreviewUrlValidationIssues,
6471
7198
  compact,
7199
+ compileNavigation,
6472
7200
  count,
6473
7201
  createLifecycleEvent,
6474
7202
  createWorkflowDocument,
@@ -6509,6 +7237,7 @@ export {
6509
7237
  defineTimeField,
6510
7238
  defineUrlField,
6511
7239
  defineView,
7240
+ defineWorkspace,
6512
7241
  diffDays,
6513
7242
  dispatchLifecycleEvent,
6514
7243
  dispatchPendingLifecycleEvents,
@@ -6537,6 +7266,8 @@ export {
6537
7266
  formatDate,
6538
7267
  formatJexlValue,
6539
7268
  generateDefaultDetailSchema,
7269
+ generateDocumentId,
7270
+ generateId,
6540
7271
  generateOpenApi,
6541
7272
  get,
6542
7273
  getAdminAuthCollection,
@@ -6563,6 +7294,7 @@ export {
6563
7294
  materializeWorkflowDocument,
6564
7295
  max,
6565
7296
  min,
7297
+ nanoid,
6566
7298
  normalizeConfig,
6567
7299
  normalizeDetailItem,
6568
7300
  now,
@@ -6571,13 +7303,17 @@ export {
6571
7303
  parseSort,
6572
7304
  parseSqlWhere,
6573
7305
  pow,
7306
+ prefixedId,
7307
+ pruneNavigationForUser,
6574
7308
  publishedStateName,
6575
7309
  publishingWorkflow,
6576
7310
  readingTime,
7311
+ reconcileNavigation,
6577
7312
  registerJexlHelpers,
6578
7313
  replace,
6579
7314
  resolveAdminAuthCollection,
6580
7315
  resolveAuthorizedSiteId,
7316
+ resolvePrefix,
6581
7317
  round,
6582
7318
  runCollectionHooks,
6583
7319
  sanitizeDocForAI,
@@ -6591,8 +7327,10 @@ export {
6591
7327
  transitionWorkflow,
6592
7328
  trim,
6593
7329
  truncate,
7330
+ ulid,
6594
7331
  unique,
6595
7332
  upper,
7333
+ uuid,
6596
7334
  validateAdminConditionExpression,
6597
7335
  validateDeclarativeAccessExpression,
6598
7336
  validateDeclarativeHookExpression,