@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/server.js CHANGED
@@ -6406,6 +6406,134 @@ function isPlainObject2(value) {
6406
6406
  return typeof value === "object" && value !== null && !Array.isArray(value);
6407
6407
  }
6408
6408
 
6409
+ // src/utils/id.ts
6410
+ import { randomBytes as randomBytes2, randomUUID } from "crypto";
6411
+ var NANOID_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-";
6412
+ var BASE62_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
6413
+ var CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
6414
+ var COMMON_SLUG_PREFIXES = {
6415
+ users: "usr",
6416
+ members: "mem",
6417
+ accounts: "acc",
6418
+ applications: "app",
6419
+ customers: "cus",
6420
+ products: "prd",
6421
+ orders: "ord",
6422
+ reports: "rpt",
6423
+ sessions: "ses",
6424
+ transactions: "txn",
6425
+ wallets: "wal",
6426
+ media: "med",
6427
+ files: "fil",
6428
+ posts: "post",
6429
+ pages: "page",
6430
+ tags: "tag",
6431
+ categories: "cat",
6432
+ comments: "com"
6433
+ };
6434
+ function nanoid(size = 21, alphabet = NANOID_ALPHABET) {
6435
+ const mask = (2 << 31 - Math.clz32(alphabet.length - 1 | 1)) - 1;
6436
+ const step = Math.ceil(1.6 * mask * size / alphabet.length);
6437
+ let id = "";
6438
+ while (true) {
6439
+ const bytes = randomBytes2(step);
6440
+ for (let i = 0; i < step; i++) {
6441
+ const byte = bytes[i] & mask;
6442
+ if (alphabet[byte]) {
6443
+ id += alphabet[byte];
6444
+ if (id.length === size) return id;
6445
+ }
6446
+ }
6447
+ }
6448
+ }
6449
+ function encodeUlidTime(now2, len = 10) {
6450
+ let str = "";
6451
+ let time = Math.floor(now2);
6452
+ for (let i = len - 1; i >= 0; i--) {
6453
+ const mod = time % 32;
6454
+ str = CROCKFORD_BASE32[mod] + str;
6455
+ time = (time - mod) / 32;
6456
+ }
6457
+ return str;
6458
+ }
6459
+ function encodeUlidRandom(bytes) {
6460
+ let str = "";
6461
+ let bitBuffer = 0;
6462
+ let bitsInBuffer = 0;
6463
+ for (let i = 0; i < bytes.length; i++) {
6464
+ bitBuffer = bitBuffer << 8 | bytes[i];
6465
+ bitsInBuffer += 8;
6466
+ while (bitsInBuffer >= 5) {
6467
+ bitsInBuffer -= 5;
6468
+ const index = bitBuffer >> bitsInBuffer & 31;
6469
+ str += CROCKFORD_BASE32[index];
6470
+ }
6471
+ }
6472
+ return str;
6473
+ }
6474
+ function ulid(seedTime = Date.now()) {
6475
+ const timePart = encodeUlidTime(seedTime);
6476
+ const randPart = encodeUlidRandom(randomBytes2(10));
6477
+ return timePart + randPart;
6478
+ }
6479
+ function uuid() {
6480
+ return randomUUID();
6481
+ }
6482
+ function resolvePrefix(slug, explicitPrefix) {
6483
+ if (explicitPrefix && typeof explicitPrefix === "string" && explicitPrefix.trim().length > 0) {
6484
+ const cleaned = explicitPrefix.trim().toLowerCase().replace(/[^a-z0-9_]/g, "");
6485
+ if (cleaned.length > 0) return cleaned;
6486
+ }
6487
+ if (!slug || typeof slug !== "string") return "col";
6488
+ const s = slug.trim().toLowerCase();
6489
+ if (COMMON_SLUG_PREFIXES[s]) return COMMON_SLUG_PREFIXES[s];
6490
+ const pieces = s.split(/[-_]/).filter(Boolean);
6491
+ if (pieces.length > 1 && pieces[0].length >= 2) {
6492
+ const first2 = pieces[0].replace(/[^a-z0-9]/g, "");
6493
+ if (first2.length >= 2) return first2.slice(0, 4);
6494
+ }
6495
+ const cleanWord = s.replace(/[^a-z0-9]/g, "");
6496
+ if (cleanWord.length <= 4) return cleanWord || "col";
6497
+ return cleanWord.slice(0, 3) || "col";
6498
+ }
6499
+ function prefixedId(prefix = "col", size = 16) {
6500
+ const cleanPrefix = (prefix || "col").toLowerCase().replace(/[^a-z0-9_]/g, "") || "col";
6501
+ return `${cleanPrefix}_${nanoid(size, BASE62_ALPHABET)}`;
6502
+ }
6503
+ function generateId(options) {
6504
+ if (typeof options === "string") {
6505
+ return prefixedId(resolvePrefix(options));
6506
+ }
6507
+ const opts = options || {};
6508
+ const type = opts.type || "prefixed-nanoid";
6509
+ switch (type) {
6510
+ case "uuid":
6511
+ return uuid();
6512
+ case "ulid":
6513
+ return ulid();
6514
+ case "nanoid":
6515
+ return nanoid(opts.size || 21);
6516
+ case "prefixed-nanoid":
6517
+ default:
6518
+ return prefixedId(opts.prefix || "col", opts.size || 16);
6519
+ }
6520
+ }
6521
+ function generateDocumentId(collectionOrSlug) {
6522
+ if (!collectionOrSlug) {
6523
+ return prefixedId("col");
6524
+ }
6525
+ if (typeof collectionOrSlug === "string") {
6526
+ const prefix2 = resolvePrefix(collectionOrSlug);
6527
+ return prefixedId(prefix2);
6528
+ }
6529
+ if (typeof collectionOrSlug.idGenerator === "function") {
6530
+ return collectionOrSlug.idGenerator(collectionOrSlug.slug);
6531
+ }
6532
+ const type = collectionOrSlug.idType || "prefixed-nanoid";
6533
+ const prefix = resolvePrefix(collectionOrSlug.slug, collectionOrSlug.idPrefix);
6534
+ return generateId({ prefix, type });
6535
+ }
6536
+
6409
6537
  // src/workflows.ts
6410
6538
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
6411
6539
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
@@ -7374,6 +7502,9 @@ var CollectionController = class {
7374
7502
  surface: "collection.beforeChange",
7375
7503
  path: `collection:${this.collection.slug}.hooks.beforeChange`
7376
7504
  });
7505
+ if (!data.id) {
7506
+ data = { ...data, id: generateDocumentId(this.collection) };
7507
+ }
7377
7508
  const doc = this.collection.workflow ? (await createWorkflowDocument({
7378
7509
  config,
7379
7510
  collection: this.collection,
@@ -7524,6 +7655,9 @@ var CollectionController = class {
7524
7655
  surface: "collection.beforeChange",
7525
7656
  path: `collection:${this.collection.slug}.hooks.beforeChange`
7526
7657
  });
7658
+ if (!data.id) {
7659
+ data = { ...data, id: generateDocumentId(this.collection) };
7660
+ }
7527
7661
  const doc = await db.create({
7528
7662
  collection: this.collection.slug,
7529
7663
  data
@@ -7789,7 +7923,8 @@ var CollectionController = class {
7789
7923
  }
7790
7924
  const body = await c.req.json().catch(() => ({}));
7791
7925
  const requestedIds = Array.isArray(body?.ids) ? body.ids.filter((value) => typeof value === "string") : typeof body?.id === "string" ? [body.id] : [];
7792
- if (!requestedIds.length) {
7926
+ const isHeaderAction = action.type === "header";
7927
+ if (!requestedIds.length && !isHeaderAction) {
7793
7928
  return c.json(
7794
7929
  { error: true, message: "Provide an `id` or an `ids` array of documents to act on." },
7795
7930
  400
@@ -7817,6 +7952,30 @@ var CollectionController = class {
7817
7952
  return c.json({ error: true, message: `Access denied: action "${action.name}"` }, 403);
7818
7953
  }
7819
7954
  }
7955
+ if (isHeaderAction && !requestedIds.length) {
7956
+ if (typeof action.handler === "function") {
7957
+ try {
7958
+ const handlerResult = await action.handler({
7959
+ doc: null,
7960
+ docs: [],
7961
+ user: user ?? null,
7962
+ input,
7963
+ collection: {
7964
+ slug: this.collection.slug,
7965
+ label: this.collection.labels?.singular ?? this.collection.slug
7966
+ }
7967
+ });
7968
+ return c.json(handlerResult ?? { success: true });
7969
+ } catch (error) {
7970
+ const status = typeof error.statusCode === "number" ? error.statusCode : 500;
7971
+ return c.json(
7972
+ { error: true, message: error instanceof Error ? error.message : String(error) },
7973
+ status
7974
+ );
7975
+ }
7976
+ }
7977
+ return c.json({ success: true });
7978
+ }
7820
7979
  const results = [];
7821
7980
  for (const id of requestedIds) {
7822
7981
  const targetDoc = await db.findOne({ collection: this.collection.slug, id });
@@ -9225,7 +9384,7 @@ var MediaFolderController = class {
9225
9384
  };
9226
9385
 
9227
9386
  // src/controllers/auth.controller.ts
9228
- import { randomBytes as randomBytes2 } from "crypto";
9387
+ import { randomBytes as randomBytes3 } from "crypto";
9229
9388
 
9230
9389
  // src/auth/lockout.ts
9231
9390
  var DEFAULT_MAX_LOGIN_ATTEMPTS = 5;
@@ -9536,7 +9695,7 @@ var AuthController = class {
9536
9695
  const data = {
9537
9696
  ...safeExtraFields,
9538
9697
  email,
9539
- password: await hashPassword(randomBytes2(32).toString("hex"))
9698
+ password: await hashPassword(randomBytes3(32).toString("hex"))
9540
9699
  };
9541
9700
  if (this.hasField("status")) {
9542
9701
  data.status = "pending";
@@ -10139,7 +10298,7 @@ var AuthController = class {
10139
10298
  };
10140
10299
 
10141
10300
  // src/controllers/admin-auth.controller.ts
10142
- import { randomBytes as randomBytes3 } from "crypto";
10301
+ import { randomBytes as randomBytes4 } from "crypto";
10143
10302
  import { TextEncoder as TextEncoder2 } from "util";
10144
10303
  import {
10145
10304
  SignJWT as SignJWT2,
@@ -10354,7 +10513,7 @@ var AdminAuthController = class {
10354
10513
  access.roles,
10355
10514
  access.data
10356
10515
  ),
10357
- password: await hashPassword(randomBytes3(32).toString("hex"))
10516
+ password: await hashPassword(randomBytes4(32).toString("hex"))
10358
10517
  }
10359
10518
  });
10360
10519
  } else if (user) {
@@ -10441,7 +10600,7 @@ var AdminAuthController = class {
10441
10600
  }
10442
10601
  async buildOidcStartUrl(provider, returnTo, siteId, origin) {
10443
10602
  const discovery = await this.getOidcDiscovery(provider);
10444
- const nonce = randomBytes3(16).toString("hex");
10603
+ const nonce = randomBytes4(16).toString("hex");
10445
10604
  const state = await this.signState({
10446
10605
  providerId: provider.id,
10447
10606
  returnTo,
@@ -11002,7 +11161,7 @@ function formatAIErrorMessage(error) {
11002
11161
  }
11003
11162
  return msg;
11004
11163
  }
11005
- function generateId() {
11164
+ function generateId2() {
11006
11165
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
11007
11166
  }
11008
11167
  function buildDyrectedSystemPrompt(context) {
@@ -11148,7 +11307,7 @@ var AIAgent = class {
11148
11307
  }
11149
11308
  async createThread(title, id) {
11150
11309
  const thread = {
11151
- id: id || generateId(),
11310
+ id: id || generateId2(),
11152
11311
  projectId: this.projectId,
11153
11312
  userId: this.userId,
11154
11313
  title: title || "New Conversation",
@@ -11215,7 +11374,7 @@ var AIAgent = class {
11215
11374
  }
11216
11375
  async persistUserMessage(threadId, content) {
11217
11376
  const message = {
11218
- id: generateId(),
11377
+ id: generateId2(),
11219
11378
  threadId,
11220
11379
  role: "user",
11221
11380
  content,
@@ -11231,7 +11390,7 @@ var AIAgent = class {
11231
11390
  }
11232
11391
  async persistAssistantMessage(threadId, content, metadata, parts) {
11233
11392
  const message = {
11234
- id: generateId(),
11393
+ id: generateId2(),
11235
11394
  threadId,
11236
11395
  role: "assistant",
11237
11396
  content,
@@ -13615,6 +13774,537 @@ function getSwaggerHtml(specUrl) {
13615
13774
  `;
13616
13775
  }
13617
13776
 
13777
+ // src/types/navigation.ts
13778
+ var CURRENT_NAV_PREFERENCES_VERSION = 1;
13779
+ var DEFAULT_USER_NAV_PREFERENCES = {
13780
+ _version: CURRENT_NAV_PREFERENCES_VERSION,
13781
+ pinned: [],
13782
+ hidden: [],
13783
+ groupOrder: [],
13784
+ itemOrder: {},
13785
+ groups: [],
13786
+ items: []
13787
+ };
13788
+
13789
+ // src/utils/navigation-reconciler.ts
13790
+ var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
13791
+ function reconcileNavigation(baseTree, prefs, schemas, options) {
13792
+ if (!baseTree) {
13793
+ return {
13794
+ groups: [],
13795
+ ungrouped: [],
13796
+ pinnedItems: []
13797
+ };
13798
+ }
13799
+ const includeHidden = options?.includeHidden ?? false;
13800
+ const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
13801
+ const hiddenSet = new Set(effectivePrefs.hidden || []);
13802
+ const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
13803
+ const knownGlobalSlugs = new Set((schemas?.globals || []).map((g) => g.slug));
13804
+ const groupsList = [];
13805
+ const findGroup = (query) => {
13806
+ const qNorm = normalizeKey(query);
13807
+ return groupsList.find(
13808
+ (g) => g.id === query || g.slug === query || g.name === query || normalizeKey(g.id) === qNorm || normalizeKey(g.slug) === qNorm || normalizeKey(g.name) === qNorm
13809
+ );
13810
+ };
13811
+ for (const g of baseTree.groups || []) {
13812
+ groupsList.push({
13813
+ ...g,
13814
+ items: (g.items || []).map((item) => ({ ...item }))
13815
+ });
13816
+ }
13817
+ for (const ug of effectivePrefs.groups || []) {
13818
+ const existing = findGroup(ug.slug || ug.name);
13819
+ if (!existing) {
13820
+ groupsList.push({
13821
+ id: ug.slug ? `group_${ug.slug}` : `group_${normalizeKey(ug.name)}`,
13822
+ name: ug.name,
13823
+ slug: ug.slug || normalizeKey(ug.name),
13824
+ icon: ug.icon || "Folder",
13825
+ defaultExpanded: ug.defaultExpanded ?? true,
13826
+ order: ug.order ?? 100,
13827
+ items: []
13828
+ });
13829
+ }
13830
+ }
13831
+ for (const itemOpt of effectivePrefs.items || []) {
13832
+ const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group;
13833
+ const groupKey = targetGroup || "Custom";
13834
+ let group = findGroup(groupKey);
13835
+ if (!group) {
13836
+ group = {
13837
+ id: `group_${normalizeKey(groupKey)}`,
13838
+ name: groupKey,
13839
+ slug: normalizeKey(groupKey),
13840
+ icon: "Folder",
13841
+ defaultExpanded: true,
13842
+ order: 99,
13843
+ items: []
13844
+ };
13845
+ groupsList.push(group);
13846
+ }
13847
+ const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
13848
+ for (const otherGroup of groupsList) {
13849
+ if (otherGroup !== group) {
13850
+ otherGroup.items = otherGroup.items.filter((i) => i.slug !== itemOpt.slug && i.id !== itemId);
13851
+ }
13852
+ }
13853
+ const existingIndex = group.items.findIndex((i) => i.id === itemId || i.slug === itemOpt.slug);
13854
+ const existingItem = existingIndex >= 0 ? group.items[existingIndex] : void 0;
13855
+ const col = schemas?.collections?.find((c) => c.slug === itemOpt.collection);
13856
+ const colViews = col?.views || [];
13857
+ const baseViews = existingItem?.views || [];
13858
+ const userViews = itemOpt.views || [];
13859
+ const userViewSlugs = new Set(userViews.map((v) => v.slug));
13860
+ let mergedViews = [
13861
+ ...baseViews.filter((v) => !userViewSlugs.has(v.slug)),
13862
+ ...userViews
13863
+ ];
13864
+ if (mergedViews.length === 0 && itemOpt.collection) {
13865
+ if (colViews.length > 0) {
13866
+ mergedViews = [...colViews];
13867
+ } else {
13868
+ mergedViews = [
13869
+ {
13870
+ slug: "default",
13871
+ label: col?.label || col?.labels?.plural || itemOpt.label || "All Records",
13872
+ layout: "table",
13873
+ collection: itemOpt.collection
13874
+ }
13875
+ ];
13876
+ }
13877
+ }
13878
+ mergedViews = mergedViews.map((v) => ({
13879
+ ...v,
13880
+ collection: v.collection || itemOpt.collection || existingItem?.collection
13881
+ }));
13882
+ const compiledItem = {
13883
+ id: itemId,
13884
+ type: itemOpt.slug ? "workspace" : itemOpt.collection ? "collection" : itemOpt.global ? "global" : "workspace",
13885
+ slug: itemOpt.slug || itemOpt.collection || itemOpt.global || "item",
13886
+ label: itemOpt.label || itemOpt.slug || "Item",
13887
+ icon: itemOpt.icon || existingItem?.icon || col?.admin?.icon || "Briefcase",
13888
+ group: group.name,
13889
+ order: itemOpt.order ?? existingItem?.order ?? 100,
13890
+ views: mergedViews,
13891
+ collection: itemOpt.collection || existingItem?.collection,
13892
+ global: itemOpt.global || existingItem?.global,
13893
+ badge: itemOpt.badge || existingItem?.badge,
13894
+ access: itemOpt.access || existingItem?.access
13895
+ };
13896
+ if (existingIndex >= 0) {
13897
+ group.items[existingIndex] = compiledItem;
13898
+ } else {
13899
+ group.items.push(compiledItem);
13900
+ }
13901
+ }
13902
+ const allAvailableItems = [];
13903
+ const reconciledGroups = [];
13904
+ for (const group of groupsList) {
13905
+ const isGroupHidden = hiddenSet.has(group.id) || !!group.slug && hiddenSet.has(group.slug) || hiddenSet.has(group.name);
13906
+ if (!includeHidden && isGroupHidden) {
13907
+ continue;
13908
+ }
13909
+ const visibleItems = [];
13910
+ for (const item of group.items) {
13911
+ const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
13912
+ if (!includeHidden && isItemHidden) {
13913
+ continue;
13914
+ }
13915
+ const allViews = item.views || [];
13916
+ const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
13917
+ const compoundKey = `${item.slug}_${v.slug}`;
13918
+ return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
13919
+ });
13920
+ let isTombstone = false;
13921
+ if (schemas && schemas.collections && schemas.collections.length > 0) {
13922
+ if (item.collection && !knownCollectionSlugs.has(item.collection)) {
13923
+ isTombstone = true;
13924
+ }
13925
+ }
13926
+ if (schemas && schemas.globals && schemas.globals.length > 0) {
13927
+ if (item.global && !knownGlobalSlugs.has(item.global)) {
13928
+ isTombstone = true;
13929
+ }
13930
+ }
13931
+ const processedItem = {
13932
+ ...item,
13933
+ views: visibleViews,
13934
+ ...isTombstone ? { isTombstone: true } : {}
13935
+ };
13936
+ visibleItems.push(processedItem);
13937
+ allAvailableItems.push(processedItem);
13938
+ }
13939
+ const groupKey = group.slug || group.name;
13940
+ const itemOrderList = effectivePrefs.itemOrder?.[group.id] || effectivePrefs.itemOrder?.[groupKey];
13941
+ if (itemOrderList && itemOrderList.length > 0) {
13942
+ visibleItems.sort((a, b) => {
13943
+ const aIdx = itemOrderList.indexOf(a.id) >= 0 ? itemOrderList.indexOf(a.id) : a.slug ? itemOrderList.indexOf(a.slug) : -1;
13944
+ const bIdx = itemOrderList.indexOf(b.id) >= 0 ? itemOrderList.indexOf(b.id) : b.slug ? itemOrderList.indexOf(b.slug) : -1;
13945
+ if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
13946
+ if (aIdx >= 0) return -1;
13947
+ if (bIdx >= 0) return 1;
13948
+ return (a.order ?? 100) - (b.order ?? 100);
13949
+ });
13950
+ } else {
13951
+ visibleItems.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
13952
+ }
13953
+ reconciledGroups.push({
13954
+ ...group,
13955
+ items: visibleItems
13956
+ });
13957
+ }
13958
+ if (effectivePrefs.groupOrder && effectivePrefs.groupOrder.length > 0) {
13959
+ reconciledGroups.sort((a, b) => {
13960
+ 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);
13961
+ 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);
13962
+ if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
13963
+ if (aIdx >= 0) return -1;
13964
+ if (bIdx >= 0) return 1;
13965
+ return (a.order ?? 100) - (b.order ?? 100);
13966
+ });
13967
+ } else {
13968
+ reconciledGroups.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
13969
+ }
13970
+ const reconciledUngrouped = (baseTree.ungrouped || []).filter((item) => {
13971
+ const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
13972
+ return includeHidden || !isItemHidden;
13973
+ }).map((item) => {
13974
+ const allViews = item.views || [];
13975
+ const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
13976
+ const compoundKey = `${item.slug}_${v.slug}`;
13977
+ return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
13978
+ });
13979
+ const processed = { ...item, views: visibleViews };
13980
+ allAvailableItems.push(processed);
13981
+ return processed;
13982
+ });
13983
+ const pinnedItems = [];
13984
+ for (const pin of effectivePrefs.pinned || []) {
13985
+ const match = allAvailableItems.find((i) => i.slug === pin.slug || i.id === pin.slug);
13986
+ if (match) {
13987
+ pinnedItems.push({
13988
+ ...match,
13989
+ id: `pin_${match.id}`,
13990
+ label: pin.label || match.label,
13991
+ icon: pin.icon || match.icon
13992
+ });
13993
+ } else {
13994
+ pinnedItems.push({
13995
+ id: `pin_${pin.slug}`,
13996
+ type: pin.type,
13997
+ slug: pin.slug,
13998
+ label: pin.label || pin.slug,
13999
+ icon: pin.icon || "Star",
14000
+ order: 0,
14001
+ views: []
14002
+ });
14003
+ }
14004
+ }
14005
+ return {
14006
+ groups: reconciledGroups,
14007
+ ungrouped: reconciledUngrouped,
14008
+ pinnedItems
14009
+ };
14010
+ }
14011
+
14012
+ // src/utils/navigation.ts
14013
+ var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
14014
+ "collections",
14015
+ "globals",
14016
+ "setup",
14017
+ "api",
14018
+ "login",
14019
+ "logout",
14020
+ "settings",
14021
+ "admin",
14022
+ "preferences",
14023
+ "auth"
14024
+ ]);
14025
+ function assertValidNavigationSlugs(navigation) {
14026
+ for (const item of navigation) {
14027
+ if (item.slug) {
14028
+ const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
14029
+ if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
14030
+ throw new Error(
14031
+ `[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.`
14032
+ );
14033
+ }
14034
+ }
14035
+ }
14036
+ }
14037
+ function resolveGroupMeta(groupProp, defaultGroupName = "Collections") {
14038
+ if (!groupProp) {
14039
+ return { name: defaultGroupName };
14040
+ }
14041
+ if (typeof groupProp === "string") {
14042
+ return { name: groupProp };
14043
+ }
14044
+ return { name: groupProp.name, meta: groupProp };
14045
+ }
14046
+ function compileNavigation(config) {
14047
+ const collections = config.collections || [];
14048
+ const globals = config.globals || [];
14049
+ const explicitNav = config.admin?.navigation || [];
14050
+ assertValidNavigationSlugs(explicitNav);
14051
+ const collectionsBySlug = /* @__PURE__ */ new Map();
14052
+ for (const col of collections) {
14053
+ collectionsBySlug.set(col.slug, col);
14054
+ }
14055
+ const globalsBySlug = /* @__PURE__ */ new Map();
14056
+ for (const gl of globals) {
14057
+ globalsBySlug.set(gl.slug, gl);
14058
+ }
14059
+ const mentionedCollections = /* @__PURE__ */ new Set();
14060
+ const mentionedGlobals = /* @__PURE__ */ new Set();
14061
+ const injectedViewsByCollection = /* @__PURE__ */ new Map();
14062
+ for (const item of explicitNav) {
14063
+ if (item.addToCollection) {
14064
+ const existing = injectedViewsByCollection.get(item.addToCollection) || [];
14065
+ if (item.views && item.views.length > 0) {
14066
+ injectedViewsByCollection.set(item.addToCollection, [...existing, ...item.views]);
14067
+ }
14068
+ }
14069
+ if (item.collection) mentionedCollections.add(item.collection);
14070
+ if (item.global) mentionedGlobals.add(item.global);
14071
+ }
14072
+ const groupMetadataMap = /* @__PURE__ */ new Map();
14073
+ const itemsByGroup = /* @__PURE__ */ new Map();
14074
+ const ungroupedItems = [];
14075
+ const registerItem = (groupName, item) => {
14076
+ if (!groupName) {
14077
+ ungroupedItems.push(item);
14078
+ } else {
14079
+ const list = itemsByGroup.get(groupName) || [];
14080
+ list.push(item);
14081
+ itemsByGroup.set(groupName, list);
14082
+ }
14083
+ };
14084
+ for (const item of explicitNav) {
14085
+ if (item.addToCollection && !item.slug && !item.collection && !item.global && !item.dashboard && !item.href) {
14086
+ continue;
14087
+ }
14088
+ const { name: groupName, meta: groupMeta } = resolveGroupMeta(item.group, void 0);
14089
+ if (groupName && groupMeta) {
14090
+ groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
14091
+ }
14092
+ if (item.dashboard) {
14093
+ registerItem(groupName, {
14094
+ id: "dashboard",
14095
+ type: "dashboard",
14096
+ slug: "dashboard",
14097
+ label: item.label || "Dashboard",
14098
+ icon: item.icon || "LayoutDashboard",
14099
+ group: groupName,
14100
+ order: item.order ?? 0,
14101
+ views: [],
14102
+ access: item.access
14103
+ });
14104
+ continue;
14105
+ }
14106
+ if (item.collection) {
14107
+ const col = collectionsBySlug.get(item.collection);
14108
+ const combinedViews = [
14109
+ ...col?.views || [],
14110
+ ...injectedViewsByCollection.get(item.collection) || [],
14111
+ ...item.views || []
14112
+ ];
14113
+ registerItem(groupName, {
14114
+ id: item.slug || `collection_${item.collection}`,
14115
+ type: item.slug ? "workspace" : "collection",
14116
+ slug: item.slug || item.collection,
14117
+ collection: item.collection,
14118
+ label: item.label || col?.labels?.plural || item.collection,
14119
+ icon: item.icon || col?.admin?.icon || "Folder",
14120
+ group: groupName,
14121
+ order: item.order ?? col?.admin?.order ?? 100,
14122
+ views: combinedViews,
14123
+ badge: item.badge,
14124
+ access: item.access || col?.access?.read
14125
+ });
14126
+ continue;
14127
+ }
14128
+ if (item.global) {
14129
+ const gl = globalsBySlug.get(item.global);
14130
+ registerItem(groupName, {
14131
+ id: item.slug || `global_${item.global}`,
14132
+ type: "global",
14133
+ slug: item.slug || item.global,
14134
+ global: item.global,
14135
+ label: item.label || gl?.label || item.global,
14136
+ icon: item.icon || "Settings",
14137
+ group: groupName,
14138
+ order: item.order ?? 100,
14139
+ views: [],
14140
+ badge: item.badge,
14141
+ access: item.access
14142
+ });
14143
+ continue;
14144
+ }
14145
+ if (item.href) {
14146
+ registerItem(groupName, {
14147
+ id: `link_${item.slug || item.href}`,
14148
+ type: "link",
14149
+ slug: item.slug || "link",
14150
+ label: item.label || "Link",
14151
+ href: item.href,
14152
+ icon: item.icon || "ExternalLink",
14153
+ group: groupName,
14154
+ order: item.order ?? 100,
14155
+ views: [],
14156
+ badge: item.badge,
14157
+ access: item.access
14158
+ });
14159
+ continue;
14160
+ }
14161
+ const workspaceSlug = item.slug || "workspace";
14162
+ registerItem(groupName, {
14163
+ id: `workspace_${workspaceSlug}`,
14164
+ type: "workspace",
14165
+ slug: workspaceSlug,
14166
+ label: item.label || workspaceSlug,
14167
+ icon: item.icon || "Briefcase",
14168
+ group: groupName,
14169
+ order: item.order ?? 100,
14170
+ views: item.views || [],
14171
+ badge: item.badge,
14172
+ access: item.access
14173
+ });
14174
+ }
14175
+ for (const col of collections) {
14176
+ if (mentionedCollections.has(col.slug)) continue;
14177
+ if (col.admin?.hidden) continue;
14178
+ const groupProp = col.admin?.group;
14179
+ const { name: groupName, meta: groupMeta } = resolveGroupMeta(groupProp, "Collections");
14180
+ if (groupMeta) {
14181
+ groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
14182
+ }
14183
+ const combinedViews = [
14184
+ ...col.views || [],
14185
+ ...injectedViewsByCollection.get(col.slug) || []
14186
+ ];
14187
+ registerItem(groupName, {
14188
+ id: `collection_${col.slug}`,
14189
+ type: "collection",
14190
+ slug: col.slug,
14191
+ collection: col.slug,
14192
+ label: col.labels?.plural || col.slug,
14193
+ icon: col.admin?.icon || "Folder",
14194
+ group: groupName,
14195
+ order: col?.admin?.order ?? 100,
14196
+ views: combinedViews,
14197
+ access: col.access?.read
14198
+ });
14199
+ }
14200
+ for (const gl of globals) {
14201
+ if (mentionedGlobals.has(gl.slug)) continue;
14202
+ const groupName = "Configuration";
14203
+ registerItem(groupName, {
14204
+ id: `global_${gl.slug}`,
14205
+ type: "global",
14206
+ slug: gl.slug,
14207
+ global: gl.slug,
14208
+ label: gl.label || gl.slug,
14209
+ icon: "Settings",
14210
+ group: groupName,
14211
+ order: 100,
14212
+ views: []
14213
+ });
14214
+ }
14215
+ const sortAndSpliceItems = (items) => {
14216
+ const sorted = [...items].sort((a, b) => a.order - b.order);
14217
+ const explicitMap = /* @__PURE__ */ new Map();
14218
+ for (const opt of explicitNav) {
14219
+ if (opt.slug) explicitMap.set(opt.slug, opt);
14220
+ if (opt.collection) explicitMap.set(opt.collection, opt);
14221
+ if (opt.global) explicitMap.set(opt.global, opt);
14222
+ }
14223
+ for (const opt of explicitNav) {
14224
+ const itemKey = opt.slug || opt.collection || opt.global;
14225
+ if (!itemKey) continue;
14226
+ if (opt.after) {
14227
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
14228
+ const targetIdx = sorted.findIndex((i) => i.slug === opt.after);
14229
+ if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
14230
+ const [moved] = sorted.splice(itemIdx, 1);
14231
+ const newTargetIdx = sorted.findIndex((i) => i.slug === opt.after);
14232
+ sorted.splice(newTargetIdx + 1, 0, moved);
14233
+ }
14234
+ } else if (opt.before) {
14235
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
14236
+ const targetIdx = sorted.findIndex((i) => i.slug === opt.before);
14237
+ if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
14238
+ const [moved] = sorted.splice(itemIdx, 1);
14239
+ const newTargetIdx = sorted.findIndex((i) => i.slug === opt.before);
14240
+ sorted.splice(newTargetIdx, 0, moved);
14241
+ }
14242
+ } else if (opt.position === "first") {
14243
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
14244
+ if (itemIdx > 0) {
14245
+ const [moved] = sorted.splice(itemIdx, 1);
14246
+ sorted.unshift(moved);
14247
+ }
14248
+ } else if (opt.position === "last") {
14249
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
14250
+ if (itemIdx !== -1 && itemIdx < sorted.length - 1) {
14251
+ const [moved] = sorted.splice(itemIdx, 1);
14252
+ sorted.push(moved);
14253
+ }
14254
+ }
14255
+ }
14256
+ return sorted;
14257
+ };
14258
+ const compiledGroups = [];
14259
+ for (const [groupName, rawItems] of itemsByGroup.entries()) {
14260
+ const meta = groupMetadataMap.get(groupName);
14261
+ const sortedItems = sortAndSpliceItems(rawItems);
14262
+ compiledGroups.push({
14263
+ id: `group_${groupName.toLowerCase().replace(/\s+/g, "_")}`,
14264
+ name: groupName,
14265
+ slug: meta?.slug || groupName.toLowerCase().replace(/\s+/g, "-"),
14266
+ icon: meta?.icon,
14267
+ defaultExpanded: meta?.defaultExpanded ?? true,
14268
+ order: meta?.order ?? 100,
14269
+ items: sortedItems
14270
+ });
14271
+ }
14272
+ compiledGroups.sort((a, b) => a.order - b.order);
14273
+ return {
14274
+ groups: compiledGroups,
14275
+ ungrouped: sortAndSpliceItems(ungroupedItems)
14276
+ };
14277
+ }
14278
+ function pruneNavigationForUser(tree, user) {
14279
+ const userRoles = /* @__PURE__ */ new Set();
14280
+ if (user?.role) userRoles.add(user.role);
14281
+ if (Array.isArray(user?.roles)) {
14282
+ for (const r of user.roles) userRoles.add(r);
14283
+ }
14284
+ const isAllowed = (item) => {
14285
+ if (!item.access) return true;
14286
+ if (Array.isArray(item.access)) {
14287
+ if (item.access.length === 0) return true;
14288
+ return item.access.some((role) => userRoles.has(role));
14289
+ }
14290
+ return true;
14291
+ };
14292
+ const prunedGroups = [];
14293
+ for (const group of tree.groups) {
14294
+ const visibleItems = group.items.filter(isAllowed);
14295
+ if (visibleItems.length > 0) {
14296
+ prunedGroups.push({
14297
+ ...group,
14298
+ items: visibleItems
14299
+ });
14300
+ }
14301
+ }
14302
+ return {
14303
+ groups: prunedGroups,
14304
+ ungrouped: tree.ungrouped.filter(isAllowed)
14305
+ };
14306
+ }
14307
+
13618
14308
  // src/router.ts
13619
14309
  init_access_control();
13620
14310
  import { HTTPException as HTTPException5 } from "hono/http-exception";
@@ -14114,6 +14804,7 @@ function registerRoutes(app, config) {
14114
14804
  const user = c.get("user");
14115
14805
  const key = c.req.param("key");
14116
14806
  const scope = c.req.query("scope");
14807
+ const roleParam = c.req.query("role");
14117
14808
  if (!db) return c.json({ message: "Database not configured" }, 500);
14118
14809
  if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
14119
14810
  const getGlobalPreference = async () => {
@@ -14123,27 +14814,61 @@ function registerRoutes(app, config) {
14123
14814
  });
14124
14815
  return globalDoc ? globalDoc.value : null;
14125
14816
  };
14126
- if (scope === "global" || !user?.collection || !user.sub) {
14817
+ const getRolePreference = async (r) => {
14818
+ const roleDoc = await db.findOne({
14819
+ collection: "__role_preferences",
14820
+ id: `${r}_${key}`
14821
+ });
14822
+ return roleDoc ? roleDoc.value : null;
14823
+ };
14824
+ const getUserPreferences = async () => {
14825
+ if (!user?.collection || !user.sub) return null;
14826
+ const doc = await db.findOne({ collection: user.collection, id: user.sub });
14827
+ if (!doc) return null;
14828
+ return typeof doc.__preferences === "object" && doc.__preferences !== null ? doc.__preferences : {};
14829
+ };
14830
+ if (scope === "global") {
14127
14831
  const globalValue2 = await getGlobalPreference();
14128
14832
  return c.json({ key, value: globalValue2 });
14129
14833
  }
14130
- const doc = await db.findOne({ collection: user.collection, id: user.sub });
14131
- if (!doc) {
14132
- const globalValue2 = await getGlobalPreference();
14133
- return c.json({ key, value: globalValue2 });
14834
+ if (scope === "role") {
14835
+ if (roleParam) {
14836
+ const val = await getRolePreference(roleParam);
14837
+ return c.json({ key, value: val, role: roleParam });
14838
+ }
14839
+ const userRoles2 = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
14840
+ for (const r of userRoles2) {
14841
+ const val = await getRolePreference(r);
14842
+ if (val !== null && val !== void 0) {
14843
+ return c.json({ key, value: val, role: r });
14844
+ }
14845
+ }
14846
+ return c.json({ key, value: null });
14134
14847
  }
14135
- const preferences = typeof doc.__preferences === "object" && doc.__preferences !== null ? doc.__preferences : {};
14136
- if (key in preferences) {
14137
- return c.json({ key, value: preferences[key] ?? null });
14848
+ if (scope === "personal") {
14849
+ const prefs2 = await getUserPreferences();
14850
+ return c.json({ key, value: prefs2 && key in prefs2 ? prefs2[key] ?? null : null });
14851
+ }
14852
+ const prefs = await getUserPreferences();
14853
+ if (prefs && key in prefs && prefs[key] !== null && prefs[key] !== void 0) {
14854
+ return c.json({ key, value: prefs[key] });
14855
+ }
14856
+ const userRoles = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
14857
+ for (const r of userRoles) {
14858
+ const val = await getRolePreference(r);
14859
+ if (val !== null && val !== void 0) {
14860
+ return c.json({ key, value: val });
14861
+ }
14138
14862
  }
14139
14863
  const globalValue = await getGlobalPreference();
14140
- return c.json({ key, value: globalValue });
14864
+ return c.json({ key, value: globalValue ?? null });
14141
14865
  });
14142
14866
  app.put("/api/preferences/:key", requireAuth(config), async (c) => {
14143
14867
  const db = config.db;
14144
14868
  const user = c.get("user");
14145
14869
  const key = c.req.param("key");
14146
- const scope = c.req.query("scope");
14870
+ const scope = c.req.query("scope") || "personal";
14871
+ const roleParam = c.req.query("role");
14147
14872
  if (!db) return c.json({ message: "Database not configured" }, 500);
14148
14873
  if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
14149
14874
  if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
@@ -14176,7 +14901,47 @@ function registerRoutes(app, config) {
14176
14901
  data: { id: key, value: body.value }
14177
14902
  });
14178
14903
  }
14179
- return c.json({ key, value: body.value });
14904
+ return c.json({ key, value: body.value, scope: "global" });
14905
+ }
14906
+ if (scope === "role") {
14907
+ const userCol = config.collections?.find((col) => col.slug === user?.collection);
14908
+ const isAdminUser = isUserAdmin(user, userCol);
14909
+ if (!isAdminUser) {
14910
+ return c.json(
14911
+ {
14912
+ error: true,
14913
+ message: "Only administrators can save role preferences."
14914
+ },
14915
+ 403
14916
+ );
14917
+ }
14918
+ if (!roleParam) {
14919
+ return c.json(
14920
+ {
14921
+ error: true,
14922
+ message: "Role query parameter is required for role-scoped preferences."
14923
+ },
14924
+ 400
14925
+ );
14926
+ }
14927
+ const roleDocId = `${roleParam}_${key}`;
14928
+ const existing = await db.findOne({
14929
+ collection: "__role_preferences",
14930
+ id: roleDocId
14931
+ });
14932
+ if (existing) {
14933
+ await db.update({
14934
+ collection: "__role_preferences",
14935
+ id: roleDocId,
14936
+ data: { value: body.value, role: roleParam, key }
14937
+ });
14938
+ } else {
14939
+ await db.create({
14940
+ collection: "__role_preferences",
14941
+ data: { id: roleDocId, role: roleParam, key, value: body.value }
14942
+ });
14943
+ }
14944
+ return c.json({ key, value: body.value, role: roleParam, scope: "role" });
14180
14945
  }
14181
14946
  const doc = await db.findOne({ collection: user.collection, id: user.sub });
14182
14947
  if (!doc) return c.json({ error: true, message: "User not found." }, 404);
@@ -14187,13 +14952,14 @@ function registerRoutes(app, config) {
14187
14952
  id: user.sub,
14188
14953
  data: { __preferences: nextPreferences }
14189
14954
  });
14190
- return c.json({ key, value: body.value });
14955
+ return c.json({ key, value: body.value, scope: "personal" });
14191
14956
  });
14192
14957
  app.delete("/api/preferences/:key", requireAuth(config), async (c) => {
14193
14958
  const db = config.db;
14194
14959
  const user = c.get("user");
14195
14960
  const key = c.req.param("key");
14196
- const scope = c.req.query("scope");
14961
+ const scope = c.req.query("scope") || "personal";
14962
+ const roleParam = c.req.query("role");
14197
14963
  if (!db) return c.json({ message: "Database not configured" }, 500);
14198
14964
  if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
14199
14965
  if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
@@ -14212,6 +14978,30 @@ function registerRoutes(app, config) {
14212
14978
  await db.delete({ collection: "__global_preferences", id: key });
14213
14979
  return c.json({ success: true });
14214
14980
  }
14981
+ if (scope === "role") {
14982
+ const userCol = config.collections?.find((col) => col.slug === user?.collection);
14983
+ const isAdminUser = isUserAdmin(user, userCol);
14984
+ if (!isAdminUser) {
14985
+ return c.json(
14986
+ {
14987
+ error: true,
14988
+ message: "Only administrators can delete role preferences."
14989
+ },
14990
+ 403
14991
+ );
14992
+ }
14993
+ if (!roleParam) {
14994
+ return c.json(
14995
+ {
14996
+ error: true,
14997
+ message: "Role query parameter is required for role-scoped preferences."
14998
+ },
14999
+ 400
15000
+ );
15001
+ }
15002
+ await db.delete({ collection: "__role_preferences", id: `${roleParam}_${key}` });
15003
+ return c.json({ success: true });
15004
+ }
14215
15005
  const doc = await db.findOne({ collection: user.collection, id: user.sub });
14216
15006
  if (!doc) return c.json({ error: true, message: "User not found." }, 404);
14217
15007
  const preferences = typeof doc.__preferences === "object" && doc.__preferences !== null ? { ...doc.__preferences } : {};
@@ -14223,6 +15013,198 @@ function registerRoutes(app, config) {
14223
15013
  });
14224
15014
  return c.json({ success: true });
14225
15015
  });
15016
+ app.get("/api/admin/navigation", optionalAuth(config), async (c) => {
15017
+ const user = c.get("user");
15018
+ const compiled = compileNavigation(config);
15019
+ const pruned = pruneNavigationForUser(compiled, user);
15020
+ return c.json(pruned);
15021
+ });
15022
+ app.on(["GET", "POST"], "/api/admin/navigation/badges", optionalAuth(config), async (c) => {
15023
+ const db = config.db;
15024
+ if (!db) return c.json({ badges: {} });
15025
+ const user = c.get("user");
15026
+ const compiled = compileNavigation(config);
15027
+ let userNavPrefs = null;
15028
+ const prefsQuery = c.req.query("prefs");
15029
+ if (prefsQuery) {
15030
+ try {
15031
+ userNavPrefs = JSON.parse(decodeURIComponent(prefsQuery));
15032
+ } catch {
15033
+ }
15034
+ }
15035
+ if (!userNavPrefs && c.req.method === "POST") {
15036
+ try {
15037
+ const body = await c.req.json();
15038
+ if (body?.preferences) userNavPrefs = body.preferences;
15039
+ } catch {
15040
+ }
15041
+ }
15042
+ if (!userNavPrefs && user?.collection && user?.sub) {
15043
+ try {
15044
+ const userDoc = await db.findOne({ collection: user.collection, id: user.sub });
15045
+ if (userDoc?.__preferences && typeof userDoc.__preferences === "object") {
15046
+ userNavPrefs = userDoc.__preferences["admin:navigation"];
15047
+ }
15048
+ } catch {
15049
+ }
15050
+ }
15051
+ if (!userNavPrefs) {
15052
+ try {
15053
+ const globalDoc = await db.findOne({
15054
+ collection: "__global_preferences",
15055
+ id: "admin:navigation"
15056
+ });
15057
+ if (globalDoc?.value) {
15058
+ userNavPrefs = globalDoc.value;
15059
+ }
15060
+ } catch {
15061
+ }
15062
+ }
15063
+ let allItems;
15064
+ if (userNavPrefs) {
15065
+ const reconciled = reconcileNavigation(compiled, userNavPrefs, {
15066
+ collections: config.collections,
15067
+ globals: config.globals
15068
+ });
15069
+ allItems = [
15070
+ ...reconciled.ungrouped || [],
15071
+ ...reconciled.pinnedItems || [],
15072
+ ...(reconciled.groups || []).flatMap((g) => g.items || [])
15073
+ ];
15074
+ } else {
15075
+ const pruned = pruneNavigationForUser(compiled, user);
15076
+ allItems = [
15077
+ ...pruned.ungrouped,
15078
+ ...pruned.groups.flatMap((g) => g.items)
15079
+ ];
15080
+ }
15081
+ const badges = {};
15082
+ const requestsByCollection = /* @__PURE__ */ new Map();
15083
+ let reqCounter = 0;
15084
+ for (const item of allItems) {
15085
+ if (item.badge) {
15086
+ const itemBadge = item.badge;
15087
+ const badgeKey = item.slug || item.collection || item.global || item.id;
15088
+ if (typeof itemBadge === "string") {
15089
+ badges[badgeKey] = { count: itemBadge, variant: "default" };
15090
+ } else {
15091
+ const badgeConfig = itemBadge;
15092
+ const variant = badgeConfig.variant ?? "default";
15093
+ if (badgeConfig.text) {
15094
+ badges[badgeKey] = { count: badgeConfig.text, variant };
15095
+ } else {
15096
+ let targetCol;
15097
+ let targetWhere;
15098
+ if (badgeConfig.aggregate) {
15099
+ targetCol = badgeConfig.aggregate.collection;
15100
+ targetWhere = badgeConfig.aggregate.where;
15101
+ } else if (badgeConfig.count) {
15102
+ targetCol = item.collection || item.views?.[0]?.collection;
15103
+ const primaryView = item.views?.[0];
15104
+ if (primaryView?.filter) {
15105
+ targetWhere = primaryView.filter;
15106
+ }
15107
+ }
15108
+ if (targetCol) {
15109
+ const reqId = `agg_${reqCounter++}`;
15110
+ const list = requestsByCollection.get(targetCol) || [];
15111
+ list.push({
15112
+ id: reqId,
15113
+ where: targetWhere,
15114
+ onResult: (count2) => {
15115
+ badges[badgeKey] = { count: count2, variant };
15116
+ }
15117
+ });
15118
+ requestsByCollection.set(targetCol, list);
15119
+ }
15120
+ }
15121
+ }
15122
+ }
15123
+ if (item.views && item.views.length > 0) {
15124
+ for (const view of item.views) {
15125
+ if (!view.badge) continue;
15126
+ const viewBadge = view.badge;
15127
+ const viewKey = `${item.slug}:${view.slug}`;
15128
+ const altKey = `${item.slug}_${view.slug}`;
15129
+ const plainKey = view.slug;
15130
+ const setBadgeResult = (res) => {
15131
+ badges[viewKey] = res;
15132
+ badges[altKey] = res;
15133
+ if (!badges[plainKey]) {
15134
+ badges[plainKey] = res;
15135
+ }
15136
+ if (view.collection) {
15137
+ badges[`${view.collection}:${view.slug}`] = res;
15138
+ badges[`${view.collection}_${view.slug}`] = res;
15139
+ }
15140
+ };
15141
+ if (typeof viewBadge === "string") {
15142
+ setBadgeResult({ count: viewBadge, variant: "default" });
15143
+ continue;
15144
+ }
15145
+ const badgeConfig = viewBadge;
15146
+ const variant = badgeConfig.variant ?? "default";
15147
+ if (badgeConfig.text) {
15148
+ setBadgeResult({ count: badgeConfig.text, variant });
15149
+ continue;
15150
+ }
15151
+ const targetCol = badgeConfig.aggregate?.collection || view.collection || item.collection || (item.type === "collection" ? item.slug : void 0);
15152
+ const targetWhere = badgeConfig.aggregate?.where || (typeof view.filter === "object" ? view.filter : void 0);
15153
+ if (!targetCol) continue;
15154
+ const reqId = `agg_${reqCounter++}`;
15155
+ const list = requestsByCollection.get(targetCol) || [];
15156
+ list.push({
15157
+ id: reqId,
15158
+ where: targetWhere,
15159
+ onResult: (count2) => {
15160
+ setBadgeResult({ count: count2, variant });
15161
+ }
15162
+ });
15163
+ requestsByCollection.set(targetCol, list);
15164
+ }
15165
+ }
15166
+ }
15167
+ const collectionTasks = Array.from(requestsByCollection.entries()).map(async ([col, reqs]) => {
15168
+ if (typeof db.aggregate === "function") {
15169
+ try {
15170
+ const aggregates = {};
15171
+ for (const req of reqs) {
15172
+ aggregates[req.id] = { count: "*", where: req.where };
15173
+ }
15174
+ const agg = await db.aggregate({
15175
+ collection: col,
15176
+ aggregates
15177
+ });
15178
+ for (const req of reqs) {
15179
+ const val = agg?.[req.id];
15180
+ req.onResult(typeof val === "number" ? val : 0);
15181
+ }
15182
+ } catch {
15183
+ for (const req of reqs) {
15184
+ req.onResult(0);
15185
+ }
15186
+ }
15187
+ } else {
15188
+ await Promise.allSettled(
15189
+ reqs.map(async (req) => {
15190
+ try {
15191
+ const res = await db.find({
15192
+ collection: col,
15193
+ where: req.where,
15194
+ limit: 1
15195
+ });
15196
+ const total = res?.totalDocs ?? res?.total ?? 0;
15197
+ req.onResult(total);
15198
+ } catch {
15199
+ req.onResult(0);
15200
+ }
15201
+ })
15202
+ );
15203
+ }
15204
+ });
15205
+ await Promise.allSettled(collectionTasks);
15206
+ return c.json({ badges });
15207
+ });
14226
15208
  app.get("/api/media/folders", async (c) => {
14227
15209
  const folderController = new MediaFolderController("media");
14228
15210
  return folderController.list(c);
@@ -14702,7 +15684,33 @@ function normalizeConfig(config) {
14702
15684
  }
14703
15685
  ];
14704
15686
  }
15687
+ if (!existingFieldNames.has("__preferences")) {
15688
+ fields = [
15689
+ ...fields,
15690
+ {
15691
+ name: "__preferences",
15692
+ type: "json",
15693
+ label: "Preferences",
15694
+ promoted: true,
15695
+ admin: {
15696
+ hidden: true,
15697
+ readOnly: true
15698
+ }
15699
+ }
15700
+ ];
15701
+ }
14705
15702
  fields = fields.map((field) => {
15703
+ if (field.name === "__preferences") {
15704
+ return {
15705
+ ...field,
15706
+ promoted: true,
15707
+ admin: {
15708
+ ...field.admin || {},
15709
+ hidden: true,
15710
+ readOnly: true
15711
+ }
15712
+ };
15713
+ }
14706
15714
  if (field.name === "email") {
14707
15715
  return {
14708
15716
  ...field,
@@ -14793,11 +15801,31 @@ function normalizeConfig(config) {
14793
15801
  }
14794
15802
  const updatedFieldNames = new Set(fields.map((f) => f.name));
14795
15803
  const fieldsToInject = SYSTEM_FIELDS.filter((f) => !updatedFieldNames.has(f.name));
15804
+ const indexedFieldNames = /* @__PURE__ */ new Set();
15805
+ if (col.indexes) {
15806
+ for (const idx of col.indexes) {
15807
+ if (Array.isArray(idx.fields)) {
15808
+ for (const f of idx.fields) {
15809
+ indexedFieldNames.add(f);
15810
+ }
15811
+ }
15812
+ }
15813
+ }
15814
+ const allFields = [...fields, ...fieldsToInject].map((field) => {
15815
+ if (field.unique || field.name && indexedFieldNames.has(field.name)) {
15816
+ return { ...field, promoted: true };
15817
+ }
15818
+ return field;
15819
+ });
14796
15820
  const workflow = col.workflow || (col.drafts ? simplePublishingWorkflow() : void 0);
15821
+ const idPrefix = col.idPrefix || resolvePrefix(col.slug);
15822
+ const idType = col.idType || "prefixed-nanoid";
14797
15823
  return {
14798
15824
  ...col,
15825
+ idPrefix,
15826
+ idType,
14799
15827
  workflow,
14800
- fields: [...fields, ...fieldsToInject]
15828
+ fields: allFields
14801
15829
  };
14802
15830
  });
14803
15831
  const hasAuditCollection = normalizedCollections.some((col) => col.slug === AUDIT_COLLECTION_SLUG);