@dyrected/core 2.18.0 → 2.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{app-Dd9cBbCs.d.cts → app-x7J5BIqK.d.cts} +1 -1
- package/dist/{app-Dd9cBbCs.d.ts → app-x7J5BIqK.d.ts} +1 -1
- package/dist/index.cjs +24 -0
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +23 -0
- package/dist/server.cjs +1873 -1849
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1873 -1849
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -6491,1229 +6491,1782 @@ async function buildCollectionSearchWhere(args) {
|
|
|
6491
6491
|
return clauses.length === 1 ? clauses[0] : { OR: clauses };
|
|
6492
6492
|
}
|
|
6493
6493
|
|
|
6494
|
-
// src/
|
|
6495
|
-
|
|
6494
|
+
// src/types/navigation.ts
|
|
6495
|
+
var CURRENT_NAV_PREFERENCES_VERSION = 1;
|
|
6496
|
+
var DEFAULT_USER_NAV_PREFERENCES = {
|
|
6497
|
+
_version: CURRENT_NAV_PREFERENCES_VERSION,
|
|
6498
|
+
pinned: [],
|
|
6499
|
+
hidden: [],
|
|
6500
|
+
groupOrder: [],
|
|
6501
|
+
itemOrder: {},
|
|
6502
|
+
groups: [],
|
|
6503
|
+
items: []
|
|
6504
|
+
};
|
|
6496
6505
|
|
|
6497
|
-
// src/utils/
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
const out = {};
|
|
6507
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
6508
|
-
out[key] = resolveValue(entry, ctx);
|
|
6509
|
-
}
|
|
6510
|
-
return out;
|
|
6506
|
+
// src/utils/navigation-reconciler.ts
|
|
6507
|
+
var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
6508
|
+
function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
6509
|
+
if (!baseTree) {
|
|
6510
|
+
return {
|
|
6511
|
+
groups: [],
|
|
6512
|
+
ungrouped: [],
|
|
6513
|
+
pinnedItems: []
|
|
6514
|
+
};
|
|
6511
6515
|
}
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
const
|
|
6517
|
-
|
|
6518
|
-
const
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
}
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6516
|
+
const includeHidden = options?.includeHidden ?? false;
|
|
6517
|
+
const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
|
|
6518
|
+
const hiddenSet = new Set(effectivePrefs.hidden || []);
|
|
6519
|
+
const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
|
|
6520
|
+
const knownGlobalSlugs = new Set((schemas?.globals || []).map((g) => g.slug));
|
|
6521
|
+
const groupsList = [];
|
|
6522
|
+
const findGroup = (query) => {
|
|
6523
|
+
const qNorm = normalizeKey(query);
|
|
6524
|
+
return groupsList.find(
|
|
6525
|
+
(g) => g.id === query || g.slug === query || g.name === query || normalizeKey(g.id) === qNorm || normalizeKey(g.slug) === qNorm || normalizeKey(g.name) === qNorm
|
|
6526
|
+
);
|
|
6527
|
+
};
|
|
6528
|
+
for (const g of baseTree.groups || []) {
|
|
6529
|
+
groupsList.push({
|
|
6530
|
+
...g,
|
|
6531
|
+
items: (g.items || []).map((item) => ({ ...item }))
|
|
6532
|
+
});
|
|
6529
6533
|
}
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
}
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
users: "usr",
|
|
6543
|
-
members: "mem",
|
|
6544
|
-
accounts: "acc",
|
|
6545
|
-
applications: "app",
|
|
6546
|
-
customers: "cus",
|
|
6547
|
-
products: "prd",
|
|
6548
|
-
orders: "ord",
|
|
6549
|
-
reports: "rpt",
|
|
6550
|
-
sessions: "ses",
|
|
6551
|
-
transactions: "txn",
|
|
6552
|
-
wallets: "wal",
|
|
6553
|
-
media: "med",
|
|
6554
|
-
files: "fil",
|
|
6555
|
-
posts: "post",
|
|
6556
|
-
pages: "page",
|
|
6557
|
-
tags: "tag",
|
|
6558
|
-
categories: "cat",
|
|
6559
|
-
comments: "com"
|
|
6560
|
-
};
|
|
6561
|
-
function nanoid(size = 21, alphabet = NANOID_ALPHABET) {
|
|
6562
|
-
const mask = (2 << 31 - Math.clz32(alphabet.length - 1 | 1)) - 1;
|
|
6563
|
-
const step = Math.ceil(1.6 * mask * size / alphabet.length);
|
|
6564
|
-
let id = "";
|
|
6565
|
-
while (true) {
|
|
6566
|
-
const bytes = (0, import_node_crypto2.randomBytes)(step);
|
|
6567
|
-
for (let i = 0; i < step; i++) {
|
|
6568
|
-
const byte = bytes[i] & mask;
|
|
6569
|
-
if (alphabet[byte]) {
|
|
6570
|
-
id += alphabet[byte];
|
|
6571
|
-
if (id.length === size) return id;
|
|
6572
|
-
}
|
|
6534
|
+
for (const ug of effectivePrefs.groups || []) {
|
|
6535
|
+
const existing = findGroup(ug.slug || ug.name);
|
|
6536
|
+
if (!existing) {
|
|
6537
|
+
groupsList.push({
|
|
6538
|
+
id: ug.slug ? `group_${ug.slug}` : `group_${normalizeKey(ug.name)}`,
|
|
6539
|
+
name: ug.name,
|
|
6540
|
+
slug: ug.slug || normalizeKey(ug.name),
|
|
6541
|
+
icon: ug.icon || "Folder",
|
|
6542
|
+
defaultExpanded: ug.defaultExpanded ?? true,
|
|
6543
|
+
order: ug.order ?? 100,
|
|
6544
|
+
items: []
|
|
6545
|
+
});
|
|
6573
6546
|
}
|
|
6574
6547
|
}
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6548
|
+
for (const itemOpt of effectivePrefs.items || []) {
|
|
6549
|
+
const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group;
|
|
6550
|
+
const groupKey = targetGroup || "Custom";
|
|
6551
|
+
let group = findGroup(groupKey);
|
|
6552
|
+
if (!group) {
|
|
6553
|
+
group = {
|
|
6554
|
+
id: `group_${normalizeKey(groupKey)}`,
|
|
6555
|
+
name: groupKey,
|
|
6556
|
+
slug: normalizeKey(groupKey),
|
|
6557
|
+
icon: "Folder",
|
|
6558
|
+
defaultExpanded: true,
|
|
6559
|
+
order: 99,
|
|
6560
|
+
items: []
|
|
6561
|
+
};
|
|
6562
|
+
groupsList.push(group);
|
|
6563
|
+
}
|
|
6564
|
+
const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
|
|
6565
|
+
for (const otherGroup of groupsList) {
|
|
6566
|
+
if (otherGroup !== group) {
|
|
6567
|
+
otherGroup.items = otherGroup.items.filter((i) => i.slug !== itemOpt.slug && i.id !== itemId);
|
|
6568
|
+
}
|
|
6569
|
+
}
|
|
6570
|
+
const existingIndex = group.items.findIndex((i) => i.id === itemId || i.slug === itemOpt.slug);
|
|
6571
|
+
const existingItem = existingIndex >= 0 ? group.items[existingIndex] : void 0;
|
|
6572
|
+
const col = schemas?.collections?.find((c) => c.slug === itemOpt.collection);
|
|
6573
|
+
const colViews = col?.views || [];
|
|
6574
|
+
const baseViews = existingItem?.views || [];
|
|
6575
|
+
const userViews = itemOpt.views || [];
|
|
6576
|
+
const userViewSlugs = new Set(userViews.map((v) => v.slug));
|
|
6577
|
+
let mergedViews = [
|
|
6578
|
+
...baseViews.filter((v) => !userViewSlugs.has(v.slug)),
|
|
6579
|
+
...userViews
|
|
6580
|
+
];
|
|
6581
|
+
if (mergedViews.length === 0 && itemOpt.collection) {
|
|
6582
|
+
if (colViews.length > 0) {
|
|
6583
|
+
mergedViews = [...colViews];
|
|
6584
|
+
} else {
|
|
6585
|
+
mergedViews = [
|
|
6586
|
+
{
|
|
6587
|
+
slug: "default",
|
|
6588
|
+
label: col?.label || col?.labels?.plural || itemOpt.label || "All Records",
|
|
6589
|
+
layout: "table",
|
|
6590
|
+
collection: itemOpt.collection
|
|
6591
|
+
}
|
|
6592
|
+
];
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
6595
|
+
mergedViews = mergedViews.map((v) => ({
|
|
6596
|
+
...v,
|
|
6597
|
+
collection: v.collection || itemOpt.collection || existingItem?.collection
|
|
6598
|
+
}));
|
|
6599
|
+
const compiledItem = {
|
|
6600
|
+
id: itemId,
|
|
6601
|
+
type: itemOpt.slug ? "workspace" : itemOpt.collection ? "collection" : itemOpt.global ? "global" : "workspace",
|
|
6602
|
+
slug: itemOpt.slug || itemOpt.collection || itemOpt.global || "item",
|
|
6603
|
+
label: itemOpt.label || itemOpt.slug || "Item",
|
|
6604
|
+
icon: itemOpt.icon || existingItem?.icon || col?.admin?.icon || "Briefcase",
|
|
6605
|
+
group: group.name,
|
|
6606
|
+
order: itemOpt.order ?? existingItem?.order ?? 100,
|
|
6607
|
+
views: mergedViews,
|
|
6608
|
+
collection: itemOpt.collection || existingItem?.collection,
|
|
6609
|
+
global: itemOpt.global || existingItem?.global,
|
|
6610
|
+
badge: itemOpt.badge || existingItem?.badge,
|
|
6611
|
+
access: itemOpt.access || existingItem?.access
|
|
6612
|
+
};
|
|
6613
|
+
if (existingIndex >= 0) {
|
|
6614
|
+
group.items[existingIndex] = compiledItem;
|
|
6615
|
+
} else {
|
|
6616
|
+
group.items.push(compiledItem);
|
|
6597
6617
|
}
|
|
6598
6618
|
}
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
}
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
}
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6619
|
+
const allAvailableItems = [];
|
|
6620
|
+
const reconciledGroups = [];
|
|
6621
|
+
for (const group of groupsList) {
|
|
6622
|
+
const isGroupHidden = hiddenSet.has(group.id) || !!group.slug && hiddenSet.has(group.slug) || hiddenSet.has(group.name);
|
|
6623
|
+
if (!includeHidden && isGroupHidden) {
|
|
6624
|
+
continue;
|
|
6625
|
+
}
|
|
6626
|
+
const visibleItems = [];
|
|
6627
|
+
for (const item of group.items) {
|
|
6628
|
+
const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
|
|
6629
|
+
if (!includeHidden && isItemHidden) {
|
|
6630
|
+
continue;
|
|
6631
|
+
}
|
|
6632
|
+
const allViews = item.views || [];
|
|
6633
|
+
const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
|
|
6634
|
+
const compoundKey = `${item.slug}_${v.slug}`;
|
|
6635
|
+
return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
|
|
6636
|
+
});
|
|
6637
|
+
let isTombstone = false;
|
|
6638
|
+
if (schemas && schemas.collections && schemas.collections.length > 0) {
|
|
6639
|
+
if (item.collection && !knownCollectionSlugs.has(item.collection)) {
|
|
6640
|
+
isTombstone = true;
|
|
6641
|
+
}
|
|
6642
|
+
}
|
|
6643
|
+
if (schemas && schemas.globals && schemas.globals.length > 0) {
|
|
6644
|
+
if (item.global && !knownGlobalSlugs.has(item.global)) {
|
|
6645
|
+
isTombstone = true;
|
|
6646
|
+
}
|
|
6647
|
+
}
|
|
6648
|
+
const processedItem = {
|
|
6649
|
+
...item,
|
|
6650
|
+
views: visibleViews,
|
|
6651
|
+
...isTombstone ? { isTombstone: true } : {}
|
|
6652
|
+
};
|
|
6653
|
+
visibleItems.push(processedItem);
|
|
6654
|
+
allAvailableItems.push(processedItem);
|
|
6655
|
+
}
|
|
6656
|
+
const groupKey = group.slug || group.name;
|
|
6657
|
+
const itemOrderList = effectivePrefs.itemOrder?.[group.id] || effectivePrefs.itemOrder?.[groupKey];
|
|
6658
|
+
if (itemOrderList && itemOrderList.length > 0) {
|
|
6659
|
+
visibleItems.sort((a, b) => {
|
|
6660
|
+
const aIdx = itemOrderList.indexOf(a.id) >= 0 ? itemOrderList.indexOf(a.id) : a.slug ? itemOrderList.indexOf(a.slug) : -1;
|
|
6661
|
+
const bIdx = itemOrderList.indexOf(b.id) >= 0 ? itemOrderList.indexOf(b.id) : b.slug ? itemOrderList.indexOf(b.slug) : -1;
|
|
6662
|
+
if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
|
|
6663
|
+
if (aIdx >= 0) return -1;
|
|
6664
|
+
if (bIdx >= 0) return 1;
|
|
6665
|
+
return (a.order ?? 100) - (b.order ?? 100);
|
|
6666
|
+
});
|
|
6667
|
+
} else {
|
|
6668
|
+
visibleItems.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
6669
|
+
}
|
|
6670
|
+
reconciledGroups.push({
|
|
6671
|
+
...group,
|
|
6672
|
+
items: visibleItems
|
|
6673
|
+
});
|
|
6651
6674
|
}
|
|
6652
|
-
if (
|
|
6653
|
-
|
|
6654
|
-
|
|
6675
|
+
if (effectivePrefs.groupOrder && effectivePrefs.groupOrder.length > 0) {
|
|
6676
|
+
reconciledGroups.sort((a, b) => {
|
|
6677
|
+
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);
|
|
6678
|
+
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);
|
|
6679
|
+
if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
|
|
6680
|
+
if (aIdx >= 0) return -1;
|
|
6681
|
+
if (bIdx >= 0) return 1;
|
|
6682
|
+
return (a.order ?? 100) - (b.order ?? 100);
|
|
6683
|
+
});
|
|
6684
|
+
} else {
|
|
6685
|
+
reconciledGroups.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
6655
6686
|
}
|
|
6656
|
-
|
|
6657
|
-
|
|
6687
|
+
const reconciledUngrouped = (baseTree.ungrouped || []).filter((item) => {
|
|
6688
|
+
const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
|
|
6689
|
+
return includeHidden || !isItemHidden;
|
|
6690
|
+
}).map((item) => {
|
|
6691
|
+
const allViews = item.views || [];
|
|
6692
|
+
const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
|
|
6693
|
+
const compoundKey = `${item.slug}_${v.slug}`;
|
|
6694
|
+
return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
|
|
6695
|
+
});
|
|
6696
|
+
const processed = { ...item, views: visibleViews };
|
|
6697
|
+
allAvailableItems.push(processed);
|
|
6698
|
+
return processed;
|
|
6699
|
+
});
|
|
6700
|
+
const pinnedItems = [];
|
|
6701
|
+
for (const pin of effectivePrefs.pinned || []) {
|
|
6702
|
+
const match = allAvailableItems.find((i) => i.slug === pin.slug || i.id === pin.slug);
|
|
6703
|
+
if (match) {
|
|
6704
|
+
pinnedItems.push({
|
|
6705
|
+
...match,
|
|
6706
|
+
id: `pin_${match.id}`,
|
|
6707
|
+
label: pin.label || match.label,
|
|
6708
|
+
icon: pin.icon || match.icon
|
|
6709
|
+
});
|
|
6710
|
+
} else {
|
|
6711
|
+
pinnedItems.push({
|
|
6712
|
+
id: `pin_${pin.slug}`,
|
|
6713
|
+
type: pin.type,
|
|
6714
|
+
slug: pin.slug,
|
|
6715
|
+
label: pin.label || pin.slug,
|
|
6716
|
+
icon: pin.icon || "Star",
|
|
6717
|
+
order: 0,
|
|
6718
|
+
views: []
|
|
6719
|
+
});
|
|
6720
|
+
}
|
|
6658
6721
|
}
|
|
6659
|
-
const type = collectionOrSlug.idType || "prefixed-nanoid";
|
|
6660
|
-
const prefix = resolvePrefix(collectionOrSlug.slug, collectionOrSlug.idPrefix);
|
|
6661
|
-
return generateId({ prefix, type });
|
|
6662
|
-
}
|
|
6663
|
-
|
|
6664
|
-
// src/workflows.ts
|
|
6665
|
-
var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
|
|
6666
|
-
var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
|
|
6667
|
-
function simplePublishingWorkflow() {
|
|
6668
6722
|
return {
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
{ name: "draft", label: "Draft", color: "neutral" },
|
|
6673
|
-
{ name: "published", label: "Published", color: "success", published: true }
|
|
6674
|
-
],
|
|
6675
|
-
transitions: [
|
|
6676
|
-
{ name: "publish", label: "Publish", from: ["draft", "published"], to: "published" },
|
|
6677
|
-
{ name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
|
|
6678
|
-
]
|
|
6679
|
-
// Intentionally no `roles`: this lightweight workflow (synthesized from
|
|
6680
|
-
// `drafts: true`) does no capability-based gating. Draft visibility for a
|
|
6681
|
-
// no-roles workflow is handled in `canViewWorkflowDraft`.
|
|
6723
|
+
groups: reconciledGroups,
|
|
6724
|
+
ungrouped: reconciledUngrouped,
|
|
6725
|
+
pinnedItems
|
|
6682
6726
|
};
|
|
6683
6727
|
}
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6728
|
+
|
|
6729
|
+
// src/utils/navigation.ts
|
|
6730
|
+
var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
|
|
6731
|
+
"collections",
|
|
6732
|
+
"globals",
|
|
6733
|
+
"setup",
|
|
6734
|
+
"api",
|
|
6735
|
+
"login",
|
|
6736
|
+
"logout",
|
|
6737
|
+
"settings",
|
|
6738
|
+
"admin",
|
|
6739
|
+
"preferences",
|
|
6740
|
+
"auth"
|
|
6741
|
+
]);
|
|
6742
|
+
function assertValidNavigationSlugs(navigation) {
|
|
6743
|
+
for (const item of navigation) {
|
|
6744
|
+
if (item.slug) {
|
|
6745
|
+
const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
|
|
6746
|
+
if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
|
|
6747
|
+
throw new Error(
|
|
6748
|
+
`[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.`
|
|
6749
|
+
);
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6693
6752
|
}
|
|
6694
|
-
return capabilities;
|
|
6695
6753
|
}
|
|
6696
|
-
function
|
|
6697
|
-
if (!
|
|
6698
|
-
|
|
6699
|
-
if (capabilities.has("entry.edit")) return true;
|
|
6700
|
-
if (workflow.transitions.some(
|
|
6701
|
-
(transition) => (transition.requiredCapabilities ?? []).some((capability) => capabilities.has(capability))
|
|
6702
|
-
)) {
|
|
6703
|
-
return true;
|
|
6754
|
+
function resolveGroupMeta(groupProp, defaultGroupName = "Collections") {
|
|
6755
|
+
if (!groupProp) {
|
|
6756
|
+
return { name: defaultGroupName };
|
|
6704
6757
|
}
|
|
6705
|
-
if (
|
|
6706
|
-
|
|
6707
|
-
}
|
|
6708
|
-
|
|
6709
|
-
const capabilities = workflowCapabilities(workflow, user);
|
|
6710
|
-
return workflow.transitions.filter((transition) => {
|
|
6711
|
-
const from = Array.isArray(transition.from) ? transition.from : [transition.from];
|
|
6712
|
-
return from.includes(state) && (transition.requiredCapabilities ?? []).every((item) => capabilities.has(item));
|
|
6713
|
-
});
|
|
6714
|
-
}
|
|
6715
|
-
function initializeWorkflowDocument(data, workflow) {
|
|
6716
|
-
return {
|
|
6717
|
-
...data,
|
|
6718
|
-
__workflow: { state: workflow.initialState, revision: 1 }
|
|
6719
|
-
};
|
|
6720
|
-
}
|
|
6721
|
-
function publishedStateName(workflow) {
|
|
6722
|
-
return workflow.states.find((state) => state.published)?.name ?? workflow.states[workflow.states.length - 1]?.name ?? workflow.initialState;
|
|
6758
|
+
if (typeof groupProp === "string") {
|
|
6759
|
+
return { name: groupProp };
|
|
6760
|
+
}
|
|
6761
|
+
return { name: groupProp.name, meta: groupProp };
|
|
6723
6762
|
}
|
|
6724
|
-
function
|
|
6725
|
-
const
|
|
6726
|
-
const
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
_workflow: {
|
|
6733
|
-
state,
|
|
6734
|
-
revision: 1,
|
|
6735
|
-
availableTransitions: availableWorkflowTransitions(workflow, state, user).map((item) => item.name)
|
|
6736
|
-
}
|
|
6737
|
-
};
|
|
6763
|
+
function compileNavigation(config) {
|
|
6764
|
+
const collections = config.collections || [];
|
|
6765
|
+
const globals = config.globals || [];
|
|
6766
|
+
const explicitNav = config.admin?.navigation || [];
|
|
6767
|
+
assertValidNavigationSlugs(explicitNav);
|
|
6768
|
+
const collectionsBySlug = /* @__PURE__ */ new Map();
|
|
6769
|
+
for (const col of collections) {
|
|
6770
|
+
collectionsBySlug.set(col.slug, col);
|
|
6738
6771
|
}
|
|
6739
|
-
const
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6772
|
+
const globalsBySlug = /* @__PURE__ */ new Map();
|
|
6773
|
+
for (const gl of globals) {
|
|
6774
|
+
globalsBySlug.set(gl.slug, gl);
|
|
6775
|
+
}
|
|
6776
|
+
const mentionedCollections = /* @__PURE__ */ new Set();
|
|
6777
|
+
const mentionedGlobals = /* @__PURE__ */ new Set();
|
|
6778
|
+
const injectedViewsByCollection = /* @__PURE__ */ new Map();
|
|
6779
|
+
for (const item of explicitNav) {
|
|
6780
|
+
if (item.addToCollection) {
|
|
6781
|
+
const existing = injectedViewsByCollection.get(item.addToCollection) || [];
|
|
6782
|
+
if (item.views && item.views.length > 0) {
|
|
6783
|
+
injectedViewsByCollection.set(item.addToCollection, [...existing, ...item.views]);
|
|
6744
6784
|
}
|
|
6745
|
-
return null;
|
|
6746
6785
|
}
|
|
6747
|
-
|
|
6786
|
+
if (item.collection) mentionedCollections.add(item.collection);
|
|
6787
|
+
if (item.global) mentionedGlobals.add(item.global);
|
|
6748
6788
|
}
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6789
|
+
const groupMetadataMap = /* @__PURE__ */ new Map();
|
|
6790
|
+
const itemsByGroup = /* @__PURE__ */ new Map();
|
|
6791
|
+
const ungroupedItems = [];
|
|
6792
|
+
const registerItem = (groupName, item) => {
|
|
6793
|
+
if (!groupName) {
|
|
6794
|
+
ungroupedItems.push(item);
|
|
6795
|
+
} else {
|
|
6796
|
+
const list = itemsByGroup.get(groupName) || [];
|
|
6797
|
+
list.push(item);
|
|
6798
|
+
itemsByGroup.set(groupName, list);
|
|
6754
6799
|
}
|
|
6755
6800
|
};
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
}
|
|
6760
|
-
function createLifecycleEvent(args) {
|
|
6761
|
-
return {
|
|
6762
|
-
id: eventId(),
|
|
6763
|
-
name: args.name,
|
|
6764
|
-
collection: args.collection,
|
|
6765
|
-
documentId: args.documentId,
|
|
6766
|
-
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6767
|
-
actorId: args.actorId,
|
|
6768
|
-
payload: args.payload,
|
|
6769
|
-
attempts: 0,
|
|
6770
|
-
status: "pending"
|
|
6771
|
-
};
|
|
6772
|
-
}
|
|
6773
|
-
async function persistEvent(db, event) {
|
|
6774
|
-
await db.create({ collection: LIFECYCLE_EVENTS_COLLECTION, data: event });
|
|
6775
|
-
}
|
|
6776
|
-
async function dispatchLifecycleEvent(config, event) {
|
|
6777
|
-
const db = config.db;
|
|
6778
|
-
if (!db || !config.events?.handlers.length) return;
|
|
6779
|
-
const maxAttempts = config.events.maxAttempts ?? 8;
|
|
6780
|
-
const retryDelayMs = config.events.retryDelayMs ?? 1e3;
|
|
6781
|
-
const attempts = event.attempts + 1;
|
|
6782
|
-
try {
|
|
6783
|
-
await db.update({
|
|
6784
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
6785
|
-
id: event.id,
|
|
6786
|
-
data: { status: "processing", attempts }
|
|
6787
|
-
});
|
|
6788
|
-
for (const handler of config.events.handlers) await handler({ ...event, status: "processing", attempts });
|
|
6789
|
-
await db.update({
|
|
6790
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
6791
|
-
id: event.id,
|
|
6792
|
-
data: { status: "delivered", attempts, deliveredAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null }
|
|
6793
|
-
});
|
|
6794
|
-
} catch (error) {
|
|
6795
|
-
const exhausted = attempts >= maxAttempts;
|
|
6796
|
-
await db.update({
|
|
6797
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
6798
|
-
id: event.id,
|
|
6799
|
-
data: {
|
|
6800
|
-
status: "failed",
|
|
6801
|
-
attempts,
|
|
6802
|
-
lastError: error instanceof Error ? error.message : String(error),
|
|
6803
|
-
nextAttemptAt: exhausted ? null : new Date(Date.now() + retryDelayMs * 2 ** (attempts - 1)).toISOString()
|
|
6804
|
-
}
|
|
6805
|
-
});
|
|
6806
|
-
}
|
|
6807
|
-
}
|
|
6808
|
-
async function saveWorkflowDraft(args) {
|
|
6809
|
-
const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
|
|
6810
|
-
const db = config.db;
|
|
6811
|
-
const workflow = collection.workflow;
|
|
6812
|
-
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
6813
|
-
const previous = originalDoc.__workflow;
|
|
6814
|
-
const isLegacyPublishedDoc = !previous;
|
|
6815
|
-
const baselineRevision = previous?.revision ?? 1;
|
|
6816
|
-
const event = createLifecycleEvent({
|
|
6817
|
-
name: "revision.created",
|
|
6818
|
-
collection: collection.slug,
|
|
6819
|
-
documentId: id,
|
|
6820
|
-
actorId: user?.sub,
|
|
6821
|
-
payload: {
|
|
6822
|
-
revision: baselineRevision + 1,
|
|
6823
|
-
previousRevision: isLegacyPublishedDoc ? baselineRevision : previous?.revision ?? null
|
|
6801
|
+
for (const item of explicitNav) {
|
|
6802
|
+
if (item.addToCollection && !item.slug && !item.collection && !item.global && !item.dashboard && !item.href) {
|
|
6803
|
+
continue;
|
|
6824
6804
|
}
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
...previous ?? {},
|
|
6829
|
-
state: isLegacyPublishedDoc ? workflow.draftState ?? workflow.initialState : originalDoc.__published ? workflow.draftState ?? workflow.initialState : previous.state,
|
|
6830
|
-
revision: baselineRevision + 1,
|
|
6831
|
-
...isLegacyPublishedDoc ? { publishedRevision: baselineRevision } : {}
|
|
6832
|
-
};
|
|
6833
|
-
const updateData = {
|
|
6834
|
-
...data,
|
|
6835
|
-
__workflow: nextMeta
|
|
6836
|
-
};
|
|
6837
|
-
if (isLegacyPublishedDoc) {
|
|
6838
|
-
const { __workflow: _legacyWorkflow, __published: _legacyPublished, ...publishedSnapshot } = originalDoc;
|
|
6839
|
-
updateData.__published = publishedSnapshot;
|
|
6805
|
+
const { name: groupName, meta: groupMeta } = resolveGroupMeta(item.group, void 0);
|
|
6806
|
+
if (groupName && groupMeta) {
|
|
6807
|
+
groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
|
|
6840
6808
|
}
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
6853
|
-
let event;
|
|
6854
|
-
const doc = await db.transaction(async (tx) => {
|
|
6855
|
-
const created = await tx.create({ collection: collection.slug, data });
|
|
6856
|
-
event = createLifecycleEvent({
|
|
6857
|
-
name: "revision.created",
|
|
6858
|
-
collection: collection.slug,
|
|
6859
|
-
documentId: created.id,
|
|
6860
|
-
actorId: user?.sub,
|
|
6861
|
-
payload: { revision: 1, previousRevision: null }
|
|
6862
|
-
});
|
|
6863
|
-
await persistEvent(tx, event);
|
|
6864
|
-
await beforeCommit?.(tx, created);
|
|
6865
|
-
return created;
|
|
6866
|
-
});
|
|
6867
|
-
void dispatchLifecycleEvent(config, event);
|
|
6868
|
-
return { doc, event };
|
|
6869
|
-
}
|
|
6870
|
-
async function transitionWorkflow(args) {
|
|
6871
|
-
const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
|
|
6872
|
-
const db = config.db;
|
|
6873
|
-
const workflow = collection.workflow;
|
|
6874
|
-
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
6875
|
-
const original = await db.findOne({ collection: collection.slug, id });
|
|
6876
|
-
if (!original) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
|
|
6877
|
-
const meta = original.__workflow;
|
|
6878
|
-
if (!meta) throw Object.assign(new Error("Entry has no workflow metadata"), { statusCode: 409 });
|
|
6879
|
-
if (expectedRevision !== void 0 && expectedRevision !== meta.revision) {
|
|
6880
|
-
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
|
|
6881
|
-
statusCode: 409
|
|
6882
|
-
});
|
|
6883
|
-
}
|
|
6884
|
-
const transition = workflow.transitions.find((item) => item.name === transitionName);
|
|
6885
|
-
const fromStates = transition ? Array.isArray(transition.from) ? transition.from : [transition.from] : [];
|
|
6886
|
-
if (!transition || !fromStates.includes(meta.state)) {
|
|
6887
|
-
throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), {
|
|
6888
|
-
statusCode: 409
|
|
6889
|
-
});
|
|
6890
|
-
}
|
|
6891
|
-
if (!availableWorkflowTransitions(workflow, meta.state, user).some((item) => item.name === transition.name)) {
|
|
6892
|
-
throw Object.assign(new Error(`You do not have permission to perform "${transition.label}".`), { statusCode: 403 });
|
|
6893
|
-
}
|
|
6894
|
-
if (transition.requireComment && !comment?.trim()) {
|
|
6895
|
-
throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
|
|
6896
|
-
}
|
|
6897
|
-
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, input, req, db };
|
|
6898
|
-
for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
|
|
6899
|
-
const events = [
|
|
6900
|
-
createLifecycleEvent({
|
|
6901
|
-
name: "workflow.transitioned",
|
|
6902
|
-
collection: collection.slug,
|
|
6903
|
-
documentId: id,
|
|
6904
|
-
actorId: user?.sub,
|
|
6905
|
-
payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
|
|
6906
|
-
})
|
|
6907
|
-
];
|
|
6908
|
-
const targetState = workflow.states.find((state) => state.name === transition.to);
|
|
6909
|
-
if (targetState.published) {
|
|
6910
|
-
events.push(
|
|
6911
|
-
createLifecycleEvent({
|
|
6912
|
-
name: "entry.published",
|
|
6913
|
-
collection: collection.slug,
|
|
6914
|
-
documentId: id,
|
|
6915
|
-
actorId: user?.sub,
|
|
6916
|
-
payload: { revision: meta.revision }
|
|
6917
|
-
})
|
|
6918
|
-
);
|
|
6919
|
-
} else if (transition.unpublish) {
|
|
6920
|
-
events.push(
|
|
6921
|
-
createLifecycleEvent({
|
|
6922
|
-
name: "entry.unpublished",
|
|
6923
|
-
collection: collection.slug,
|
|
6924
|
-
documentId: id,
|
|
6925
|
-
actorId: user?.sub,
|
|
6926
|
-
payload: { revision: meta.publishedRevision }
|
|
6927
|
-
})
|
|
6928
|
-
);
|
|
6929
|
-
}
|
|
6930
|
-
const updated = await db.transaction(async (tx) => {
|
|
6931
|
-
const locked = await tx.findOne({ collection: collection.slug, id });
|
|
6932
|
-
if (!locked) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
|
|
6933
|
-
const lockedMeta = locked.__workflow;
|
|
6934
|
-
if (lockedMeta.revision !== meta.revision || lockedMeta.state !== meta.state || expectedRevision !== void 0 && lockedMeta.revision !== expectedRevision) {
|
|
6935
|
-
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
|
|
6936
|
-
statusCode: 409
|
|
6809
|
+
if (item.dashboard) {
|
|
6810
|
+
registerItem(groupName, {
|
|
6811
|
+
id: "dashboard",
|
|
6812
|
+
type: "dashboard",
|
|
6813
|
+
slug: "dashboard",
|
|
6814
|
+
label: item.label || "Dashboard",
|
|
6815
|
+
icon: item.icon || "LayoutDashboard",
|
|
6816
|
+
group: groupName,
|
|
6817
|
+
order: item.order ?? 0,
|
|
6818
|
+
views: [],
|
|
6819
|
+
access: item.access
|
|
6937
6820
|
});
|
|
6821
|
+
continue;
|
|
6938
6822
|
}
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
comment,
|
|
6959
|
-
user,
|
|
6960
|
-
req,
|
|
6961
|
-
tx
|
|
6823
|
+
if (item.collection) {
|
|
6824
|
+
const col = collectionsBySlug.get(item.collection);
|
|
6825
|
+
const combinedViews = [
|
|
6826
|
+
...col?.views || [],
|
|
6827
|
+
...injectedViewsByCollection.get(item.collection) || [],
|
|
6828
|
+
...item.views || []
|
|
6829
|
+
];
|
|
6830
|
+
registerItem(groupName, {
|
|
6831
|
+
id: item.slug || `collection_${item.collection}`,
|
|
6832
|
+
type: item.slug ? "workspace" : "collection",
|
|
6833
|
+
slug: item.slug || item.collection,
|
|
6834
|
+
collection: item.collection,
|
|
6835
|
+
label: item.label || col?.labels?.plural || item.collection,
|
|
6836
|
+
icon: item.icon || col?.admin?.icon || "Folder",
|
|
6837
|
+
group: groupName,
|
|
6838
|
+
order: item.order ?? col?.admin?.order ?? 100,
|
|
6839
|
+
views: combinedViews,
|
|
6840
|
+
badge: item.badge,
|
|
6841
|
+
access: item.access || col?.access?.read
|
|
6962
6842
|
});
|
|
6843
|
+
continue;
|
|
6963
6844
|
}
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
for (const event of events) await persistEvent(tx, event);
|
|
6979
|
-
if (collection.audit) {
|
|
6980
|
-
await tx.create({
|
|
6981
|
-
collection: "__audit",
|
|
6982
|
-
data: {
|
|
6983
|
-
collection: collection.slug,
|
|
6984
|
-
documentId: id,
|
|
6985
|
-
operation: "workflow.transition",
|
|
6986
|
-
user: user?.sub ?? null,
|
|
6987
|
-
timestamp: now2,
|
|
6988
|
-
changes: JSON.stringify({
|
|
6989
|
-
transition: transition.name,
|
|
6990
|
-
from: lockedMeta.state,
|
|
6991
|
-
to: transition.to,
|
|
6992
|
-
revision: lockedMeta.revision
|
|
6993
|
-
})
|
|
6994
|
-
}
|
|
6845
|
+
if (item.global) {
|
|
6846
|
+
const gl = globalsBySlug.get(item.global);
|
|
6847
|
+
registerItem(groupName, {
|
|
6848
|
+
id: item.slug || `global_${item.global}`,
|
|
6849
|
+
type: "global",
|
|
6850
|
+
slug: item.slug || item.global,
|
|
6851
|
+
global: item.global,
|
|
6852
|
+
label: item.label || gl?.label || item.global,
|
|
6853
|
+
icon: item.icon || "Settings",
|
|
6854
|
+
group: groupName,
|
|
6855
|
+
order: item.order ?? 100,
|
|
6856
|
+
views: [],
|
|
6857
|
+
badge: item.badge,
|
|
6858
|
+
access: item.access
|
|
6995
6859
|
});
|
|
6860
|
+
continue;
|
|
6996
6861
|
}
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
logger.child({ component: "workflow" }).error({
|
|
7013
|
-
err: error,
|
|
7014
|
-
msg: "afterTransition hook failed",
|
|
7015
|
-
collection: collection.slug,
|
|
7016
|
-
transition: transition.name,
|
|
7017
|
-
documentId: id
|
|
7018
|
-
});
|
|
7019
|
-
} else if (logger && typeof logger.error === "function") {
|
|
7020
|
-
logger.error({
|
|
7021
|
-
err: error,
|
|
7022
|
-
msg: "afterTransition hook failed",
|
|
7023
|
-
collection: collection.slug,
|
|
7024
|
-
transition: transition.name,
|
|
7025
|
-
documentId: id
|
|
7026
|
-
});
|
|
7027
|
-
} else {
|
|
7028
|
-
console.error("afterTransition hook failed:", error);
|
|
7029
|
-
}
|
|
7030
|
-
} catch {
|
|
7031
|
-
console.error("afterTransition hook failed:", error);
|
|
7032
|
-
}
|
|
6862
|
+
if (item.href) {
|
|
6863
|
+
registerItem(groupName, {
|
|
6864
|
+
id: `link_${item.slug || item.href}`,
|
|
6865
|
+
type: "link",
|
|
6866
|
+
slug: item.slug || "link",
|
|
6867
|
+
label: item.label || "Link",
|
|
6868
|
+
href: item.href,
|
|
6869
|
+
icon: item.icon || "ExternalLink",
|
|
6870
|
+
group: groupName,
|
|
6871
|
+
order: item.order ?? 100,
|
|
6872
|
+
views: [],
|
|
6873
|
+
badge: item.badge,
|
|
6874
|
+
access: item.access
|
|
6875
|
+
});
|
|
6876
|
+
continue;
|
|
7033
6877
|
}
|
|
6878
|
+
const workspaceSlug = item.slug || "workspace";
|
|
6879
|
+
registerItem(groupName, {
|
|
6880
|
+
id: `workspace_${workspaceSlug}`,
|
|
6881
|
+
type: "workspace",
|
|
6882
|
+
slug: workspaceSlug,
|
|
6883
|
+
label: item.label || workspaceSlug,
|
|
6884
|
+
icon: item.icon || "Briefcase",
|
|
6885
|
+
group: groupName,
|
|
6886
|
+
order: item.order ?? 100,
|
|
6887
|
+
views: item.views || [],
|
|
6888
|
+
badge: item.badge,
|
|
6889
|
+
access: item.access
|
|
6890
|
+
});
|
|
7034
6891
|
}
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
6892
|
+
for (const col of collections) {
|
|
6893
|
+
if (mentionedCollections.has(col.slug)) continue;
|
|
6894
|
+
if (col.admin?.hidden) continue;
|
|
6895
|
+
const groupProp = col.admin?.group;
|
|
6896
|
+
const { name: groupName, meta: groupMeta } = resolveGroupMeta(groupProp, "Collections");
|
|
6897
|
+
if (groupMeta) {
|
|
6898
|
+
groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
|
|
6899
|
+
}
|
|
6900
|
+
const combinedViews = [
|
|
6901
|
+
...col.views || [],
|
|
6902
|
+
...injectedViewsByCollection.get(col.slug) || []
|
|
6903
|
+
];
|
|
6904
|
+
registerItem(groupName, {
|
|
6905
|
+
id: `collection_${col.slug}`,
|
|
6906
|
+
type: "collection",
|
|
6907
|
+
slug: col.slug,
|
|
6908
|
+
collection: col.slug,
|
|
6909
|
+
label: col.labels?.plural || col.slug,
|
|
6910
|
+
icon: col.admin?.icon || "Folder",
|
|
6911
|
+
group: groupName,
|
|
6912
|
+
order: col?.admin?.order ?? 100,
|
|
6913
|
+
views: combinedViews,
|
|
6914
|
+
access: col.access?.read
|
|
6915
|
+
});
|
|
7045
6916
|
}
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
6917
|
+
for (const gl of globals) {
|
|
6918
|
+
if (mentionedGlobals.has(gl.slug)) continue;
|
|
6919
|
+
const groupName = "Configuration";
|
|
6920
|
+
registerItem(groupName, {
|
|
6921
|
+
id: `global_${gl.slug}`,
|
|
6922
|
+
type: "global",
|
|
6923
|
+
slug: gl.slug,
|
|
6924
|
+
global: gl.slug,
|
|
6925
|
+
label: gl.label || gl.slug,
|
|
6926
|
+
icon: "Settings",
|
|
6927
|
+
group: groupName,
|
|
6928
|
+
order: 100,
|
|
6929
|
+
views: []
|
|
6930
|
+
});
|
|
7051
6931
|
}
|
|
7052
|
-
const
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
if (
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
6932
|
+
const sortAndSpliceItems = (items) => {
|
|
6933
|
+
const sorted = [...items].sort((a, b) => a.order - b.order);
|
|
6934
|
+
const explicitMap = /* @__PURE__ */ new Map();
|
|
6935
|
+
for (const opt of explicitNav) {
|
|
6936
|
+
if (opt.slug) explicitMap.set(opt.slug, opt);
|
|
6937
|
+
if (opt.collection) explicitMap.set(opt.collection, opt);
|
|
6938
|
+
if (opt.global) explicitMap.set(opt.global, opt);
|
|
6939
|
+
}
|
|
6940
|
+
for (const opt of explicitNav) {
|
|
6941
|
+
const itemKey = opt.slug || opt.collection || opt.global;
|
|
6942
|
+
if (!itemKey) continue;
|
|
6943
|
+
if (opt.after) {
|
|
6944
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
6945
|
+
const targetIdx = sorted.findIndex((i) => i.slug === opt.after);
|
|
6946
|
+
if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
|
|
6947
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
6948
|
+
const newTargetIdx = sorted.findIndex((i) => i.slug === opt.after);
|
|
6949
|
+
sorted.splice(newTargetIdx + 1, 0, moved);
|
|
7066
6950
|
}
|
|
7067
|
-
} else if (
|
|
7068
|
-
|
|
7069
|
-
|
|
6951
|
+
} else if (opt.before) {
|
|
6952
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
6953
|
+
const targetIdx = sorted.findIndex((i) => i.slug === opt.before);
|
|
6954
|
+
if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
|
|
6955
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
6956
|
+
const newTargetIdx = sorted.findIndex((i) => i.slug === opt.before);
|
|
6957
|
+
sorted.splice(newTargetIdx, 0, moved);
|
|
7070
6958
|
}
|
|
7071
|
-
} else if (
|
|
7072
|
-
|
|
7073
|
-
|
|
6959
|
+
} else if (opt.position === "first") {
|
|
6960
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
6961
|
+
if (itemIdx > 0) {
|
|
6962
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
6963
|
+
sorted.unshift(moved);
|
|
6964
|
+
}
|
|
6965
|
+
} else if (opt.position === "last") {
|
|
6966
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
6967
|
+
if (itemIdx !== -1 && itemIdx < sorted.length - 1) {
|
|
6968
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
6969
|
+
sorted.push(moved);
|
|
7074
6970
|
}
|
|
7075
6971
|
}
|
|
7076
6972
|
}
|
|
6973
|
+
return sorted;
|
|
7077
6974
|
};
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
}
|
|
6975
|
+
const compiledGroups = [];
|
|
6976
|
+
for (const [groupName, rawItems] of itemsByGroup.entries()) {
|
|
6977
|
+
const meta = groupMetadataMap.get(groupName);
|
|
6978
|
+
const sortedItems = sortAndSpliceItems(rawItems);
|
|
6979
|
+
compiledGroups.push({
|
|
6980
|
+
id: `group_${groupName.toLowerCase().replace(/\s+/g, "_")}`,
|
|
6981
|
+
name: groupName,
|
|
6982
|
+
slug: meta?.slug || groupName.toLowerCase().replace(/\s+/g, "-"),
|
|
6983
|
+
icon: meta?.icon,
|
|
6984
|
+
defaultExpanded: meta?.defaultExpanded ?? true,
|
|
6985
|
+
order: meta?.order ?? 100,
|
|
6986
|
+
items: sortedItems
|
|
6987
|
+
});
|
|
7091
6988
|
}
|
|
6989
|
+
compiledGroups.sort((a, b) => a.order - b.order);
|
|
7092
6990
|
return {
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
...doc._meta || {},
|
|
7096
|
-
computed: evaluated
|
|
7097
|
-
}
|
|
6991
|
+
groups: compiledGroups,
|
|
6992
|
+
ungrouped: sortAndSpliceItems(ungroupedItems)
|
|
7098
6993
|
};
|
|
7099
6994
|
}
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
// src/utils/tenant.ts
|
|
7106
|
-
var import_http_exception = require("hono/http-exception");
|
|
7107
|
-
function getAllowedSitesForUser(user, payload) {
|
|
7108
|
-
const raw = user?.allowedSites ?? user?.siteIds ?? user?.siteAccess ?? (user?.siteId ? [user.siteId] : void 0) ?? payload?.allowedSites ?? payload?.siteIds ?? payload?.siteAccess ?? (payload?.siteId ? [payload.siteId] : void 0);
|
|
7109
|
-
if (Array.isArray(raw)) {
|
|
7110
|
-
return raw.filter((s) => typeof s === "string" && s.trim().length > 0);
|
|
6995
|
+
function pruneNavigationForUser(tree, user) {
|
|
6996
|
+
const userRoles = /* @__PURE__ */ new Set();
|
|
6997
|
+
if (user?.role) userRoles.add(user.role);
|
|
6998
|
+
if (Array.isArray(user?.roles)) {
|
|
6999
|
+
for (const r of user.roles) userRoles.add(r);
|
|
7111
7000
|
}
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7001
|
+
const isAllowed = (item) => {
|
|
7002
|
+
if (!item.access) return true;
|
|
7003
|
+
if (Array.isArray(item.access)) {
|
|
7004
|
+
if (item.access.length === 0) return true;
|
|
7005
|
+
return item.access.some((role) => userRoles.has(role));
|
|
7006
|
+
}
|
|
7117
7007
|
return true;
|
|
7008
|
+
};
|
|
7009
|
+
const prunedGroups = [];
|
|
7010
|
+
for (const group of tree.groups) {
|
|
7011
|
+
const visibleItems = group.items.filter(isAllowed);
|
|
7012
|
+
if (visibleItems.length > 0) {
|
|
7013
|
+
prunedGroups.push({
|
|
7014
|
+
...group,
|
|
7015
|
+
items: visibleItems
|
|
7016
|
+
});
|
|
7017
|
+
}
|
|
7118
7018
|
}
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7019
|
+
return {
|
|
7020
|
+
groups: prunedGroups,
|
|
7021
|
+
ungrouped: tree.ungrouped.filter(isAllowed)
|
|
7022
|
+
};
|
|
7123
7023
|
}
|
|
7124
|
-
function
|
|
7125
|
-
const
|
|
7126
|
-
const
|
|
7127
|
-
const
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7024
|
+
function resolveAllCollectionViews(collection, config) {
|
|
7025
|
+
const views = [...collection.views ?? []];
|
|
7026
|
+
const seenSlugs = new Set(views.map((v) => v.slug));
|
|
7027
|
+
const explicitNav = config?.admin?.navigation;
|
|
7028
|
+
if (Array.isArray(explicitNav)) {
|
|
7029
|
+
for (const item of explicitNav) {
|
|
7030
|
+
if (!item) continue;
|
|
7031
|
+
const itemCol = item.collection || item.addToCollection;
|
|
7032
|
+
if (Array.isArray(item.views)) {
|
|
7033
|
+
for (const view of item.views) {
|
|
7034
|
+
if (!view || !view.slug) continue;
|
|
7035
|
+
const targetCol = view.collection || itemCol;
|
|
7036
|
+
if (targetCol === collection.slug && !seenSlugs.has(view.slug)) {
|
|
7037
|
+
views.push(view);
|
|
7038
|
+
seenSlugs.add(view.slug);
|
|
7039
|
+
}
|
|
7040
|
+
}
|
|
7041
|
+
}
|
|
7137
7042
|
}
|
|
7138
|
-
return requestedSiteId;
|
|
7139
7043
|
}
|
|
7140
|
-
return
|
|
7044
|
+
return views;
|
|
7141
7045
|
}
|
|
7142
7046
|
|
|
7143
7047
|
// src/controllers/collection.controller.ts
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
}
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
if (err instanceof import_http_exception2.HTTPException) {
|
|
7159
|
-
return {
|
|
7160
|
-
ok: false,
|
|
7161
|
-
response: c.json({ error: true, message: err.message }, err.status)
|
|
7162
|
-
};
|
|
7163
|
-
}
|
|
7164
|
-
throw err;
|
|
7048
|
+
init_access_control();
|
|
7049
|
+
|
|
7050
|
+
// src/utils/action-mutation.ts
|
|
7051
|
+
function resolveActionMutation(mutation, ctx) {
|
|
7052
|
+
if (!mutation) return { ...ctx.input ?? {} };
|
|
7053
|
+
return resolveValue(mutation, ctx);
|
|
7054
|
+
}
|
|
7055
|
+
function resolveValue(value, ctx) {
|
|
7056
|
+
if (typeof value === "string") return resolveExpression(value, ctx);
|
|
7057
|
+
if (Array.isArray(value)) return value.map((item) => resolveValue(item, ctx));
|
|
7058
|
+
if (isPlainObject2(value)) {
|
|
7059
|
+
const out = {};
|
|
7060
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
7061
|
+
out[key] = resolveValue(entry, ctx);
|
|
7165
7062
|
}
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7063
|
+
return out;
|
|
7064
|
+
}
|
|
7065
|
+
return value;
|
|
7066
|
+
}
|
|
7067
|
+
function resolveExpression(expression, ctx) {
|
|
7068
|
+
if (expression === "now()") return (/* @__PURE__ */ new Date()).toISOString();
|
|
7069
|
+
const inputPath = expression.match(/^input\.([A-Za-z0-9_.-]+)$/)?.[1];
|
|
7070
|
+
if (inputPath !== void 0) return getPath(ctx.input, inputPath);
|
|
7071
|
+
const docPath = expression.match(/^doc\.([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)$/)?.[1];
|
|
7072
|
+
if (docPath !== void 0) return getPath(ctx.doc, docPath);
|
|
7073
|
+
const userPath = expression.match(/^user\.([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)$/)?.[1];
|
|
7074
|
+
if (userPath !== void 0 && ctx.user) return getPath(ctx.user, userPath);
|
|
7075
|
+
return expression;
|
|
7076
|
+
}
|
|
7077
|
+
function getPath(source, path) {
|
|
7078
|
+
let cursor = source;
|
|
7079
|
+
for (const segment of path.split(".")) {
|
|
7080
|
+
if (!cursor || typeof cursor !== "object") return void 0;
|
|
7081
|
+
cursor = cursor[segment];
|
|
7082
|
+
}
|
|
7083
|
+
return cursor;
|
|
7084
|
+
}
|
|
7085
|
+
function isPlainObject2(value) {
|
|
7086
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7087
|
+
}
|
|
7088
|
+
|
|
7089
|
+
// src/utils/id.ts
|
|
7090
|
+
var import_node_crypto2 = require("crypto");
|
|
7091
|
+
var NANOID_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-";
|
|
7092
|
+
var BASE62_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
7093
|
+
var CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
7094
|
+
var COMMON_SLUG_PREFIXES = {
|
|
7095
|
+
users: "usr",
|
|
7096
|
+
members: "mem",
|
|
7097
|
+
accounts: "acc",
|
|
7098
|
+
applications: "app",
|
|
7099
|
+
customers: "cus",
|
|
7100
|
+
products: "prd",
|
|
7101
|
+
orders: "ord",
|
|
7102
|
+
reports: "rpt",
|
|
7103
|
+
sessions: "ses",
|
|
7104
|
+
transactions: "txn",
|
|
7105
|
+
wallets: "wal",
|
|
7106
|
+
media: "med",
|
|
7107
|
+
files: "fil",
|
|
7108
|
+
posts: "post",
|
|
7109
|
+
pages: "page",
|
|
7110
|
+
tags: "tag",
|
|
7111
|
+
categories: "cat",
|
|
7112
|
+
comments: "com"
|
|
7113
|
+
};
|
|
7114
|
+
function nanoid(size = 21, alphabet = NANOID_ALPHABET) {
|
|
7115
|
+
const mask = (2 << 31 - Math.clz32(alphabet.length - 1 | 1)) - 1;
|
|
7116
|
+
const step = Math.ceil(1.6 * mask * size / alphabet.length);
|
|
7117
|
+
let id = "";
|
|
7118
|
+
while (true) {
|
|
7119
|
+
const bytes = (0, import_node_crypto2.randomBytes)(step);
|
|
7120
|
+
for (let i = 0; i < step; i++) {
|
|
7121
|
+
const byte = bytes[i] & mask;
|
|
7122
|
+
if (alphabet[byte]) {
|
|
7123
|
+
id += alphabet[byte];
|
|
7124
|
+
if (id.length === size) return id;
|
|
7175
7125
|
}
|
|
7176
7126
|
}
|
|
7177
|
-
return { ok: true };
|
|
7178
7127
|
}
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7128
|
+
}
|
|
7129
|
+
function encodeUlidTime(now2, len = 10) {
|
|
7130
|
+
let str = "";
|
|
7131
|
+
let time = Math.floor(now2);
|
|
7132
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
7133
|
+
const mod = time % 32;
|
|
7134
|
+
str = CROCKFORD_BASE32[mod] + str;
|
|
7135
|
+
time = (time - mod) / 32;
|
|
7136
|
+
}
|
|
7137
|
+
return str;
|
|
7138
|
+
}
|
|
7139
|
+
function encodeUlidRandom(bytes) {
|
|
7140
|
+
let str = "";
|
|
7141
|
+
let bitBuffer = 0;
|
|
7142
|
+
let bitsInBuffer = 0;
|
|
7143
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
7144
|
+
bitBuffer = bitBuffer << 8 | bytes[i];
|
|
7145
|
+
bitsInBuffer += 8;
|
|
7146
|
+
while (bitsInBuffer >= 5) {
|
|
7147
|
+
bitsInBuffer -= 5;
|
|
7148
|
+
const index = bitBuffer >> bitsInBuffer & 31;
|
|
7149
|
+
str += CROCKFORD_BASE32[index];
|
|
7184
7150
|
}
|
|
7185
|
-
return config.adminAuth?.providers?.find((p) => p.members) || null;
|
|
7186
7151
|
}
|
|
7187
|
-
|
|
7188
|
-
|
|
7152
|
+
return str;
|
|
7153
|
+
}
|
|
7154
|
+
function ulid(seedTime = Date.now()) {
|
|
7155
|
+
const timePart = encodeUlidTime(seedTime);
|
|
7156
|
+
const randPart = encodeUlidRandom((0, import_node_crypto2.randomBytes)(10));
|
|
7157
|
+
return timePart + randPart;
|
|
7158
|
+
}
|
|
7159
|
+
function uuid() {
|
|
7160
|
+
return (0, import_node_crypto2.randomUUID)();
|
|
7161
|
+
}
|
|
7162
|
+
function resolvePrefix(slug, explicitPrefix) {
|
|
7163
|
+
if (explicitPrefix && typeof explicitPrefix === "string" && explicitPrefix.trim().length > 0) {
|
|
7164
|
+
const cleaned = explicitPrefix.trim().toLowerCase().replace(/[^a-z0-9_]/g, "");
|
|
7165
|
+
if (cleaned.length > 0) return cleaned;
|
|
7189
7166
|
}
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
resetPasswordExpires: _rpe,
|
|
7198
|
-
loginAttempts: _la,
|
|
7199
|
-
lockUntil: _lu,
|
|
7200
|
-
...safeDoc
|
|
7201
|
-
} = doc;
|
|
7202
|
-
return safeDoc;
|
|
7203
|
-
}
|
|
7204
|
-
return doc;
|
|
7167
|
+
if (!slug || typeof slug !== "string") return "col";
|
|
7168
|
+
const s = slug.trim().toLowerCase();
|
|
7169
|
+
if (COMMON_SLUG_PREFIXES[s]) return COMMON_SLUG_PREFIXES[s];
|
|
7170
|
+
const pieces = s.split(/[-_]/).filter(Boolean);
|
|
7171
|
+
if (pieces.length > 1 && pieces[0].length >= 2) {
|
|
7172
|
+
const first2 = pieces[0].replace(/[^a-z0-9]/g, "");
|
|
7173
|
+
if (first2.length >= 2) return first2.slice(0, 4);
|
|
7205
7174
|
}
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
doc: options.doc ?? void 0,
|
|
7218
|
-
data: options.data
|
|
7219
|
-
}
|
|
7220
|
-
);
|
|
7175
|
+
const cleanWord = s.replace(/[^a-z0-9]/g, "");
|
|
7176
|
+
if (cleanWord.length <= 4) return cleanWord || "col";
|
|
7177
|
+
return cleanWord.slice(0, 3) || "col";
|
|
7178
|
+
}
|
|
7179
|
+
function prefixedId(prefix = "col", size = 16) {
|
|
7180
|
+
const cleanPrefix = (prefix || "col").toLowerCase().replace(/[^a-z0-9_]/g, "") || "col";
|
|
7181
|
+
return `${cleanPrefix}_${nanoid(size, BASE62_ALPHABET)}`;
|
|
7182
|
+
}
|
|
7183
|
+
function generateId(options) {
|
|
7184
|
+
if (typeof options === "string") {
|
|
7185
|
+
return prefixedId(resolvePrefix(options));
|
|
7221
7186
|
}
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7187
|
+
const opts = options || {};
|
|
7188
|
+
const type = opts.type || "prefixed-nanoid";
|
|
7189
|
+
switch (type) {
|
|
7190
|
+
case "uuid":
|
|
7191
|
+
return uuid();
|
|
7192
|
+
case "ulid":
|
|
7193
|
+
return ulid();
|
|
7194
|
+
case "nanoid":
|
|
7195
|
+
return nanoid(opts.size || 21);
|
|
7196
|
+
case "prefixed-nanoid":
|
|
7197
|
+
default:
|
|
7198
|
+
return prefixedId(opts.prefix || "col", opts.size || 16);
|
|
7199
|
+
}
|
|
7200
|
+
}
|
|
7201
|
+
function generateDocumentId(collectionOrSlug) {
|
|
7202
|
+
if (!collectionOrSlug) {
|
|
7203
|
+
return prefixedId("col");
|
|
7204
|
+
}
|
|
7205
|
+
if (typeof collectionOrSlug === "string") {
|
|
7206
|
+
const prefix2 = resolvePrefix(collectionOrSlug);
|
|
7207
|
+
return prefixedId(prefix2);
|
|
7208
|
+
}
|
|
7209
|
+
if (typeof collectionOrSlug.idGenerator === "function") {
|
|
7210
|
+
return collectionOrSlug.idGenerator(collectionOrSlug.slug);
|
|
7211
|
+
}
|
|
7212
|
+
const type = collectionOrSlug.idType || "prefixed-nanoid";
|
|
7213
|
+
const prefix = resolvePrefix(collectionOrSlug.slug, collectionOrSlug.idPrefix);
|
|
7214
|
+
return generateId({ prefix, type });
|
|
7215
|
+
}
|
|
7216
|
+
|
|
7217
|
+
// src/workflows.ts
|
|
7218
|
+
var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
|
|
7219
|
+
var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
|
|
7220
|
+
function simplePublishingWorkflow() {
|
|
7221
|
+
return {
|
|
7222
|
+
initialState: "draft",
|
|
7223
|
+
draftState: "draft",
|
|
7224
|
+
states: [
|
|
7225
|
+
{ name: "draft", label: "Draft", color: "neutral" },
|
|
7226
|
+
{ name: "published", label: "Published", color: "success", published: true }
|
|
7227
|
+
],
|
|
7228
|
+
transitions: [
|
|
7229
|
+
{ name: "publish", label: "Publish", from: ["draft", "published"], to: "published" },
|
|
7230
|
+
{ name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
|
|
7231
|
+
]
|
|
7232
|
+
// Intentionally no `roles`: this lightweight workflow (synthesized from
|
|
7233
|
+
// `drafts: true`) does no capability-based gating. Draft visibility for a
|
|
7234
|
+
// no-roles workflow is handled in `canViewWorkflowDraft`.
|
|
7235
|
+
};
|
|
7236
|
+
}
|
|
7237
|
+
function publicMetadata(meta) {
|
|
7238
|
+
const { availableTransitions: _availableTransitions, ...safe } = meta;
|
|
7239
|
+
return safe;
|
|
7240
|
+
}
|
|
7241
|
+
function workflowCapabilities(workflow, user) {
|
|
7242
|
+
const capabilities = new Set(Array.isArray(user?.capabilities) ? user.capabilities : []);
|
|
7243
|
+
const roles = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
|
|
7244
|
+
for (const mapping of workflow.roles ?? []) {
|
|
7245
|
+
if (roles.includes(mapping.role)) mapping.capabilities.forEach((capability) => capabilities.add(capability));
|
|
7246
|
+
}
|
|
7247
|
+
return capabilities;
|
|
7248
|
+
}
|
|
7249
|
+
function canViewWorkflowDraft(workflow, user) {
|
|
7250
|
+
if (!user) return false;
|
|
7251
|
+
const capabilities = workflowCapabilities(workflow, user);
|
|
7252
|
+
if (capabilities.has("entry.edit")) return true;
|
|
7253
|
+
if (workflow.transitions.some(
|
|
7254
|
+
(transition) => (transition.requiredCapabilities ?? []).some((capability) => capabilities.has(capability))
|
|
7255
|
+
)) {
|
|
7256
|
+
return true;
|
|
7257
|
+
}
|
|
7258
|
+
if (!workflow.roles || workflow.roles.length === 0) return true;
|
|
7259
|
+
return false;
|
|
7260
|
+
}
|
|
7261
|
+
function availableWorkflowTransitions(workflow, state, user) {
|
|
7262
|
+
const capabilities = workflowCapabilities(workflow, user);
|
|
7263
|
+
return workflow.transitions.filter((transition) => {
|
|
7264
|
+
const from = Array.isArray(transition.from) ? transition.from : [transition.from];
|
|
7265
|
+
return from.includes(state) && (transition.requiredCapabilities ?? []).every((item) => capabilities.has(item));
|
|
7266
|
+
});
|
|
7267
|
+
}
|
|
7268
|
+
function initializeWorkflowDocument(data, workflow) {
|
|
7269
|
+
return {
|
|
7270
|
+
...data,
|
|
7271
|
+
__workflow: { state: workflow.initialState, revision: 1 }
|
|
7272
|
+
};
|
|
7273
|
+
}
|
|
7274
|
+
function publishedStateName(workflow) {
|
|
7275
|
+
return workflow.states.find((state) => state.published)?.name ?? workflow.states[workflow.states.length - 1]?.name ?? workflow.initialState;
|
|
7276
|
+
}
|
|
7277
|
+
function materializeWorkflowDocument(doc, workflow, user) {
|
|
7278
|
+
const meta = doc.__workflow;
|
|
7279
|
+
const publishedState = publishedStateName(workflow);
|
|
7280
|
+
if (!meta) {
|
|
7281
|
+
const { __published: _legacyPublished, __workflow: _legacyWorkflow, ...working2 } = doc;
|
|
7282
|
+
const state = publishedStateName(workflow);
|
|
7283
|
+
return {
|
|
7284
|
+
...working2,
|
|
7285
|
+
_workflow: {
|
|
7286
|
+
state,
|
|
7287
|
+
revision: 1,
|
|
7288
|
+
availableTransitions: availableWorkflowTransitions(workflow, state, user).map((item) => item.name)
|
|
7251
7289
|
}
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
const mappedDocs = [];
|
|
7260
|
-
for (const m of paginatedResult.docs) {
|
|
7261
|
-
let localId = m.id;
|
|
7262
|
-
const localDoc = await db.find({
|
|
7263
|
-
collection: this.collection.slug,
|
|
7264
|
-
where: m.id ? { externalSubject: { equals: m.id } } : { email: { equals: m.email } },
|
|
7265
|
-
limit: 1
|
|
7266
|
-
});
|
|
7267
|
-
if (localDoc.docs[0]) {
|
|
7268
|
-
localId = localDoc.docs[0].id;
|
|
7269
|
-
}
|
|
7270
|
-
mappedDocs.push({
|
|
7271
|
-
...m,
|
|
7272
|
-
id: localId,
|
|
7273
|
-
externalSubject: m.id
|
|
7274
|
-
});
|
|
7290
|
+
};
|
|
7291
|
+
}
|
|
7292
|
+
const { __published, __workflow, ...working } = doc;
|
|
7293
|
+
if (!canViewWorkflowDraft(workflow, user)) {
|
|
7294
|
+
if (!__published || typeof __published !== "object") {
|
|
7295
|
+
if (meta.state === publishedState) {
|
|
7296
|
+
return { ...working, _workflow: publicMetadata(meta) };
|
|
7275
7297
|
}
|
|
7276
|
-
return
|
|
7277
|
-
...paginatedResult,
|
|
7278
|
-
docs: mappedDocs
|
|
7279
|
-
});
|
|
7298
|
+
return null;
|
|
7280
7299
|
}
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
let where = void 0;
|
|
7289
|
-
const whereRaw = c.req.query("where");
|
|
7290
|
-
if (whereRaw) {
|
|
7291
|
-
try {
|
|
7292
|
-
where = JSON.parse(decodeURIComponent(whereRaw));
|
|
7293
|
-
} catch {
|
|
7294
|
-
}
|
|
7300
|
+
return { id: doc.id, ...__published, _workflow: publicMetadata(meta) };
|
|
7301
|
+
}
|
|
7302
|
+
return {
|
|
7303
|
+
...working,
|
|
7304
|
+
_workflow: {
|
|
7305
|
+
...meta,
|
|
7306
|
+
availableTransitions: availableWorkflowTransitions(workflow, meta.state, user).map((item) => item.name)
|
|
7295
7307
|
}
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7308
|
+
};
|
|
7309
|
+
}
|
|
7310
|
+
function eventId() {
|
|
7311
|
+
return globalThis.crypto?.randomUUID?.() ?? `evt_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
7312
|
+
}
|
|
7313
|
+
function createLifecycleEvent(args) {
|
|
7314
|
+
return {
|
|
7315
|
+
id: eventId(),
|
|
7316
|
+
name: args.name,
|
|
7317
|
+
collection: args.collection,
|
|
7318
|
+
documentId: args.documentId,
|
|
7319
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7320
|
+
actorId: args.actorId,
|
|
7321
|
+
payload: args.payload,
|
|
7322
|
+
attempts: 0,
|
|
7323
|
+
status: "pending"
|
|
7324
|
+
};
|
|
7325
|
+
}
|
|
7326
|
+
async function persistEvent(db, event) {
|
|
7327
|
+
await db.create({ collection: LIFECYCLE_EVENTS_COLLECTION, data: event });
|
|
7328
|
+
}
|
|
7329
|
+
async function dispatchLifecycleEvent(config, event) {
|
|
7330
|
+
const db = config.db;
|
|
7331
|
+
if (!db || !config.events?.handlers.length) return;
|
|
7332
|
+
const maxAttempts = config.events.maxAttempts ?? 8;
|
|
7333
|
+
const retryDelayMs = config.events.retryDelayMs ?? 1e3;
|
|
7334
|
+
const attempts = event.attempts + 1;
|
|
7335
|
+
try {
|
|
7336
|
+
await db.update({
|
|
7337
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
7338
|
+
id: event.id,
|
|
7339
|
+
data: { status: "processing", attempts }
|
|
7340
|
+
});
|
|
7341
|
+
for (const handler of config.events.handlers) await handler({ ...event, status: "processing", attempts });
|
|
7342
|
+
await db.update({
|
|
7343
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
7344
|
+
id: event.id,
|
|
7345
|
+
data: { status: "delivered", attempts, deliveredAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null }
|
|
7346
|
+
});
|
|
7347
|
+
} catch (error) {
|
|
7348
|
+
const exhausted = attempts >= maxAttempts;
|
|
7349
|
+
await db.update({
|
|
7350
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
7351
|
+
id: event.id,
|
|
7352
|
+
data: {
|
|
7353
|
+
status: "failed",
|
|
7354
|
+
attempts,
|
|
7355
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
7356
|
+
nextAttemptAt: exhausted ? null : new Date(Date.now() + retryDelayMs * 2 ** (attempts - 1)).toISOString()
|
|
7305
7357
|
}
|
|
7306
|
-
}
|
|
7307
|
-
const searchWhere = await buildCollectionSearchWhere({
|
|
7308
|
-
collection: this.collection,
|
|
7309
|
-
search,
|
|
7310
|
-
db: readonlyDb,
|
|
7311
|
-
collections: config.collections
|
|
7312
7358
|
});
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
}
|
|
7332
|
-
if (this.collection.workflow && !canViewWorkflowDraft(this.collection.workflow, user)) {
|
|
7333
|
-
const publicWorkflowState = publishedStateName(this.collection.workflow);
|
|
7334
|
-
where = where ? {
|
|
7335
|
-
AND: [
|
|
7336
|
-
where,
|
|
7337
|
-
{
|
|
7338
|
-
OR: [
|
|
7339
|
-
{ __published: { exists: true } },
|
|
7340
|
-
{ __workflow: { exists: false } },
|
|
7341
|
-
{ "__workflow.state": { equals: publicWorkflowState } }
|
|
7342
|
-
]
|
|
7343
|
-
}
|
|
7344
|
-
]
|
|
7345
|
-
} : {
|
|
7346
|
-
OR: [
|
|
7347
|
-
{ __published: { exists: true } },
|
|
7348
|
-
{ __workflow: { exists: false } },
|
|
7349
|
-
{ "__workflow.state": { equals: publicWorkflowState } }
|
|
7350
|
-
]
|
|
7351
|
-
};
|
|
7352
|
-
}
|
|
7353
|
-
const access = await this.evaluateAccess(c, "read");
|
|
7354
|
-
if (!access.allowed) {
|
|
7355
|
-
return c.json(
|
|
7356
|
-
{
|
|
7357
|
-
error: true,
|
|
7358
|
-
message: `Access denied: read on ${this.collection.slug}`
|
|
7359
|
-
},
|
|
7360
|
-
403
|
|
7361
|
-
);
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
async function saveWorkflowDraft(args) {
|
|
7362
|
+
const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
|
|
7363
|
+
const db = config.db;
|
|
7364
|
+
const workflow = collection.workflow;
|
|
7365
|
+
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
7366
|
+
const previous = originalDoc.__workflow;
|
|
7367
|
+
const isLegacyPublishedDoc = !previous;
|
|
7368
|
+
const baselineRevision = previous?.revision ?? 1;
|
|
7369
|
+
const event = createLifecycleEvent({
|
|
7370
|
+
name: "revision.created",
|
|
7371
|
+
collection: collection.slug,
|
|
7372
|
+
documentId: id,
|
|
7373
|
+
actorId: user?.sub,
|
|
7374
|
+
payload: {
|
|
7375
|
+
revision: baselineRevision + 1,
|
|
7376
|
+
previousRevision: isLegacyPublishedDoc ? baselineRevision : previous?.revision ?? null
|
|
7362
7377
|
}
|
|
7363
|
-
|
|
7364
|
-
|
|
7378
|
+
});
|
|
7379
|
+
const doc = await db.transaction(async (tx) => {
|
|
7380
|
+
const nextMeta = {
|
|
7381
|
+
...previous ?? {},
|
|
7382
|
+
state: isLegacyPublishedDoc ? workflow.draftState ?? workflow.initialState : originalDoc.__published ? workflow.draftState ?? workflow.initialState : previous.state,
|
|
7383
|
+
revision: baselineRevision + 1,
|
|
7384
|
+
...isLegacyPublishedDoc ? { publishedRevision: baselineRevision } : {}
|
|
7385
|
+
};
|
|
7386
|
+
const updateData = {
|
|
7387
|
+
...data,
|
|
7388
|
+
__workflow: nextMeta
|
|
7389
|
+
};
|
|
7390
|
+
if (isLegacyPublishedDoc) {
|
|
7391
|
+
const { __workflow: _legacyWorkflow, __published: _legacyPublished, ...publishedSnapshot } = originalDoc;
|
|
7392
|
+
updateData.__published = publishedSnapshot;
|
|
7365
7393
|
}
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7394
|
+
const updated = await tx.update({ collection: collection.slug, id, data: updateData });
|
|
7395
|
+
await persistEvent(tx, event);
|
|
7396
|
+
await beforeCommit?.(tx, updated);
|
|
7397
|
+
return updated;
|
|
7398
|
+
});
|
|
7399
|
+
void dispatchLifecycleEvent(config, event);
|
|
7400
|
+
return { doc, event };
|
|
7401
|
+
}
|
|
7402
|
+
async function createWorkflowDocument(args) {
|
|
7403
|
+
const { config, collection, data, user, beforeCommit } = args;
|
|
7404
|
+
const db = config.db;
|
|
7405
|
+
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
7406
|
+
let event;
|
|
7407
|
+
const doc = await db.transaction(async (tx) => {
|
|
7408
|
+
const created = await tx.create({ collection: collection.slug, data });
|
|
7409
|
+
event = createLifecycleEvent({
|
|
7410
|
+
name: "revision.created",
|
|
7411
|
+
collection: collection.slug,
|
|
7412
|
+
documentId: created.id,
|
|
7413
|
+
actorId: user?.sub,
|
|
7414
|
+
payload: { revision: 1, previousRevision: null }
|
|
7373
7415
|
});
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7416
|
+
await persistEvent(tx, event);
|
|
7417
|
+
await beforeCommit?.(tx, created);
|
|
7418
|
+
return created;
|
|
7419
|
+
});
|
|
7420
|
+
void dispatchLifecycleEvent(config, event);
|
|
7421
|
+
return { doc, event };
|
|
7422
|
+
}
|
|
7423
|
+
async function transitionWorkflow(args) {
|
|
7424
|
+
const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
|
|
7425
|
+
const db = config.db;
|
|
7426
|
+
const workflow = collection.workflow;
|
|
7427
|
+
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
7428
|
+
const original = await db.findOne({ collection: collection.slug, id });
|
|
7429
|
+
if (!original) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
|
|
7430
|
+
const meta = original.__workflow;
|
|
7431
|
+
if (!meta) throw Object.assign(new Error("Entry has no workflow metadata"), { statusCode: 409 });
|
|
7432
|
+
if (expectedRevision !== void 0 && expectedRevision !== meta.revision) {
|
|
7433
|
+
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
|
|
7434
|
+
statusCode: 409
|
|
7435
|
+
});
|
|
7436
|
+
}
|
|
7437
|
+
const transition = workflow.transitions.find((item) => item.name === transitionName);
|
|
7438
|
+
const fromStates = transition ? Array.isArray(transition.from) ? transition.from : [transition.from] : [];
|
|
7439
|
+
if (!transition || !fromStates.includes(meta.state)) {
|
|
7440
|
+
throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), {
|
|
7441
|
+
statusCode: 409
|
|
7442
|
+
});
|
|
7443
|
+
}
|
|
7444
|
+
if (!availableWorkflowTransitions(workflow, meta.state, user).some((item) => item.name === transition.name)) {
|
|
7445
|
+
throw Object.assign(new Error(`You do not have permission to perform "${transition.label}".`), { statusCode: 403 });
|
|
7446
|
+
}
|
|
7447
|
+
if (transition.requireComment && !comment?.trim()) {
|
|
7448
|
+
throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
|
|
7449
|
+
}
|
|
7450
|
+
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, input, req, db };
|
|
7451
|
+
for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
|
|
7452
|
+
const events = [
|
|
7453
|
+
createLifecycleEvent({
|
|
7454
|
+
name: "workflow.transitioned",
|
|
7455
|
+
collection: collection.slug,
|
|
7456
|
+
documentId: id,
|
|
7457
|
+
actorId: user?.sub,
|
|
7458
|
+
payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
|
|
7459
|
+
})
|
|
7460
|
+
];
|
|
7461
|
+
const targetState = workflow.states.find((state) => state.name === transition.to);
|
|
7462
|
+
if (targetState.published) {
|
|
7463
|
+
events.push(
|
|
7464
|
+
createLifecycleEvent({
|
|
7465
|
+
name: "entry.published",
|
|
7466
|
+
collection: collection.slug,
|
|
7467
|
+
documentId: id,
|
|
7468
|
+
actorId: user?.sub,
|
|
7469
|
+
payload: { revision: meta.revision }
|
|
7470
|
+
})
|
|
7471
|
+
);
|
|
7472
|
+
} else if (transition.unpublish) {
|
|
7473
|
+
events.push(
|
|
7474
|
+
createLifecycleEvent({
|
|
7475
|
+
name: "entry.unpublished",
|
|
7476
|
+
collection: collection.slug,
|
|
7477
|
+
documentId: id,
|
|
7478
|
+
actorId: user?.sub,
|
|
7479
|
+
payload: { revision: meta.publishedRevision }
|
|
7480
|
+
})
|
|
7481
|
+
);
|
|
7482
|
+
}
|
|
7483
|
+
const updated = await db.transaction(async (tx) => {
|
|
7484
|
+
const locked = await tx.findOne({ collection: collection.slug, id });
|
|
7485
|
+
if (!locked) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
|
|
7486
|
+
const lockedMeta = locked.__workflow;
|
|
7487
|
+
if (lockedMeta.revision !== meta.revision || lockedMeta.state !== meta.state || expectedRevision !== void 0 && lockedMeta.revision !== expectedRevision) {
|
|
7488
|
+
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
|
|
7489
|
+
statusCode: 409
|
|
7378
7490
|
});
|
|
7379
|
-
|
|
7380
|
-
|
|
7491
|
+
}
|
|
7492
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
7493
|
+
const { __published: _published, __workflow: _workflow, id: _id, ...working } = locked;
|
|
7494
|
+
const nextMeta = {
|
|
7495
|
+
...lockedMeta,
|
|
7496
|
+
state: transition.to,
|
|
7497
|
+
...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now2, publishedBy: user?.sub } : {},
|
|
7498
|
+
...transition.unpublish ? { publishedRevision: void 0, publishedAt: void 0, publishedBy: void 0 } : {}
|
|
7499
|
+
};
|
|
7500
|
+
const data = { __workflow: nextMeta };
|
|
7501
|
+
if (targetState.published) data.__published = working;
|
|
7502
|
+
if (transition.unpublish) data.__published = null;
|
|
7503
|
+
const next = await tx.update({ collection: collection.slug, id, data });
|
|
7504
|
+
if (transition.onTransition) {
|
|
7505
|
+
await transition.onTransition({
|
|
7506
|
+
transition,
|
|
7507
|
+
from: lockedMeta.state,
|
|
7508
|
+
to: transition.to,
|
|
7509
|
+
doc: locked,
|
|
7510
|
+
input,
|
|
7511
|
+
comment,
|
|
7512
|
+
user,
|
|
7513
|
+
req,
|
|
7514
|
+
tx
|
|
7515
|
+
});
|
|
7516
|
+
}
|
|
7517
|
+
await tx.create({
|
|
7518
|
+
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
7519
|
+
data: {
|
|
7520
|
+
collection: collection.slug,
|
|
7521
|
+
documentId: id,
|
|
7522
|
+
transition: transition.name,
|
|
7523
|
+
from: lockedMeta.state,
|
|
7524
|
+
to: transition.to,
|
|
7525
|
+
revision: lockedMeta.revision,
|
|
7526
|
+
comment: comment ?? null,
|
|
7527
|
+
actorId: user?.sub ?? null,
|
|
7528
|
+
createdAt: now2
|
|
7381
7529
|
}
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7530
|
+
});
|
|
7531
|
+
for (const event of events) await persistEvent(tx, event);
|
|
7532
|
+
if (collection.audit) {
|
|
7533
|
+
await tx.create({
|
|
7534
|
+
collection: "__audit",
|
|
7535
|
+
data: {
|
|
7536
|
+
collection: collection.slug,
|
|
7537
|
+
documentId: id,
|
|
7538
|
+
operation: "workflow.transition",
|
|
7539
|
+
user: user?.sub ?? null,
|
|
7540
|
+
timestamp: now2,
|
|
7541
|
+
changes: JSON.stringify({
|
|
7542
|
+
transition: transition.name,
|
|
7543
|
+
from: lockedMeta.state,
|
|
7544
|
+
to: transition.to,
|
|
7545
|
+
revision: lockedMeta.revision
|
|
7546
|
+
})
|
|
7547
|
+
}
|
|
7389
7548
|
});
|
|
7390
7549
|
}
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7550
|
+
return next;
|
|
7551
|
+
});
|
|
7552
|
+
for (const event of events) void dispatchLifecycleEvent(config, event);
|
|
7553
|
+
for (const hook of workflow.hooks?.afterTransition ?? []) {
|
|
7554
|
+
try {
|
|
7555
|
+
await hook({ ...hookContext, doc: updated, event: events[0] });
|
|
7556
|
+
} catch (error) {
|
|
7557
|
+
try {
|
|
7558
|
+
const obs = config?.__observability;
|
|
7559
|
+
obs?.recordWorkflowHookFailure?.({
|
|
7560
|
+
collection: collection.slug,
|
|
7561
|
+
transition: transition.name
|
|
7562
|
+
});
|
|
7563
|
+
const logger = config?.logger;
|
|
7564
|
+
if (logger && typeof logger.child === "function") {
|
|
7565
|
+
logger.child({ component: "workflow" }).error({
|
|
7566
|
+
err: error,
|
|
7567
|
+
msg: "afterTransition hook failed",
|
|
7568
|
+
collection: collection.slug,
|
|
7569
|
+
transition: transition.name,
|
|
7570
|
+
documentId: id
|
|
7571
|
+
});
|
|
7572
|
+
} else if (logger && typeof logger.error === "function") {
|
|
7573
|
+
logger.error({
|
|
7574
|
+
err: error,
|
|
7575
|
+
msg: "afterTransition hook failed",
|
|
7576
|
+
collection: collection.slug,
|
|
7577
|
+
transition: transition.name,
|
|
7578
|
+
documentId: id
|
|
7579
|
+
});
|
|
7580
|
+
} else {
|
|
7581
|
+
console.error("afterTransition hook failed:", error);
|
|
7411
7582
|
}
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
docWithCollectionHooks,
|
|
7416
|
-
user,
|
|
7417
|
-
readonlyDb
|
|
7418
|
-
);
|
|
7419
|
-
const docWithFieldAccess = await applyFieldReadAccess(
|
|
7420
|
-
{
|
|
7421
|
-
config,
|
|
7422
|
-
fields: this.collection.fields,
|
|
7423
|
-
user,
|
|
7424
|
-
req: this.toHookRequestContext(c),
|
|
7425
|
-
doc: docWithFieldHooks
|
|
7426
|
-
},
|
|
7427
|
-
docWithFieldHooks
|
|
7428
|
-
);
|
|
7429
|
-
processedDocs.push(docWithFieldAccess);
|
|
7430
|
-
}
|
|
7431
|
-
result.docs = processedDocs;
|
|
7432
|
-
if (depth > 0) {
|
|
7433
|
-
const populationService = new PopulationService(db, config.collections);
|
|
7434
|
-
result = await populationService.populateResult(
|
|
7435
|
-
result,
|
|
7436
|
-
this.collection.fields,
|
|
7437
|
-
depth
|
|
7438
|
-
);
|
|
7439
|
-
}
|
|
7440
|
-
if (this.collection.auth && result.docs && Array.isArray(result.docs)) {
|
|
7441
|
-
result.docs = result.docs.map((d) => this.sanitizeDoc(d));
|
|
7583
|
+
} catch {
|
|
7584
|
+
console.error("afterTransition hook failed:", error);
|
|
7585
|
+
}
|
|
7442
7586
|
}
|
|
7443
|
-
return c.json(result);
|
|
7444
7587
|
}
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7588
|
+
return updated;
|
|
7589
|
+
}
|
|
7590
|
+
|
|
7591
|
+
// src/detail.ts
|
|
7592
|
+
function normalizeDetailItem(item) {
|
|
7593
|
+
if (typeof item === "string") {
|
|
7594
|
+
return {
|
|
7595
|
+
type: "field",
|
|
7596
|
+
field: item
|
|
7597
|
+
};
|
|
7598
|
+
}
|
|
7599
|
+
return item;
|
|
7600
|
+
}
|
|
7601
|
+
async function evaluateDetailComputed(detail, doc, user, db) {
|
|
7602
|
+
if (!doc || !detail || !Array.isArray(detail)) {
|
|
7603
|
+
return doc;
|
|
7604
|
+
}
|
|
7605
|
+
const computedItems = [];
|
|
7606
|
+
const extractComputed = (items) => {
|
|
7607
|
+
for (const rawItem of items) {
|
|
7608
|
+
if (!rawItem) continue;
|
|
7609
|
+
const item = normalizeDetailItem(rawItem);
|
|
7610
|
+
if (item.type === "computed") {
|
|
7611
|
+
const id = item.id || item.label?.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
7612
|
+
if (id) {
|
|
7613
|
+
computedItems.push({
|
|
7614
|
+
id,
|
|
7615
|
+
label: item.label,
|
|
7616
|
+
expression: item.expression,
|
|
7617
|
+
handler: item.handler
|
|
7471
7618
|
});
|
|
7472
7619
|
}
|
|
7473
|
-
} else if (
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
externalSubject: matched.id
|
|
7481
|
-
});
|
|
7620
|
+
} else if (item.type === "section" || item.type === "grid" || item.type === "repeat") {
|
|
7621
|
+
if (item.items && Array.isArray(item.items)) {
|
|
7622
|
+
extractComputed(item.items);
|
|
7623
|
+
}
|
|
7624
|
+
} else if (item.type === "tabs" && item.tabs && Array.isArray(item.tabs)) {
|
|
7625
|
+
for (const tab of item.tabs) {
|
|
7626
|
+
if (tab.items) extractComputed(tab.items);
|
|
7482
7627
|
}
|
|
7483
7628
|
}
|
|
7484
7629
|
}
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
}
|
|
7496
|
-
let effectiveDoc = doc;
|
|
7497
|
-
if (this.collection.workflow) {
|
|
7498
|
-
effectiveDoc = materializeWorkflowDocument(
|
|
7499
|
-
doc,
|
|
7500
|
-
this.collection.workflow,
|
|
7501
|
-
user
|
|
7502
|
-
);
|
|
7503
|
-
if (!effectiveDoc) return c.json({ message: "Document not found" }, 404);
|
|
7504
|
-
}
|
|
7505
|
-
const access = await this.evaluateAccess(c, "read", { id, doc: effectiveDoc });
|
|
7506
|
-
if (!access.allowed) {
|
|
7507
|
-
return c.json(
|
|
7508
|
-
{
|
|
7509
|
-
error: true,
|
|
7510
|
-
message: `Access denied: read on ${this.collection.slug}`
|
|
7511
|
-
},
|
|
7512
|
-
403
|
|
7513
|
-
);
|
|
7514
|
-
}
|
|
7515
|
-
const docWithDefaults = DefaultsService.apply(
|
|
7516
|
-
this.collection.fields,
|
|
7517
|
-
effectiveDoc
|
|
7518
|
-
);
|
|
7519
|
-
const readonlyDb = createReadonlyDb(db);
|
|
7520
|
-
const docWithCollectionHooks = await runCollectionHooks(
|
|
7521
|
-
this.collection.hooks?.afterRead,
|
|
7522
|
-
{
|
|
7523
|
-
doc: docWithDefaults,
|
|
7524
|
-
req: c.req,
|
|
7525
|
-
user,
|
|
7526
|
-
db: readonlyDb
|
|
7630
|
+
};
|
|
7631
|
+
extractComputed(detail);
|
|
7632
|
+
if (computedItems.length === 0) return doc;
|
|
7633
|
+
const evaluated = {};
|
|
7634
|
+
for (const item of computedItems) {
|
|
7635
|
+
try {
|
|
7636
|
+
if (typeof item.handler === "function") {
|
|
7637
|
+
evaluated[item.id] = await item.handler({ doc, user, db });
|
|
7638
|
+
} else if (item.expression) {
|
|
7639
|
+
evaluated[item.id] = await evaluateJexl(item.expression, { doc, user });
|
|
7527
7640
|
}
|
|
7528
|
-
)
|
|
7529
|
-
|
|
7530
|
-
this.collection.fields,
|
|
7531
|
-
docWithCollectionHooks,
|
|
7532
|
-
user,
|
|
7533
|
-
readonlyDb
|
|
7534
|
-
);
|
|
7535
|
-
const docWithFieldAccess = await applyFieldReadAccess(
|
|
7536
|
-
{
|
|
7537
|
-
config,
|
|
7538
|
-
fields: this.collection.fields,
|
|
7539
|
-
user,
|
|
7540
|
-
req: this.toHookRequestContext(c),
|
|
7541
|
-
doc: docWithFieldHooks
|
|
7542
|
-
},
|
|
7543
|
-
docWithFieldHooks
|
|
7544
|
-
);
|
|
7545
|
-
let finalDoc = docWithFieldAccess;
|
|
7546
|
-
if (depth > 0 && docWithFieldAccess) {
|
|
7547
|
-
const populationService = new PopulationService(db, config.collections);
|
|
7548
|
-
finalDoc = await populationService.populate({
|
|
7549
|
-
data: docWithFieldAccess,
|
|
7550
|
-
fields: this.collection.fields,
|
|
7551
|
-
currentDepth: 0,
|
|
7552
|
-
maxDepth: depth
|
|
7553
|
-
});
|
|
7641
|
+
} catch (_err) {
|
|
7642
|
+
evaluated[item.id] = null;
|
|
7554
7643
|
}
|
|
7555
|
-
|
|
7556
|
-
|
|
7644
|
+
}
|
|
7645
|
+
return {
|
|
7646
|
+
...doc,
|
|
7647
|
+
_meta: {
|
|
7648
|
+
...doc._meta || {},
|
|
7649
|
+
computed: evaluated
|
|
7557
7650
|
}
|
|
7558
|
-
|
|
7651
|
+
};
|
|
7652
|
+
}
|
|
7653
|
+
|
|
7654
|
+
// src/controllers/collection.controller.ts
|
|
7655
|
+
init_rag_service();
|
|
7656
|
+
var import_http_exception2 = require("hono/http-exception");
|
|
7657
|
+
|
|
7658
|
+
// src/utils/tenant.ts
|
|
7659
|
+
var import_http_exception = require("hono/http-exception");
|
|
7660
|
+
function getAllowedSitesForUser(user, payload) {
|
|
7661
|
+
const raw = user?.allowedSites ?? user?.siteIds ?? user?.siteAccess ?? (user?.siteId ? [user.siteId] : void 0) ?? payload?.allowedSites ?? payload?.siteIds ?? payload?.siteAccess ?? (payload?.siteId ? [payload.siteId] : void 0);
|
|
7662
|
+
if (Array.isArray(raw)) {
|
|
7663
|
+
return raw.filter((s) => typeof s === "string" && s.trim().length > 0);
|
|
7559
7664
|
}
|
|
7560
|
-
|
|
7561
|
-
|
|
7665
|
+
return void 0;
|
|
7666
|
+
}
|
|
7667
|
+
function isSiteAuthorized(user, payload, requestedSiteId) {
|
|
7668
|
+
const allowedSites = getAllowedSitesForUser(user, payload);
|
|
7669
|
+
if (!allowedSites || allowedSites.length === 0) {
|
|
7670
|
+
return true;
|
|
7671
|
+
}
|
|
7672
|
+
if (!requestedSiteId) {
|
|
7673
|
+
return false;
|
|
7674
|
+
}
|
|
7675
|
+
return allowedSites.includes(requestedSiteId);
|
|
7676
|
+
}
|
|
7677
|
+
function resolveAuthorizedSiteId(c) {
|
|
7678
|
+
const user = typeof c.get === "function" ? c.get("user") : void 0;
|
|
7679
|
+
const tokenPayload = typeof c.get === "function" ? c.get("authTokenPayload") : void 0;
|
|
7680
|
+
const requestedSiteId = (typeof c.req?.header === "function" ? c.req.header("X-Site-Id") : void 0) || (typeof c.get === "function" ? c.get("siteId") : void 0);
|
|
7681
|
+
const allowedSites = getAllowedSitesForUser(user, tokenPayload);
|
|
7682
|
+
if (Array.isArray(allowedSites) && allowedSites.length > 0) {
|
|
7683
|
+
if (!requestedSiteId) {
|
|
7684
|
+
return allowedSites[0];
|
|
7685
|
+
}
|
|
7686
|
+
if (!allowedSites.includes(requestedSiteId)) {
|
|
7687
|
+
throw new import_http_exception.HTTPException(403, {
|
|
7688
|
+
message: `Forbidden: User is not authorized to access site "${requestedSiteId}".`
|
|
7689
|
+
});
|
|
7690
|
+
}
|
|
7691
|
+
return requestedSiteId;
|
|
7692
|
+
}
|
|
7693
|
+
return requestedSiteId || "default";
|
|
7694
|
+
}
|
|
7695
|
+
|
|
7696
|
+
// src/controllers/collection.controller.ts
|
|
7697
|
+
var CollectionController = class {
|
|
7698
|
+
collection;
|
|
7699
|
+
constructor(collection) {
|
|
7700
|
+
this.collection = collection;
|
|
7701
|
+
}
|
|
7702
|
+
checkTenantAccess(c) {
|
|
7703
|
+
let siteId;
|
|
7704
|
+
try {
|
|
7705
|
+
const hasSiteHeader = Boolean(c.req.header("X-Site-Id") || c.get("siteId"));
|
|
7706
|
+
const allowedSites = getAllowedSitesForUser(c.get("user"), c.get("authTokenPayload"));
|
|
7707
|
+
if (hasSiteHeader || allowedSites && allowedSites.length > 0) {
|
|
7708
|
+
siteId = resolveAuthorizedSiteId(c);
|
|
7709
|
+
}
|
|
7710
|
+
} catch (err) {
|
|
7711
|
+
if (err instanceof import_http_exception2.HTTPException) {
|
|
7712
|
+
return {
|
|
7713
|
+
ok: false,
|
|
7714
|
+
response: c.json({ error: true, message: err.message }, err.status)
|
|
7715
|
+
};
|
|
7716
|
+
}
|
|
7717
|
+
throw err;
|
|
7718
|
+
}
|
|
7719
|
+
if (this.collection.siteId && !this.collection.shared) {
|
|
7720
|
+
if (!siteId || this.collection.siteId !== siteId) {
|
|
7721
|
+
return {
|
|
7722
|
+
ok: false,
|
|
7723
|
+
response: c.json(
|
|
7724
|
+
{ error: true, message: `Collection "${this.collection.slug}" not found in project.` },
|
|
7725
|
+
404
|
|
7726
|
+
)
|
|
7727
|
+
};
|
|
7728
|
+
}
|
|
7729
|
+
}
|
|
7730
|
+
return { ok: true };
|
|
7731
|
+
}
|
|
7732
|
+
getDelegatedProvider(c) {
|
|
7733
|
+
const config = c.get("config");
|
|
7734
|
+
const authCollection = getAdminAuthCollection(config);
|
|
7735
|
+
if (!authCollection || this.collection.slug !== authCollection.slug) {
|
|
7736
|
+
return null;
|
|
7737
|
+
}
|
|
7738
|
+
return config.adminAuth?.providers?.find((p) => p.members) || null;
|
|
7739
|
+
}
|
|
7740
|
+
toHookRequestContext(c) {
|
|
7741
|
+
return toHookRequestContext(c.req);
|
|
7742
|
+
}
|
|
7743
|
+
sanitizeDoc(doc) {
|
|
7744
|
+
if (!doc || typeof doc !== "object") return doc;
|
|
7745
|
+
if (this.collection.auth) {
|
|
7746
|
+
const {
|
|
7747
|
+
password: _p,
|
|
7748
|
+
salt: _s,
|
|
7749
|
+
resetPasswordToken: _rpt,
|
|
7750
|
+
resetPasswordExpires: _rpe,
|
|
7751
|
+
loginAttempts: _la,
|
|
7752
|
+
lockUntil: _lu,
|
|
7753
|
+
...safeDoc
|
|
7754
|
+
} = doc;
|
|
7755
|
+
return safeDoc;
|
|
7756
|
+
}
|
|
7757
|
+
return doc;
|
|
7758
|
+
}
|
|
7759
|
+
async evaluateAccess(c, action, options = {}) {
|
|
7760
|
+
const config = c.get("config");
|
|
7761
|
+
return resolveCollectionAccess(
|
|
7762
|
+
config,
|
|
7763
|
+
this.collection.slug,
|
|
7764
|
+
action,
|
|
7765
|
+
this.collection.access?.[action],
|
|
7766
|
+
{
|
|
7767
|
+
id: options.id,
|
|
7768
|
+
user: c.get("user"),
|
|
7769
|
+
req: this.toHookRequestContext(c),
|
|
7770
|
+
doc: options.doc ?? void 0,
|
|
7771
|
+
data: options.data
|
|
7772
|
+
}
|
|
7773
|
+
);
|
|
7774
|
+
}
|
|
7775
|
+
async find(c) {
|
|
7776
|
+
const tenantCheck = this.checkTenantAccess(c);
|
|
7562
7777
|
if (!tenantCheck.ok) return tenantCheck.response;
|
|
7563
7778
|
const config = c.get("config");
|
|
7564
7779
|
const db = config.db;
|
|
7565
7780
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
7566
7781
|
const provider = this.getDelegatedProvider(c);
|
|
7567
|
-
if (provider && provider.members?.
|
|
7782
|
+
if (provider && provider.members?.list) {
|
|
7568
7783
|
const hookReq = this.toHookRequestContext(c);
|
|
7569
|
-
const
|
|
7570
|
-
|
|
7571
|
-
|
|
7784
|
+
const limit2 = Number(c.req.query("limit")) || 10;
|
|
7785
|
+
const page2 = Number(c.req.query("page")) || 1;
|
|
7786
|
+
const sort2 = c.req.query("sort") || void 0;
|
|
7787
|
+
const search2 = c.req.query("search") || void 0;
|
|
7788
|
+
let where2 = void 0;
|
|
7789
|
+
const whereRaw2 = c.req.query("where");
|
|
7790
|
+
if (whereRaw2) {
|
|
7791
|
+
try {
|
|
7792
|
+
where2 = JSON.parse(decodeURIComponent(whereRaw2));
|
|
7793
|
+
} catch {
|
|
7794
|
+
}
|
|
7572
7795
|
}
|
|
7573
|
-
const
|
|
7574
|
-
|
|
7575
|
-
|
|
7796
|
+
const searchWhere2 = await buildCollectionSearchWhere({
|
|
7797
|
+
collection: this.collection,
|
|
7798
|
+
search: search2,
|
|
7799
|
+
db: createReadonlyDb(db),
|
|
7800
|
+
collections: config.collections
|
|
7801
|
+
});
|
|
7802
|
+
if (searchWhere2) {
|
|
7803
|
+
where2 = mergeWhereConstraint(where2, searchWhere2);
|
|
7804
|
+
}
|
|
7805
|
+
const paginatedResult = await provider.members.list({
|
|
7806
|
+
limit: limit2,
|
|
7807
|
+
page: page2,
|
|
7808
|
+
sort: sort2,
|
|
7809
|
+
where: where2,
|
|
7576
7810
|
req: hookReq
|
|
7577
7811
|
});
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7812
|
+
const mappedDocs = [];
|
|
7813
|
+
for (const m of paginatedResult.docs) {
|
|
7814
|
+
let localId = m.id;
|
|
7815
|
+
const localDoc = await db.find({
|
|
7816
|
+
collection: this.collection.slug,
|
|
7817
|
+
where: m.id ? { externalSubject: { equals: m.id } } : { email: { equals: m.email } },
|
|
7818
|
+
limit: 1
|
|
7819
|
+
});
|
|
7820
|
+
if (localDoc.docs[0]) {
|
|
7821
|
+
localId = localDoc.docs[0].id;
|
|
7822
|
+
}
|
|
7823
|
+
mappedDocs.push({
|
|
7824
|
+
...m,
|
|
7825
|
+
id: localId,
|
|
7826
|
+
externalSubject: m.id
|
|
7827
|
+
});
|
|
7828
|
+
}
|
|
7829
|
+
return c.json({
|
|
7830
|
+
...paginatedResult,
|
|
7831
|
+
docs: mappedDocs
|
|
7832
|
+
});
|
|
7586
7833
|
}
|
|
7587
7834
|
const readonlyDb = createReadonlyDb(db);
|
|
7588
|
-
const
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
const
|
|
7835
|
+
const limit = Math.min(Number(c.req.query("limit")) || 10, 100);
|
|
7836
|
+
const page = Number(c.req.query("page")) || 1;
|
|
7837
|
+
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
|
|
7838
|
+
const sort = c.req.query("sort") || void 0;
|
|
7839
|
+
const search = c.req.query("search") || void 0;
|
|
7593
7840
|
const user = c.get("user");
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
};
|
|
7602
|
-
data = DefaultsService.apply(this.collection.fields, data);
|
|
7603
|
-
data = normalizeMoneyFields(this.collection.fields, data);
|
|
7604
|
-
if (this.collection.workflow) {
|
|
7605
|
-
data = initializeWorkflowDocument(data, this.collection.workflow);
|
|
7841
|
+
let where = void 0;
|
|
7842
|
+
const whereRaw = c.req.query("where");
|
|
7843
|
+
if (whereRaw) {
|
|
7844
|
+
try {
|
|
7845
|
+
where = JSON.parse(decodeURIComponent(whereRaw));
|
|
7846
|
+
} catch {
|
|
7847
|
+
}
|
|
7606
7848
|
}
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7849
|
+
if (where) {
|
|
7850
|
+
if (this.collection.admin?.filterable === false) {
|
|
7851
|
+
where = void 0;
|
|
7852
|
+
} else {
|
|
7853
|
+
const { sanitizeWhereClause: sanitizeWhereClause2 } = await Promise.resolve().then(() => (init_where_sanitizer(), where_sanitizer_exports));
|
|
7854
|
+
where = sanitizeWhereClause2(where, this.collection.fields);
|
|
7855
|
+
if (Object.keys(where).length === 0) {
|
|
7856
|
+
where = void 0;
|
|
7857
|
+
}
|
|
7858
|
+
}
|
|
7616
7859
|
}
|
|
7617
|
-
|
|
7618
|
-
|
|
7860
|
+
const searchWhere = await buildCollectionSearchWhere({
|
|
7861
|
+
collection: this.collection,
|
|
7862
|
+
search,
|
|
7863
|
+
db: readonlyDb,
|
|
7864
|
+
collections: config.collections
|
|
7865
|
+
});
|
|
7866
|
+
if (searchWhere) {
|
|
7867
|
+
where = mergeWhereConstraint(where, searchWhere);
|
|
7619
7868
|
}
|
|
7620
|
-
|
|
7869
|
+
const beforeReadResult = await runCollectionHooks(
|
|
7870
|
+
this.collection.hooks?.beforeRead,
|
|
7621
7871
|
{
|
|
7622
|
-
|
|
7623
|
-
|
|
7872
|
+
req: c.req,
|
|
7873
|
+
query: where,
|
|
7624
7874
|
user,
|
|
7625
|
-
|
|
7626
|
-
data,
|
|
7627
|
-
operation: "create"
|
|
7875
|
+
db: readonlyDb
|
|
7628
7876
|
},
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
data,
|
|
7634
|
-
null,
|
|
7635
|
-
user,
|
|
7636
|
-
readonlyDb
|
|
7877
|
+
{
|
|
7878
|
+
surface: "collection.beforeRead",
|
|
7879
|
+
path: `collection:${this.collection.slug}.hooks.beforeRead`
|
|
7880
|
+
}
|
|
7637
7881
|
);
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
req: c.req,
|
|
7641
|
-
user,
|
|
7642
|
-
operation: "create",
|
|
7643
|
-
db: readonlyDb
|
|
7644
|
-
}, {
|
|
7645
|
-
surface: "collection.beforeChange",
|
|
7646
|
-
path: `collection:${this.collection.slug}.hooks.beforeChange`
|
|
7647
|
-
});
|
|
7648
|
-
if (!data.id) {
|
|
7649
|
-
data = { ...data, id: generateDocumentId(this.collection) };
|
|
7882
|
+
if (beforeReadResult !== void 0) {
|
|
7883
|
+
where = beforeReadResult;
|
|
7650
7884
|
}
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7885
|
+
if (this.collection.workflow && !canViewWorkflowDraft(this.collection.workflow, user)) {
|
|
7886
|
+
const publicWorkflowState = publishedStateName(this.collection.workflow);
|
|
7887
|
+
where = where ? {
|
|
7888
|
+
AND: [
|
|
7889
|
+
where,
|
|
7890
|
+
{
|
|
7891
|
+
OR: [
|
|
7892
|
+
{ __published: { exists: true } },
|
|
7893
|
+
{ __workflow: { exists: false } },
|
|
7894
|
+
{ "__workflow.state": { equals: publicWorkflowState } }
|
|
7895
|
+
]
|
|
7896
|
+
}
|
|
7897
|
+
]
|
|
7898
|
+
} : {
|
|
7899
|
+
OR: [
|
|
7900
|
+
{ __published: { exists: true } },
|
|
7901
|
+
{ __workflow: { exists: false } },
|
|
7902
|
+
{ "__workflow.state": { equals: publicWorkflowState } }
|
|
7903
|
+
]
|
|
7904
|
+
};
|
|
7905
|
+
}
|
|
7906
|
+
const access = await this.evaluateAccess(c, "read");
|
|
7907
|
+
if (!access.allowed) {
|
|
7908
|
+
return c.json(
|
|
7909
|
+
{
|
|
7910
|
+
error: true,
|
|
7911
|
+
message: `Access denied: read on ${this.collection.slug}`
|
|
7912
|
+
},
|
|
7913
|
+
403
|
|
7914
|
+
);
|
|
7915
|
+
}
|
|
7916
|
+
if (access.constraint) {
|
|
7917
|
+
where = mergeWhereConstraint(where, access.constraint);
|
|
7918
|
+
}
|
|
7919
|
+
let result = await db.find({
|
|
7920
|
+
collection: this.collection.slug,
|
|
7921
|
+
limit,
|
|
7922
|
+
page,
|
|
7923
|
+
sort,
|
|
7924
|
+
where,
|
|
7925
|
+
fields: this.collection.fields
|
|
7926
|
+
});
|
|
7927
|
+
if (result.total === 0 && this.collection.initialData && !where && page === 1) {
|
|
7928
|
+
getRequestLogger(c, "collection").info({
|
|
7929
|
+
msg: "Auto-seeding collection from config.initialData",
|
|
7930
|
+
collection: this.collection.slug
|
|
7931
|
+
});
|
|
7932
|
+
for (const data of this.collection.initialData) {
|
|
7933
|
+
await db.create({ collection: this.collection.slug, data });
|
|
7934
|
+
}
|
|
7935
|
+
result = await db.find({
|
|
7666
7936
|
collection: this.collection.slug,
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7937
|
+
limit,
|
|
7938
|
+
page,
|
|
7939
|
+
sort,
|
|
7940
|
+
where,
|
|
7941
|
+
fields: this.collection.fields
|
|
7942
|
+
});
|
|
7672
7943
|
}
|
|
7673
|
-
|
|
7674
|
-
this.collection.
|
|
7675
|
-
{
|
|
7944
|
+
result.docs = result.docs.map(
|
|
7945
|
+
(doc) => this.collection.workflow ? materializeWorkflowDocument(
|
|
7676
7946
|
doc,
|
|
7947
|
+
this.collection.workflow,
|
|
7948
|
+
user
|
|
7949
|
+
) : doc
|
|
7950
|
+
).filter((doc) => doc !== null);
|
|
7951
|
+
const processedDocs = [];
|
|
7952
|
+
for (const doc of result.docs) {
|
|
7953
|
+
const docWithDefaults = DefaultsService.apply(
|
|
7954
|
+
this.collection.fields,
|
|
7955
|
+
doc
|
|
7956
|
+
);
|
|
7957
|
+
const docWithCollectionHooks = await runCollectionHooks(
|
|
7958
|
+
this.collection.hooks?.afterRead,
|
|
7959
|
+
{
|
|
7960
|
+
doc: docWithDefaults,
|
|
7961
|
+
req: c.req,
|
|
7962
|
+
user,
|
|
7963
|
+
db: readonlyDb
|
|
7964
|
+
}
|
|
7965
|
+
);
|
|
7966
|
+
const docWithFieldHooks = await executeFieldAfterRead(
|
|
7967
|
+
this.collection.fields,
|
|
7968
|
+
docWithCollectionHooks,
|
|
7677
7969
|
user,
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7970
|
+
readonlyDb
|
|
7971
|
+
);
|
|
7972
|
+
const docWithFieldAccess = await applyFieldReadAccess(
|
|
7973
|
+
{
|
|
7974
|
+
config,
|
|
7975
|
+
fields: this.collection.fields,
|
|
7976
|
+
user,
|
|
7977
|
+
req: this.toHookRequestContext(c),
|
|
7978
|
+
doc: docWithFieldHooks
|
|
7979
|
+
},
|
|
7980
|
+
docWithFieldHooks
|
|
7981
|
+
);
|
|
7982
|
+
processedDocs.push(docWithFieldAccess);
|
|
7983
|
+
}
|
|
7984
|
+
result.docs = processedDocs;
|
|
7985
|
+
if (depth > 0) {
|
|
7986
|
+
const populationService = new PopulationService(db, config.collections);
|
|
7987
|
+
result = await populationService.populateResult(
|
|
7988
|
+
result,
|
|
7989
|
+
this.collection.fields,
|
|
7990
|
+
depth
|
|
7991
|
+
);
|
|
7992
|
+
}
|
|
7993
|
+
if (this.collection.auth && result.docs && Array.isArray(result.docs)) {
|
|
7994
|
+
result.docs = result.docs.map((d) => this.sanitizeDoc(d));
|
|
7995
|
+
}
|
|
7996
|
+
return c.json(result);
|
|
7997
|
+
}
|
|
7998
|
+
async findOne(c) {
|
|
7999
|
+
const tenantCheck = this.checkTenantAccess(c);
|
|
8000
|
+
if (!tenantCheck.ok) return tenantCheck.response;
|
|
8001
|
+
const config = c.get("config");
|
|
8002
|
+
const db = config.db;
|
|
8003
|
+
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
8004
|
+
const provider = this.getDelegatedProvider(c);
|
|
8005
|
+
if (provider && (provider.members?.get || provider.members?.list)) {
|
|
8006
|
+
const hookReq = this.toHookRequestContext(c);
|
|
8007
|
+
const id2 = c.req.param("id");
|
|
8008
|
+
if (!id2) return c.json({ message: "Missing ID" }, 400);
|
|
8009
|
+
const localDoc = await db.findOne({
|
|
7689
8010
|
collection: this.collection.slug,
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
8011
|
+
id: id2
|
|
8012
|
+
});
|
|
8013
|
+
const externalSubject = localDoc?.externalSubject || id2;
|
|
8014
|
+
if (provider.members.get) {
|
|
8015
|
+
const member = await provider.members.get({
|
|
8016
|
+
externalSubject,
|
|
8017
|
+
req: hookReq
|
|
8018
|
+
});
|
|
8019
|
+
if (member) {
|
|
8020
|
+
return c.json({
|
|
8021
|
+
...member,
|
|
8022
|
+
id: localDoc ? localDoc.id : member.id,
|
|
8023
|
+
externalSubject: member.id
|
|
8024
|
+
});
|
|
8025
|
+
}
|
|
8026
|
+
} else if (provider.members.list) {
|
|
8027
|
+
const listResult = await provider.members.list({ req: hookReq });
|
|
8028
|
+
const matched = listResult.docs.find((m) => m.id === externalSubject || m.id === id2);
|
|
8029
|
+
if (matched) {
|
|
8030
|
+
return c.json({
|
|
8031
|
+
...matched,
|
|
8032
|
+
id: localDoc ? localDoc.id : matched.id,
|
|
8033
|
+
externalSubject: matched.id
|
|
8034
|
+
});
|
|
8035
|
+
}
|
|
8036
|
+
}
|
|
7693
8037
|
}
|
|
7694
|
-
const
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
surface: "collection.afterRead",
|
|
7702
|
-
path: `collection:${this.collection.slug}.hooks.afterRead`
|
|
8038
|
+
const id = c.req.param("id");
|
|
8039
|
+
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
8040
|
+
const depth = Number.parseInt(c.req.query("depth") || "0", 10);
|
|
8041
|
+
const user = c.get("user");
|
|
8042
|
+
const doc = await db.findOne({
|
|
8043
|
+
collection: this.collection.slug,
|
|
8044
|
+
id
|
|
7703
8045
|
});
|
|
7704
|
-
|
|
8046
|
+
if (!doc) {
|
|
8047
|
+
return c.json({ message: "Document not found" }, 404);
|
|
8048
|
+
}
|
|
8049
|
+
let effectiveDoc = doc;
|
|
8050
|
+
if (this.collection.workflow) {
|
|
8051
|
+
effectiveDoc = materializeWorkflowDocument(
|
|
8052
|
+
doc,
|
|
8053
|
+
this.collection.workflow,
|
|
8054
|
+
user
|
|
8055
|
+
);
|
|
8056
|
+
if (!effectiveDoc) return c.json({ message: "Document not found" }, 404);
|
|
8057
|
+
}
|
|
8058
|
+
const access = await this.evaluateAccess(c, "read", { id, doc: effectiveDoc });
|
|
8059
|
+
if (!access.allowed) {
|
|
8060
|
+
return c.json(
|
|
8061
|
+
{
|
|
8062
|
+
error: true,
|
|
8063
|
+
message: `Access denied: read on ${this.collection.slug}`
|
|
8064
|
+
},
|
|
8065
|
+
403
|
|
8066
|
+
);
|
|
8067
|
+
}
|
|
8068
|
+
const docWithDefaults = DefaultsService.apply(
|
|
7705
8069
|
this.collection.fields,
|
|
7706
|
-
|
|
8070
|
+
effectiveDoc
|
|
8071
|
+
);
|
|
8072
|
+
const readonlyDb = createReadonlyDb(db);
|
|
8073
|
+
const docWithCollectionHooks = await runCollectionHooks(
|
|
8074
|
+
this.collection.hooks?.afterRead,
|
|
8075
|
+
{
|
|
8076
|
+
doc: docWithDefaults,
|
|
8077
|
+
req: c.req,
|
|
8078
|
+
user,
|
|
8079
|
+
db: readonlyDb
|
|
8080
|
+
}
|
|
8081
|
+
);
|
|
8082
|
+
const docWithFieldHooks = await executeFieldAfterRead(
|
|
8083
|
+
this.collection.fields,
|
|
8084
|
+
docWithCollectionHooks,
|
|
7707
8085
|
user,
|
|
7708
8086
|
readonlyDb
|
|
7709
8087
|
);
|
|
7710
|
-
const
|
|
8088
|
+
const docWithFieldAccess = await applyFieldReadAccess(
|
|
7711
8089
|
{
|
|
7712
8090
|
config,
|
|
7713
8091
|
fields: this.collection.fields,
|
|
7714
8092
|
user,
|
|
7715
8093
|
req: this.toHookRequestContext(c),
|
|
7716
|
-
doc:
|
|
8094
|
+
doc: docWithFieldHooks
|
|
8095
|
+
},
|
|
8096
|
+
docWithFieldHooks
|
|
8097
|
+
);
|
|
8098
|
+
let finalDoc = docWithFieldAccess;
|
|
8099
|
+
if (depth > 0 && docWithFieldAccess) {
|
|
8100
|
+
const populationService = new PopulationService(db, config.collections);
|
|
8101
|
+
finalDoc = await populationService.populate({
|
|
8102
|
+
data: docWithFieldAccess,
|
|
8103
|
+
fields: this.collection.fields,
|
|
8104
|
+
currentDepth: 0,
|
|
8105
|
+
maxDepth: depth
|
|
8106
|
+
});
|
|
8107
|
+
}
|
|
8108
|
+
if (finalDoc && this.collection.detail) {
|
|
8109
|
+
finalDoc = await evaluateDetailComputed(this.collection.detail, finalDoc, user, readonlyDb);
|
|
8110
|
+
}
|
|
8111
|
+
return c.json(this.sanitizeDoc(finalDoc));
|
|
8112
|
+
}
|
|
8113
|
+
async create(c) {
|
|
8114
|
+
const tenantCheck = this.checkTenantAccess(c);
|
|
8115
|
+
if (!tenantCheck.ok) return tenantCheck.response;
|
|
8116
|
+
const config = c.get("config");
|
|
8117
|
+
const db = config.db;
|
|
8118
|
+
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
8119
|
+
const provider = this.getDelegatedProvider(c);
|
|
8120
|
+
if (provider && provider.members?.create) {
|
|
8121
|
+
const hookReq = this.toHookRequestContext(c);
|
|
8122
|
+
const contentType2 = c.req.header("Content-Type") || "";
|
|
8123
|
+
if (contentType2.toLowerCase().includes("multipart/form-data")) {
|
|
8124
|
+
return this.upload(c);
|
|
8125
|
+
}
|
|
8126
|
+
const body2 = await c.req.json();
|
|
8127
|
+
const member = await provider.members.create({
|
|
8128
|
+
data: body2,
|
|
8129
|
+
req: hookReq
|
|
8130
|
+
});
|
|
8131
|
+
return c.json(
|
|
8132
|
+
{
|
|
8133
|
+
...member,
|
|
8134
|
+
id: member.id,
|
|
8135
|
+
externalSubject: member.id
|
|
8136
|
+
},
|
|
8137
|
+
201
|
|
8138
|
+
);
|
|
8139
|
+
}
|
|
8140
|
+
const readonlyDb = createReadonlyDb(db);
|
|
8141
|
+
const contentType = c.req.header("Content-Type") || "";
|
|
8142
|
+
if (contentType.toLowerCase().includes("multipart/form-data")) {
|
|
8143
|
+
return this.upload(c);
|
|
8144
|
+
}
|
|
8145
|
+
const body = await c.req.json();
|
|
8146
|
+
const user = c.get("user");
|
|
8147
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
8148
|
+
let data = {
|
|
8149
|
+
...body,
|
|
8150
|
+
createdAt: now2,
|
|
8151
|
+
updatedAt: now2,
|
|
8152
|
+
createdBy: user?.sub ?? null,
|
|
8153
|
+
updatedBy: user?.sub ?? null
|
|
8154
|
+
};
|
|
8155
|
+
data = DefaultsService.apply(this.collection.fields, data);
|
|
8156
|
+
data = normalizeMoneyFields(this.collection.fields, data);
|
|
8157
|
+
if (this.collection.workflow) {
|
|
8158
|
+
data = initializeWorkflowDocument(data, this.collection.workflow);
|
|
8159
|
+
}
|
|
8160
|
+
const createAccess = await this.evaluateAccess(c, "create", { data });
|
|
8161
|
+
if (!createAccess.allowed) {
|
|
8162
|
+
return c.json(
|
|
8163
|
+
{
|
|
8164
|
+
error: true,
|
|
8165
|
+
message: `Access denied: create on ${this.collection.slug}`
|
|
8166
|
+
},
|
|
8167
|
+
403
|
|
8168
|
+
);
|
|
8169
|
+
}
|
|
8170
|
+
if (this.collection.auth && data.password) {
|
|
8171
|
+
data.password = await hashPassword(data.password);
|
|
8172
|
+
}
|
|
8173
|
+
data = await applyFieldWriteAccess(
|
|
8174
|
+
{
|
|
8175
|
+
config,
|
|
8176
|
+
fields: this.collection.fields,
|
|
8177
|
+
user,
|
|
8178
|
+
req: this.toHookRequestContext(c),
|
|
8179
|
+
data,
|
|
8180
|
+
operation: "create"
|
|
8181
|
+
},
|
|
8182
|
+
data
|
|
8183
|
+
);
|
|
8184
|
+
data = await executeFieldBeforeChange(
|
|
8185
|
+
this.collection.fields,
|
|
8186
|
+
data,
|
|
8187
|
+
null,
|
|
8188
|
+
user,
|
|
8189
|
+
readonlyDb
|
|
8190
|
+
);
|
|
8191
|
+
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
8192
|
+
data,
|
|
8193
|
+
req: c.req,
|
|
8194
|
+
user,
|
|
8195
|
+
operation: "create",
|
|
8196
|
+
db: readonlyDb
|
|
8197
|
+
}, {
|
|
8198
|
+
surface: "collection.beforeChange",
|
|
8199
|
+
path: `collection:${this.collection.slug}.hooks.beforeChange`
|
|
8200
|
+
});
|
|
8201
|
+
if (!data.id) {
|
|
8202
|
+
data = { ...data, id: generateDocumentId(this.collection) };
|
|
8203
|
+
}
|
|
8204
|
+
const beforeCommit = this.buildBeforeCommit(c, "create", user);
|
|
8205
|
+
const doc = this.collection.workflow ? (await createWorkflowDocument({
|
|
8206
|
+
config,
|
|
8207
|
+
collection: this.collection,
|
|
8208
|
+
data,
|
|
8209
|
+
user,
|
|
8210
|
+
beforeCommit
|
|
8211
|
+
})).doc : await this.persistInTransaction(
|
|
8212
|
+
db,
|
|
8213
|
+
beforeCommit,
|
|
8214
|
+
(writer) => writer.create({ collection: this.collection.slug, data })
|
|
8215
|
+
);
|
|
8216
|
+
if (this.collection.audit && db) {
|
|
8217
|
+
AuditService.log(db, {
|
|
8218
|
+
operation: "create",
|
|
8219
|
+
collection: this.collection.slug,
|
|
8220
|
+
documentId: doc.id,
|
|
8221
|
+
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
8222
|
+
before: null,
|
|
8223
|
+
after: doc
|
|
8224
|
+
}, config);
|
|
8225
|
+
}
|
|
8226
|
+
await runCollectionHooks(
|
|
8227
|
+
this.collection.hooks?.afterChange,
|
|
8228
|
+
{
|
|
8229
|
+
doc,
|
|
8230
|
+
user,
|
|
8231
|
+
req: c.req,
|
|
8232
|
+
operation: "create",
|
|
8233
|
+
db
|
|
8234
|
+
},
|
|
8235
|
+
{ isolated: true }
|
|
8236
|
+
);
|
|
8237
|
+
if (db && config) {
|
|
8238
|
+
const siteId = resolveAuthorizedSiteId(c);
|
|
8239
|
+
RAGService.indexDocument({
|
|
8240
|
+
db,
|
|
8241
|
+
config,
|
|
8242
|
+
collection: this.collection.slug,
|
|
8243
|
+
doc,
|
|
8244
|
+
projectId: siteId
|
|
8245
|
+
}).catch((err) => console.error(`[dyrected/rag] Auto-index create failed:`, err?.message || err));
|
|
8246
|
+
}
|
|
8247
|
+
const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
|
|
8248
|
+
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
8249
|
+
doc: responseDoc,
|
|
8250
|
+
req: c.req,
|
|
8251
|
+
user,
|
|
8252
|
+
db: readonlyDb
|
|
8253
|
+
}, {
|
|
8254
|
+
surface: "collection.afterRead",
|
|
8255
|
+
path: `collection:${this.collection.slug}.hooks.afterRead`
|
|
8256
|
+
});
|
|
8257
|
+
const finalDoc = await executeFieldAfterRead(
|
|
8258
|
+
this.collection.fields,
|
|
8259
|
+
readDoc,
|
|
8260
|
+
user,
|
|
8261
|
+
readonlyDb
|
|
8262
|
+
);
|
|
8263
|
+
const accessibleDoc = await applyFieldReadAccess(
|
|
8264
|
+
{
|
|
8265
|
+
config,
|
|
8266
|
+
fields: this.collection.fields,
|
|
8267
|
+
user,
|
|
8268
|
+
req: this.toHookRequestContext(c),
|
|
8269
|
+
doc: finalDoc
|
|
7717
8270
|
},
|
|
7718
8271
|
finalDoc
|
|
7719
8272
|
);
|
|
@@ -8085,13 +8638,14 @@ var CollectionController = class {
|
|
|
8085
8638
|
const config = c.get("config");
|
|
8086
8639
|
const db = config.db;
|
|
8087
8640
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
8088
|
-
|
|
8641
|
+
const allViews = resolveAllCollectionViews(this.collection, config);
|
|
8642
|
+
if (!allViews.length && !this.collection.actions?.length) {
|
|
8089
8643
|
return c.json({ error: true, message: `Collection ${this.collection.slug} has no views or actions` }, 404);
|
|
8090
8644
|
}
|
|
8091
8645
|
const actionName = c.req.param("action");
|
|
8092
8646
|
const viewSlugParam = c.req.param("viewSlug");
|
|
8093
8647
|
let action;
|
|
8094
|
-
for (const view2 of
|
|
8648
|
+
for (const view2 of allViews) {
|
|
8095
8649
|
if (viewSlugParam && view2.slug !== viewSlugParam) continue;
|
|
8096
8650
|
const match = view2.actions?.find((candidate) => candidate.name === actionName);
|
|
8097
8651
|
if (match) {
|
|
@@ -8120,7 +8674,7 @@ var CollectionController = class {
|
|
|
8120
8674
|
const input = body?.input ?? {};
|
|
8121
8675
|
const user = c.get("user");
|
|
8122
8676
|
const accessArgs = { user, req: this.toHookRequestContext(c) };
|
|
8123
|
-
const view = viewSlugParam ?
|
|
8677
|
+
const view = viewSlugParam ? allViews.find((candidate) => candidate.slug === viewSlugParam) : void 0;
|
|
8124
8678
|
if (view?.access) {
|
|
8125
8679
|
const allowed = await resolveBooleanAccess(config, view.access.update ?? view.access.read ?? true, {
|
|
8126
8680
|
...accessArgs,
|
|
@@ -13750,762 +14304,231 @@ function generateOpenApi(config) {
|
|
|
13750
14304
|
};
|
|
13751
14305
|
}
|
|
13752
14306
|
if (config.storage) {
|
|
13753
|
-
spec.paths["/api/media/{filename}"] = {
|
|
13754
|
-
get: {
|
|
13755
|
-
tags: ["Media"],
|
|
13756
|
-
summary: "Serve a stored file",
|
|
13757
|
-
security: [],
|
|
13758
|
-
parameters: [
|
|
13759
|
-
{
|
|
13760
|
-
name: "filename",
|
|
13761
|
-
in: "path",
|
|
13762
|
-
required: true,
|
|
13763
|
-
schema: { type: "string" }
|
|
13764
|
-
}
|
|
13765
|
-
],
|
|
13766
|
-
responses: {
|
|
13767
|
-
200: { description: "Stored file bytes" },
|
|
13768
|
-
404: { description: "File not found" }
|
|
13769
|
-
}
|
|
13770
|
-
}
|
|
13771
|
-
};
|
|
13772
|
-
}
|
|
13773
|
-
return spec;
|
|
13774
|
-
}
|
|
13775
|
-
function collectionToSchema(collection) {
|
|
13776
|
-
const { properties, required } = fieldsToProperties(collection.fields);
|
|
13777
|
-
return {
|
|
13778
|
-
type: "object",
|
|
13779
|
-
properties: {
|
|
13780
|
-
id: { type: "string" },
|
|
13781
|
-
createdAt: { type: "string", format: "date-time" },
|
|
13782
|
-
updatedAt: { type: "string", format: "date-time" },
|
|
13783
|
-
...collection.workflow ? { _workflow: { $ref: "#/components/schemas/WorkflowMetadata" } } : {},
|
|
13784
|
-
...properties
|
|
13785
|
-
},
|
|
13786
|
-
required: ["id", ...required]
|
|
13787
|
-
};
|
|
13788
|
-
}
|
|
13789
|
-
function globalToSchema(global) {
|
|
13790
|
-
const { properties, required } = fieldsToProperties(global.fields);
|
|
13791
|
-
return {
|
|
13792
|
-
type: "object",
|
|
13793
|
-
properties,
|
|
13794
|
-
required
|
|
13795
|
-
};
|
|
13796
|
-
}
|
|
13797
|
-
function fieldsToProperties(fields) {
|
|
13798
|
-
const props = {};
|
|
13799
|
-
const required = [];
|
|
13800
|
-
for (const field of fields) {
|
|
13801
|
-
if (field.type === "row") {
|
|
13802
|
-
const nested = fieldsToProperties(field.fields || []);
|
|
13803
|
-
Object.assign(props, nested.properties);
|
|
13804
|
-
required.push(...nested.required);
|
|
13805
|
-
continue;
|
|
13806
|
-
}
|
|
13807
|
-
if (!field.name) continue;
|
|
13808
|
-
props[field.name] = fieldToSchema(field);
|
|
13809
|
-
if (field.required) {
|
|
13810
|
-
required.push(field.name);
|
|
13811
|
-
}
|
|
13812
|
-
}
|
|
13813
|
-
return { properties: props, required };
|
|
13814
|
-
}
|
|
13815
|
-
function fieldToSchema(field) {
|
|
13816
|
-
let schema;
|
|
13817
|
-
switch (field.type) {
|
|
13818
|
-
case "text":
|
|
13819
|
-
case "textarea":
|
|
13820
|
-
case "email":
|
|
13821
|
-
schema = { type: "string" };
|
|
13822
|
-
break;
|
|
13823
|
-
case "url":
|
|
13824
|
-
schema = {
|
|
13825
|
-
oneOf: [
|
|
13826
|
-
{ type: "string" },
|
|
13827
|
-
{ type: "object", additionalProperties: true }
|
|
13828
|
-
]
|
|
13829
|
-
};
|
|
13830
|
-
break;
|
|
13831
|
-
case "icon":
|
|
13832
|
-
schema = { type: "string" };
|
|
13833
|
-
break;
|
|
13834
|
-
case "number":
|
|
13835
|
-
schema = { type: "number" };
|
|
13836
|
-
break;
|
|
13837
|
-
case "money":
|
|
13838
|
-
schema = { type: "integer", description: "Amount in minor units (for example kobo or cents)." };
|
|
13839
|
-
break;
|
|
13840
|
-
case "boolean":
|
|
13841
|
-
schema = { type: "boolean" };
|
|
13842
|
-
break;
|
|
13843
|
-
case "date":
|
|
13844
|
-
schema = { type: "string", format: "date" };
|
|
13845
|
-
break;
|
|
13846
|
-
case "datetime":
|
|
13847
|
-
schema = { type: "string", format: "date-time" };
|
|
13848
|
-
break;
|
|
13849
|
-
case "time":
|
|
13850
|
-
schema = { type: "string", format: "time" };
|
|
13851
|
-
break;
|
|
13852
|
-
case "select":
|
|
13853
|
-
case "radio":
|
|
13854
|
-
schema = {
|
|
13855
|
-
type: "string",
|
|
13856
|
-
enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0
|
|
13857
|
-
};
|
|
13858
|
-
break;
|
|
13859
|
-
case "multiSelect":
|
|
13860
|
-
schema = {
|
|
13861
|
-
type: "array",
|
|
13862
|
-
items: {
|
|
13863
|
-
type: "string",
|
|
13864
|
-
enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0
|
|
13865
|
-
}
|
|
13866
|
-
};
|
|
13867
|
-
break;
|
|
13868
|
-
case "relationship":
|
|
13869
|
-
case "image": {
|
|
13870
|
-
const valueSchema = {
|
|
13871
|
-
type: "string",
|
|
13872
|
-
description: `ID of a ${field.relationTo} record`
|
|
13873
|
-
};
|
|
13874
|
-
schema = field.hasMany ? { type: "array", items: valueSchema } : valueSchema;
|
|
13875
|
-
break;
|
|
13876
|
-
}
|
|
13877
|
-
case "join":
|
|
13878
|
-
schema = {
|
|
13879
|
-
type: "array",
|
|
13880
|
-
readOnly: true,
|
|
13881
|
-
items: { type: "object", additionalProperties: true }
|
|
13882
|
-
};
|
|
13883
|
-
break;
|
|
13884
|
-
case "object": {
|
|
13885
|
-
const { properties, required } = fieldsToProperties(field.fields || []);
|
|
13886
|
-
schema = { type: "object", properties, required };
|
|
13887
|
-
break;
|
|
13888
|
-
}
|
|
13889
|
-
case "array": {
|
|
13890
|
-
const { properties, required } = fieldsToProperties(field.fields || []);
|
|
13891
|
-
schema = {
|
|
13892
|
-
type: "array",
|
|
13893
|
-
items: { type: "object", properties, required }
|
|
13894
|
-
};
|
|
13895
|
-
break;
|
|
13896
|
-
}
|
|
13897
|
-
case "json":
|
|
13898
|
-
case "richText":
|
|
13899
|
-
schema = { type: "object", additionalProperties: true };
|
|
13900
|
-
break;
|
|
13901
|
-
case "blocks":
|
|
13902
|
-
schema = {
|
|
13903
|
-
type: "array",
|
|
13904
|
-
items: {
|
|
13905
|
-
oneOf: field.blocks?.map((block) => {
|
|
13906
|
-
const { properties, required } = fieldsToProperties(block.fields);
|
|
13907
|
-
return {
|
|
13908
|
-
type: "object",
|
|
13909
|
-
properties: {
|
|
13910
|
-
blockType: { type: "string", enum: [block.slug] },
|
|
13911
|
-
...properties
|
|
13912
|
-
},
|
|
13913
|
-
required: ["blockType", ...required]
|
|
13914
|
-
};
|
|
13915
|
-
})
|
|
13916
|
-
}
|
|
13917
|
-
};
|
|
13918
|
-
break;
|
|
13919
|
-
default:
|
|
13920
|
-
schema = { type: "string" };
|
|
13921
|
-
}
|
|
13922
|
-
if (field.label) schema.description = field.label;
|
|
13923
|
-
return schema;
|
|
13924
|
-
}
|
|
13925
|
-
|
|
13926
|
-
// src/utils/swagger.ts
|
|
13927
|
-
function getSwaggerHtml(specUrl) {
|
|
13928
|
-
return `
|
|
13929
|
-
<!DOCTYPE html>
|
|
13930
|
-
<html lang="en">
|
|
13931
|
-
<head>
|
|
13932
|
-
<meta charset="utf-8" />
|
|
13933
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
13934
|
-
<meta name="description" content="SwaggerUI" />
|
|
13935
|
-
<title>Dyrected API Documentation</title>
|
|
13936
|
-
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
|
13937
|
-
</head>
|
|
13938
|
-
<body>
|
|
13939
|
-
<div id="swagger-ui"></div>
|
|
13940
|
-
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" charset="UTF-8"></script>
|
|
13941
|
-
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js" charset="UTF-8"></script>
|
|
13942
|
-
<script>
|
|
13943
|
-
window.onload = () => {
|
|
13944
|
-
// Forward the apikey query param when loading the spec and making API calls
|
|
13945
|
-
const params = new URLSearchParams(window.location.search);
|
|
13946
|
-
const apiKey = params.get('apikey');
|
|
13947
|
-
// Resolve the spec relative to this docs page so any mount prefix
|
|
13948
|
-
// (e.g. "/dyrected/api/docs") is preserved. Falls back to an explicit URL
|
|
13949
|
-
// when one is supplied by the caller.
|
|
13950
|
-
const explicitSpecUrl = ${JSON.stringify(specUrl ?? "")};
|
|
13951
|
-
const specUrl = explicitSpecUrl || (window.location.pathname.replace(/\\/docs\\/?$/, '') + '/openapi.json');
|
|
13952
|
-
const specUrlWithKey = apiKey ? specUrl + '?apikey=' + encodeURIComponent(apiKey) : specUrl;
|
|
13953
|
-
|
|
13954
|
-
window.ui = SwaggerUIBundle({
|
|
13955
|
-
url: specUrlWithKey,
|
|
13956
|
-
dom_id: '#swagger-ui',
|
|
13957
|
-
presets: [
|
|
13958
|
-
SwaggerUIBundle.presets.apis,
|
|
13959
|
-
SwaggerUIStandalonePreset
|
|
13960
|
-
],
|
|
13961
|
-
layout: "BaseLayout",
|
|
13962
|
-
deepLinking: true,
|
|
13963
|
-
showExtensions: true,
|
|
13964
|
-
showCommonExtensions: true,
|
|
13965
|
-
// Inject x-api-key header on every request made from the Swagger UI
|
|
13966
|
-
requestInterceptor: (request) => {
|
|
13967
|
-
if (apiKey) {
|
|
13968
|
-
request.headers['x-api-key'] = apiKey;
|
|
13969
|
-
}
|
|
13970
|
-
return request;
|
|
13971
|
-
}
|
|
13972
|
-
});
|
|
13973
|
-
};
|
|
13974
|
-
</script>
|
|
13975
|
-
</body>
|
|
13976
|
-
</html>
|
|
13977
|
-
`;
|
|
13978
|
-
}
|
|
13979
|
-
|
|
13980
|
-
// src/types/navigation.ts
|
|
13981
|
-
var CURRENT_NAV_PREFERENCES_VERSION = 1;
|
|
13982
|
-
var DEFAULT_USER_NAV_PREFERENCES = {
|
|
13983
|
-
_version: CURRENT_NAV_PREFERENCES_VERSION,
|
|
13984
|
-
pinned: [],
|
|
13985
|
-
hidden: [],
|
|
13986
|
-
groupOrder: [],
|
|
13987
|
-
itemOrder: {},
|
|
13988
|
-
groups: [],
|
|
13989
|
-
items: []
|
|
13990
|
-
};
|
|
13991
|
-
|
|
13992
|
-
// src/utils/navigation-reconciler.ts
|
|
13993
|
-
var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
13994
|
-
function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
13995
|
-
if (!baseTree) {
|
|
13996
|
-
return {
|
|
13997
|
-
groups: [],
|
|
13998
|
-
ungrouped: [],
|
|
13999
|
-
pinnedItems: []
|
|
14000
|
-
};
|
|
14001
|
-
}
|
|
14002
|
-
const includeHidden = options?.includeHidden ?? false;
|
|
14003
|
-
const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
|
|
14004
|
-
const hiddenSet = new Set(effectivePrefs.hidden || []);
|
|
14005
|
-
const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
|
|
14006
|
-
const knownGlobalSlugs = new Set((schemas?.globals || []).map((g) => g.slug));
|
|
14007
|
-
const groupsList = [];
|
|
14008
|
-
const findGroup = (query) => {
|
|
14009
|
-
const qNorm = normalizeKey(query);
|
|
14010
|
-
return groupsList.find(
|
|
14011
|
-
(g) => g.id === query || g.slug === query || g.name === query || normalizeKey(g.id) === qNorm || normalizeKey(g.slug) === qNorm || normalizeKey(g.name) === qNorm
|
|
14012
|
-
);
|
|
14013
|
-
};
|
|
14014
|
-
for (const g of baseTree.groups || []) {
|
|
14015
|
-
groupsList.push({
|
|
14016
|
-
...g,
|
|
14017
|
-
items: (g.items || []).map((item) => ({ ...item }))
|
|
14018
|
-
});
|
|
14019
|
-
}
|
|
14020
|
-
for (const ug of effectivePrefs.groups || []) {
|
|
14021
|
-
const existing = findGroup(ug.slug || ug.name);
|
|
14022
|
-
if (!existing) {
|
|
14023
|
-
groupsList.push({
|
|
14024
|
-
id: ug.slug ? `group_${ug.slug}` : `group_${normalizeKey(ug.name)}`,
|
|
14025
|
-
name: ug.name,
|
|
14026
|
-
slug: ug.slug || normalizeKey(ug.name),
|
|
14027
|
-
icon: ug.icon || "Folder",
|
|
14028
|
-
defaultExpanded: ug.defaultExpanded ?? true,
|
|
14029
|
-
order: ug.order ?? 100,
|
|
14030
|
-
items: []
|
|
14031
|
-
});
|
|
14032
|
-
}
|
|
14033
|
-
}
|
|
14034
|
-
for (const itemOpt of effectivePrefs.items || []) {
|
|
14035
|
-
const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group;
|
|
14036
|
-
const groupKey = targetGroup || "Custom";
|
|
14037
|
-
let group = findGroup(groupKey);
|
|
14038
|
-
if (!group) {
|
|
14039
|
-
group = {
|
|
14040
|
-
id: `group_${normalizeKey(groupKey)}`,
|
|
14041
|
-
name: groupKey,
|
|
14042
|
-
slug: normalizeKey(groupKey),
|
|
14043
|
-
icon: "Folder",
|
|
14044
|
-
defaultExpanded: true,
|
|
14045
|
-
order: 99,
|
|
14046
|
-
items: []
|
|
14047
|
-
};
|
|
14048
|
-
groupsList.push(group);
|
|
14049
|
-
}
|
|
14050
|
-
const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
|
|
14051
|
-
for (const otherGroup of groupsList) {
|
|
14052
|
-
if (otherGroup !== group) {
|
|
14053
|
-
otherGroup.items = otherGroup.items.filter((i) => i.slug !== itemOpt.slug && i.id !== itemId);
|
|
14054
|
-
}
|
|
14055
|
-
}
|
|
14056
|
-
const existingIndex = group.items.findIndex((i) => i.id === itemId || i.slug === itemOpt.slug);
|
|
14057
|
-
const existingItem = existingIndex >= 0 ? group.items[existingIndex] : void 0;
|
|
14058
|
-
const col = schemas?.collections?.find((c) => c.slug === itemOpt.collection);
|
|
14059
|
-
const colViews = col?.views || [];
|
|
14060
|
-
const baseViews = existingItem?.views || [];
|
|
14061
|
-
const userViews = itemOpt.views || [];
|
|
14062
|
-
const userViewSlugs = new Set(userViews.map((v) => v.slug));
|
|
14063
|
-
let mergedViews = [
|
|
14064
|
-
...baseViews.filter((v) => !userViewSlugs.has(v.slug)),
|
|
14065
|
-
...userViews
|
|
14066
|
-
];
|
|
14067
|
-
if (mergedViews.length === 0 && itemOpt.collection) {
|
|
14068
|
-
if (colViews.length > 0) {
|
|
14069
|
-
mergedViews = [...colViews];
|
|
14070
|
-
} else {
|
|
14071
|
-
mergedViews = [
|
|
14072
|
-
{
|
|
14073
|
-
slug: "default",
|
|
14074
|
-
label: col?.label || col?.labels?.plural || itemOpt.label || "All Records",
|
|
14075
|
-
layout: "table",
|
|
14076
|
-
collection: itemOpt.collection
|
|
14077
|
-
}
|
|
14078
|
-
];
|
|
14079
|
-
}
|
|
14080
|
-
}
|
|
14081
|
-
mergedViews = mergedViews.map((v) => ({
|
|
14082
|
-
...v,
|
|
14083
|
-
collection: v.collection || itemOpt.collection || existingItem?.collection
|
|
14084
|
-
}));
|
|
14085
|
-
const compiledItem = {
|
|
14086
|
-
id: itemId,
|
|
14087
|
-
type: itemOpt.slug ? "workspace" : itemOpt.collection ? "collection" : itemOpt.global ? "global" : "workspace",
|
|
14088
|
-
slug: itemOpt.slug || itemOpt.collection || itemOpt.global || "item",
|
|
14089
|
-
label: itemOpt.label || itemOpt.slug || "Item",
|
|
14090
|
-
icon: itemOpt.icon || existingItem?.icon || col?.admin?.icon || "Briefcase",
|
|
14091
|
-
group: group.name,
|
|
14092
|
-
order: itemOpt.order ?? existingItem?.order ?? 100,
|
|
14093
|
-
views: mergedViews,
|
|
14094
|
-
collection: itemOpt.collection || existingItem?.collection,
|
|
14095
|
-
global: itemOpt.global || existingItem?.global,
|
|
14096
|
-
badge: itemOpt.badge || existingItem?.badge,
|
|
14097
|
-
access: itemOpt.access || existingItem?.access
|
|
14098
|
-
};
|
|
14099
|
-
if (existingIndex >= 0) {
|
|
14100
|
-
group.items[existingIndex] = compiledItem;
|
|
14101
|
-
} else {
|
|
14102
|
-
group.items.push(compiledItem);
|
|
14103
|
-
}
|
|
14104
|
-
}
|
|
14105
|
-
const allAvailableItems = [];
|
|
14106
|
-
const reconciledGroups = [];
|
|
14107
|
-
for (const group of groupsList) {
|
|
14108
|
-
const isGroupHidden = hiddenSet.has(group.id) || !!group.slug && hiddenSet.has(group.slug) || hiddenSet.has(group.name);
|
|
14109
|
-
if (!includeHidden && isGroupHidden) {
|
|
14110
|
-
continue;
|
|
14111
|
-
}
|
|
14112
|
-
const visibleItems = [];
|
|
14113
|
-
for (const item of group.items) {
|
|
14114
|
-
const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
|
|
14115
|
-
if (!includeHidden && isItemHidden) {
|
|
14116
|
-
continue;
|
|
14117
|
-
}
|
|
14118
|
-
const allViews = item.views || [];
|
|
14119
|
-
const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
|
|
14120
|
-
const compoundKey = `${item.slug}_${v.slug}`;
|
|
14121
|
-
return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
|
|
14122
|
-
});
|
|
14123
|
-
let isTombstone = false;
|
|
14124
|
-
if (schemas && schemas.collections && schemas.collections.length > 0) {
|
|
14125
|
-
if (item.collection && !knownCollectionSlugs.has(item.collection)) {
|
|
14126
|
-
isTombstone = true;
|
|
14127
|
-
}
|
|
14128
|
-
}
|
|
14129
|
-
if (schemas && schemas.globals && schemas.globals.length > 0) {
|
|
14130
|
-
if (item.global && !knownGlobalSlugs.has(item.global)) {
|
|
14131
|
-
isTombstone = true;
|
|
14132
|
-
}
|
|
14133
|
-
}
|
|
14134
|
-
const processedItem = {
|
|
14135
|
-
...item,
|
|
14136
|
-
views: visibleViews,
|
|
14137
|
-
...isTombstone ? { isTombstone: true } : {}
|
|
14138
|
-
};
|
|
14139
|
-
visibleItems.push(processedItem);
|
|
14140
|
-
allAvailableItems.push(processedItem);
|
|
14141
|
-
}
|
|
14142
|
-
const groupKey = group.slug || group.name;
|
|
14143
|
-
const itemOrderList = effectivePrefs.itemOrder?.[group.id] || effectivePrefs.itemOrder?.[groupKey];
|
|
14144
|
-
if (itemOrderList && itemOrderList.length > 0) {
|
|
14145
|
-
visibleItems.sort((a, b) => {
|
|
14146
|
-
const aIdx = itemOrderList.indexOf(a.id) >= 0 ? itemOrderList.indexOf(a.id) : a.slug ? itemOrderList.indexOf(a.slug) : -1;
|
|
14147
|
-
const bIdx = itemOrderList.indexOf(b.id) >= 0 ? itemOrderList.indexOf(b.id) : b.slug ? itemOrderList.indexOf(b.slug) : -1;
|
|
14148
|
-
if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
|
|
14149
|
-
if (aIdx >= 0) return -1;
|
|
14150
|
-
if (bIdx >= 0) return 1;
|
|
14151
|
-
return (a.order ?? 100) - (b.order ?? 100);
|
|
14152
|
-
});
|
|
14153
|
-
} else {
|
|
14154
|
-
visibleItems.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
14155
|
-
}
|
|
14156
|
-
reconciledGroups.push({
|
|
14157
|
-
...group,
|
|
14158
|
-
items: visibleItems
|
|
14159
|
-
});
|
|
14160
|
-
}
|
|
14161
|
-
if (effectivePrefs.groupOrder && effectivePrefs.groupOrder.length > 0) {
|
|
14162
|
-
reconciledGroups.sort((a, b) => {
|
|
14163
|
-
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);
|
|
14164
|
-
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);
|
|
14165
|
-
if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
|
|
14166
|
-
if (aIdx >= 0) return -1;
|
|
14167
|
-
if (bIdx >= 0) return 1;
|
|
14168
|
-
return (a.order ?? 100) - (b.order ?? 100);
|
|
14169
|
-
});
|
|
14170
|
-
} else {
|
|
14171
|
-
reconciledGroups.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
14172
|
-
}
|
|
14173
|
-
const reconciledUngrouped = (baseTree.ungrouped || []).filter((item) => {
|
|
14174
|
-
const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
|
|
14175
|
-
return includeHidden || !isItemHidden;
|
|
14176
|
-
}).map((item) => {
|
|
14177
|
-
const allViews = item.views || [];
|
|
14178
|
-
const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
|
|
14179
|
-
const compoundKey = `${item.slug}_${v.slug}`;
|
|
14180
|
-
return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
|
|
14181
|
-
});
|
|
14182
|
-
const processed = { ...item, views: visibleViews };
|
|
14183
|
-
allAvailableItems.push(processed);
|
|
14184
|
-
return processed;
|
|
14185
|
-
});
|
|
14186
|
-
const pinnedItems = [];
|
|
14187
|
-
for (const pin of effectivePrefs.pinned || []) {
|
|
14188
|
-
const match = allAvailableItems.find((i) => i.slug === pin.slug || i.id === pin.slug);
|
|
14189
|
-
if (match) {
|
|
14190
|
-
pinnedItems.push({
|
|
14191
|
-
...match,
|
|
14192
|
-
id: `pin_${match.id}`,
|
|
14193
|
-
label: pin.label || match.label,
|
|
14194
|
-
icon: pin.icon || match.icon
|
|
14195
|
-
});
|
|
14196
|
-
} else {
|
|
14197
|
-
pinnedItems.push({
|
|
14198
|
-
id: `pin_${pin.slug}`,
|
|
14199
|
-
type: pin.type,
|
|
14200
|
-
slug: pin.slug,
|
|
14201
|
-
label: pin.label || pin.slug,
|
|
14202
|
-
icon: pin.icon || "Star",
|
|
14203
|
-
order: 0,
|
|
14204
|
-
views: []
|
|
14205
|
-
});
|
|
14206
|
-
}
|
|
14207
|
-
}
|
|
14208
|
-
return {
|
|
14209
|
-
groups: reconciledGroups,
|
|
14210
|
-
ungrouped: reconciledUngrouped,
|
|
14211
|
-
pinnedItems
|
|
14212
|
-
};
|
|
14213
|
-
}
|
|
14214
|
-
|
|
14215
|
-
// src/utils/navigation.ts
|
|
14216
|
-
var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
|
|
14217
|
-
"collections",
|
|
14218
|
-
"globals",
|
|
14219
|
-
"setup",
|
|
14220
|
-
"api",
|
|
14221
|
-
"login",
|
|
14222
|
-
"logout",
|
|
14223
|
-
"settings",
|
|
14224
|
-
"admin",
|
|
14225
|
-
"preferences",
|
|
14226
|
-
"auth"
|
|
14227
|
-
]);
|
|
14228
|
-
function assertValidNavigationSlugs(navigation) {
|
|
14229
|
-
for (const item of navigation) {
|
|
14230
|
-
if (item.slug) {
|
|
14231
|
-
const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
|
|
14232
|
-
if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
|
|
14233
|
-
throw new Error(
|
|
14234
|
-
`[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.`
|
|
14235
|
-
);
|
|
14236
|
-
}
|
|
14237
|
-
}
|
|
14238
|
-
}
|
|
14239
|
-
}
|
|
14240
|
-
function resolveGroupMeta(groupProp, defaultGroupName = "Collections") {
|
|
14241
|
-
if (!groupProp) {
|
|
14242
|
-
return { name: defaultGroupName };
|
|
14243
|
-
}
|
|
14244
|
-
if (typeof groupProp === "string") {
|
|
14245
|
-
return { name: groupProp };
|
|
14246
|
-
}
|
|
14247
|
-
return { name: groupProp.name, meta: groupProp };
|
|
14248
|
-
}
|
|
14249
|
-
function compileNavigation(config) {
|
|
14250
|
-
const collections = config.collections || [];
|
|
14251
|
-
const globals = config.globals || [];
|
|
14252
|
-
const explicitNav = config.admin?.navigation || [];
|
|
14253
|
-
assertValidNavigationSlugs(explicitNav);
|
|
14254
|
-
const collectionsBySlug = /* @__PURE__ */ new Map();
|
|
14255
|
-
for (const col of collections) {
|
|
14256
|
-
collectionsBySlug.set(col.slug, col);
|
|
14257
|
-
}
|
|
14258
|
-
const globalsBySlug = /* @__PURE__ */ new Map();
|
|
14259
|
-
for (const gl of globals) {
|
|
14260
|
-
globalsBySlug.set(gl.slug, gl);
|
|
14261
|
-
}
|
|
14262
|
-
const mentionedCollections = /* @__PURE__ */ new Set();
|
|
14263
|
-
const mentionedGlobals = /* @__PURE__ */ new Set();
|
|
14264
|
-
const injectedViewsByCollection = /* @__PURE__ */ new Map();
|
|
14265
|
-
for (const item of explicitNav) {
|
|
14266
|
-
if (item.addToCollection) {
|
|
14267
|
-
const existing = injectedViewsByCollection.get(item.addToCollection) || [];
|
|
14268
|
-
if (item.views && item.views.length > 0) {
|
|
14269
|
-
injectedViewsByCollection.set(item.addToCollection, [...existing, ...item.views]);
|
|
14270
|
-
}
|
|
14271
|
-
}
|
|
14272
|
-
if (item.collection) mentionedCollections.add(item.collection);
|
|
14273
|
-
if (item.global) mentionedGlobals.add(item.global);
|
|
14274
|
-
}
|
|
14275
|
-
const groupMetadataMap = /* @__PURE__ */ new Map();
|
|
14276
|
-
const itemsByGroup = /* @__PURE__ */ new Map();
|
|
14277
|
-
const ungroupedItems = [];
|
|
14278
|
-
const registerItem = (groupName, item) => {
|
|
14279
|
-
if (!groupName) {
|
|
14280
|
-
ungroupedItems.push(item);
|
|
14281
|
-
} else {
|
|
14282
|
-
const list = itemsByGroup.get(groupName) || [];
|
|
14283
|
-
list.push(item);
|
|
14284
|
-
itemsByGroup.set(groupName, list);
|
|
14285
|
-
}
|
|
14286
|
-
};
|
|
14287
|
-
for (const item of explicitNav) {
|
|
14288
|
-
if (item.addToCollection && !item.slug && !item.collection && !item.global && !item.dashboard && !item.href) {
|
|
14289
|
-
continue;
|
|
14290
|
-
}
|
|
14291
|
-
const { name: groupName, meta: groupMeta } = resolveGroupMeta(item.group, void 0);
|
|
14292
|
-
if (groupName && groupMeta) {
|
|
14293
|
-
groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
|
|
14294
|
-
}
|
|
14295
|
-
if (item.dashboard) {
|
|
14296
|
-
registerItem(groupName, {
|
|
14297
|
-
id: "dashboard",
|
|
14298
|
-
type: "dashboard",
|
|
14299
|
-
slug: "dashboard",
|
|
14300
|
-
label: item.label || "Dashboard",
|
|
14301
|
-
icon: item.icon || "LayoutDashboard",
|
|
14302
|
-
group: groupName,
|
|
14303
|
-
order: item.order ?? 0,
|
|
14304
|
-
views: [],
|
|
14305
|
-
access: item.access
|
|
14306
|
-
});
|
|
14307
|
-
continue;
|
|
14308
|
-
}
|
|
14309
|
-
if (item.collection) {
|
|
14310
|
-
const col = collectionsBySlug.get(item.collection);
|
|
14311
|
-
const combinedViews = [
|
|
14312
|
-
...col?.views || [],
|
|
14313
|
-
...injectedViewsByCollection.get(item.collection) || [],
|
|
14314
|
-
...item.views || []
|
|
14315
|
-
];
|
|
14316
|
-
registerItem(groupName, {
|
|
14317
|
-
id: item.slug || `collection_${item.collection}`,
|
|
14318
|
-
type: item.slug ? "workspace" : "collection",
|
|
14319
|
-
slug: item.slug || item.collection,
|
|
14320
|
-
collection: item.collection,
|
|
14321
|
-
label: item.label || col?.labels?.plural || item.collection,
|
|
14322
|
-
icon: item.icon || col?.admin?.icon || "Folder",
|
|
14323
|
-
group: groupName,
|
|
14324
|
-
order: item.order ?? col?.admin?.order ?? 100,
|
|
14325
|
-
views: combinedViews,
|
|
14326
|
-
badge: item.badge,
|
|
14327
|
-
access: item.access || col?.access?.read
|
|
14328
|
-
});
|
|
14329
|
-
continue;
|
|
14330
|
-
}
|
|
14331
|
-
if (item.global) {
|
|
14332
|
-
const gl = globalsBySlug.get(item.global);
|
|
14333
|
-
registerItem(groupName, {
|
|
14334
|
-
id: item.slug || `global_${item.global}`,
|
|
14335
|
-
type: "global",
|
|
14336
|
-
slug: item.slug || item.global,
|
|
14337
|
-
global: item.global,
|
|
14338
|
-
label: item.label || gl?.label || item.global,
|
|
14339
|
-
icon: item.icon || "Settings",
|
|
14340
|
-
group: groupName,
|
|
14341
|
-
order: item.order ?? 100,
|
|
14342
|
-
views: [],
|
|
14343
|
-
badge: item.badge,
|
|
14344
|
-
access: item.access
|
|
14345
|
-
});
|
|
14346
|
-
continue;
|
|
14347
|
-
}
|
|
14348
|
-
if (item.href) {
|
|
14349
|
-
registerItem(groupName, {
|
|
14350
|
-
id: `link_${item.slug || item.href}`,
|
|
14351
|
-
type: "link",
|
|
14352
|
-
slug: item.slug || "link",
|
|
14353
|
-
label: item.label || "Link",
|
|
14354
|
-
href: item.href,
|
|
14355
|
-
icon: item.icon || "ExternalLink",
|
|
14356
|
-
group: groupName,
|
|
14357
|
-
order: item.order ?? 100,
|
|
14358
|
-
views: [],
|
|
14359
|
-
badge: item.badge,
|
|
14360
|
-
access: item.access
|
|
14361
|
-
});
|
|
14362
|
-
continue;
|
|
14363
|
-
}
|
|
14364
|
-
const workspaceSlug = item.slug || "workspace";
|
|
14365
|
-
registerItem(groupName, {
|
|
14366
|
-
id: `workspace_${workspaceSlug}`,
|
|
14367
|
-
type: "workspace",
|
|
14368
|
-
slug: workspaceSlug,
|
|
14369
|
-
label: item.label || workspaceSlug,
|
|
14370
|
-
icon: item.icon || "Briefcase",
|
|
14371
|
-
group: groupName,
|
|
14372
|
-
order: item.order ?? 100,
|
|
14373
|
-
views: item.views || [],
|
|
14374
|
-
badge: item.badge,
|
|
14375
|
-
access: item.access
|
|
14376
|
-
});
|
|
14377
|
-
}
|
|
14378
|
-
for (const col of collections) {
|
|
14379
|
-
if (mentionedCollections.has(col.slug)) continue;
|
|
14380
|
-
if (col.admin?.hidden) continue;
|
|
14381
|
-
const groupProp = col.admin?.group;
|
|
14382
|
-
const { name: groupName, meta: groupMeta } = resolveGroupMeta(groupProp, "Collections");
|
|
14383
|
-
if (groupMeta) {
|
|
14384
|
-
groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
|
|
14385
|
-
}
|
|
14386
|
-
const combinedViews = [
|
|
14387
|
-
...col.views || [],
|
|
14388
|
-
...injectedViewsByCollection.get(col.slug) || []
|
|
14389
|
-
];
|
|
14390
|
-
registerItem(groupName, {
|
|
14391
|
-
id: `collection_${col.slug}`,
|
|
14392
|
-
type: "collection",
|
|
14393
|
-
slug: col.slug,
|
|
14394
|
-
collection: col.slug,
|
|
14395
|
-
label: col.labels?.plural || col.slug,
|
|
14396
|
-
icon: col.admin?.icon || "Folder",
|
|
14397
|
-
group: groupName,
|
|
14398
|
-
order: col?.admin?.order ?? 100,
|
|
14399
|
-
views: combinedViews,
|
|
14400
|
-
access: col.access?.read
|
|
14401
|
-
});
|
|
14402
|
-
}
|
|
14403
|
-
for (const gl of globals) {
|
|
14404
|
-
if (mentionedGlobals.has(gl.slug)) continue;
|
|
14405
|
-
const groupName = "Configuration";
|
|
14406
|
-
registerItem(groupName, {
|
|
14407
|
-
id: `global_${gl.slug}`,
|
|
14408
|
-
type: "global",
|
|
14409
|
-
slug: gl.slug,
|
|
14410
|
-
global: gl.slug,
|
|
14411
|
-
label: gl.label || gl.slug,
|
|
14412
|
-
icon: "Settings",
|
|
14413
|
-
group: groupName,
|
|
14414
|
-
order: 100,
|
|
14415
|
-
views: []
|
|
14416
|
-
});
|
|
14417
|
-
}
|
|
14418
|
-
const sortAndSpliceItems = (items) => {
|
|
14419
|
-
const sorted = [...items].sort((a, b) => a.order - b.order);
|
|
14420
|
-
const explicitMap = /* @__PURE__ */ new Map();
|
|
14421
|
-
for (const opt of explicitNav) {
|
|
14422
|
-
if (opt.slug) explicitMap.set(opt.slug, opt);
|
|
14423
|
-
if (opt.collection) explicitMap.set(opt.collection, opt);
|
|
14424
|
-
if (opt.global) explicitMap.set(opt.global, opt);
|
|
14425
|
-
}
|
|
14426
|
-
for (const opt of explicitNav) {
|
|
14427
|
-
const itemKey = opt.slug || opt.collection || opt.global;
|
|
14428
|
-
if (!itemKey) continue;
|
|
14429
|
-
if (opt.after) {
|
|
14430
|
-
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14431
|
-
const targetIdx = sorted.findIndex((i) => i.slug === opt.after);
|
|
14432
|
-
if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
|
|
14433
|
-
const [moved] = sorted.splice(itemIdx, 1);
|
|
14434
|
-
const newTargetIdx = sorted.findIndex((i) => i.slug === opt.after);
|
|
14435
|
-
sorted.splice(newTargetIdx + 1, 0, moved);
|
|
14436
|
-
}
|
|
14437
|
-
} else if (opt.before) {
|
|
14438
|
-
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14439
|
-
const targetIdx = sorted.findIndex((i) => i.slug === opt.before);
|
|
14440
|
-
if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
|
|
14441
|
-
const [moved] = sorted.splice(itemIdx, 1);
|
|
14442
|
-
const newTargetIdx = sorted.findIndex((i) => i.slug === opt.before);
|
|
14443
|
-
sorted.splice(newTargetIdx, 0, moved);
|
|
14444
|
-
}
|
|
14445
|
-
} else if (opt.position === "first") {
|
|
14446
|
-
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14447
|
-
if (itemIdx > 0) {
|
|
14448
|
-
const [moved] = sorted.splice(itemIdx, 1);
|
|
14449
|
-
sorted.unshift(moved);
|
|
14450
|
-
}
|
|
14451
|
-
} else if (opt.position === "last") {
|
|
14452
|
-
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14453
|
-
if (itemIdx !== -1 && itemIdx < sorted.length - 1) {
|
|
14454
|
-
const [moved] = sorted.splice(itemIdx, 1);
|
|
14455
|
-
sorted.push(moved);
|
|
14307
|
+
spec.paths["/api/media/{filename}"] = {
|
|
14308
|
+
get: {
|
|
14309
|
+
tags: ["Media"],
|
|
14310
|
+
summary: "Serve a stored file",
|
|
14311
|
+
security: [],
|
|
14312
|
+
parameters: [
|
|
14313
|
+
{
|
|
14314
|
+
name: "filename",
|
|
14315
|
+
in: "path",
|
|
14316
|
+
required: true,
|
|
14317
|
+
schema: { type: "string" }
|
|
14318
|
+
}
|
|
14319
|
+
],
|
|
14320
|
+
responses: {
|
|
14321
|
+
200: { description: "Stored file bytes" },
|
|
14322
|
+
404: { description: "File not found" }
|
|
14456
14323
|
}
|
|
14457
14324
|
}
|
|
14458
|
-
}
|
|
14459
|
-
return sorted;
|
|
14460
|
-
};
|
|
14461
|
-
const compiledGroups = [];
|
|
14462
|
-
for (const [groupName, rawItems] of itemsByGroup.entries()) {
|
|
14463
|
-
const meta = groupMetadataMap.get(groupName);
|
|
14464
|
-
const sortedItems = sortAndSpliceItems(rawItems);
|
|
14465
|
-
compiledGroups.push({
|
|
14466
|
-
id: `group_${groupName.toLowerCase().replace(/\s+/g, "_")}`,
|
|
14467
|
-
name: groupName,
|
|
14468
|
-
slug: meta?.slug || groupName.toLowerCase().replace(/\s+/g, "-"),
|
|
14469
|
-
icon: meta?.icon,
|
|
14470
|
-
defaultExpanded: meta?.defaultExpanded ?? true,
|
|
14471
|
-
order: meta?.order ?? 100,
|
|
14472
|
-
items: sortedItems
|
|
14473
|
-
});
|
|
14325
|
+
};
|
|
14474
14326
|
}
|
|
14475
|
-
|
|
14327
|
+
return spec;
|
|
14328
|
+
}
|
|
14329
|
+
function collectionToSchema(collection) {
|
|
14330
|
+
const { properties, required } = fieldsToProperties(collection.fields);
|
|
14476
14331
|
return {
|
|
14477
|
-
|
|
14478
|
-
|
|
14332
|
+
type: "object",
|
|
14333
|
+
properties: {
|
|
14334
|
+
id: { type: "string" },
|
|
14335
|
+
createdAt: { type: "string", format: "date-time" },
|
|
14336
|
+
updatedAt: { type: "string", format: "date-time" },
|
|
14337
|
+
...collection.workflow ? { _workflow: { $ref: "#/components/schemas/WorkflowMetadata" } } : {},
|
|
14338
|
+
...properties
|
|
14339
|
+
},
|
|
14340
|
+
required: ["id", ...required]
|
|
14479
14341
|
};
|
|
14480
14342
|
}
|
|
14481
|
-
function
|
|
14482
|
-
const
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
14343
|
+
function globalToSchema(global) {
|
|
14344
|
+
const { properties, required } = fieldsToProperties(global.fields);
|
|
14345
|
+
return {
|
|
14346
|
+
type: "object",
|
|
14347
|
+
properties,
|
|
14348
|
+
required
|
|
14349
|
+
};
|
|
14350
|
+
}
|
|
14351
|
+
function fieldsToProperties(fields) {
|
|
14352
|
+
const props = {};
|
|
14353
|
+
const required = [];
|
|
14354
|
+
for (const field of fields) {
|
|
14355
|
+
if (field.type === "row") {
|
|
14356
|
+
const nested = fieldsToProperties(field.fields || []);
|
|
14357
|
+
Object.assign(props, nested.properties);
|
|
14358
|
+
required.push(...nested.required);
|
|
14359
|
+
continue;
|
|
14360
|
+
}
|
|
14361
|
+
if (!field.name) continue;
|
|
14362
|
+
props[field.name] = fieldToSchema(field);
|
|
14363
|
+
if (field.required) {
|
|
14364
|
+
required.push(field.name);
|
|
14365
|
+
}
|
|
14486
14366
|
}
|
|
14487
|
-
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
|
|
14491
|
-
|
|
14367
|
+
return { properties: props, required };
|
|
14368
|
+
}
|
|
14369
|
+
function fieldToSchema(field) {
|
|
14370
|
+
let schema;
|
|
14371
|
+
switch (field.type) {
|
|
14372
|
+
case "text":
|
|
14373
|
+
case "textarea":
|
|
14374
|
+
case "email":
|
|
14375
|
+
schema = { type: "string" };
|
|
14376
|
+
break;
|
|
14377
|
+
case "url":
|
|
14378
|
+
schema = {
|
|
14379
|
+
oneOf: [
|
|
14380
|
+
{ type: "string" },
|
|
14381
|
+
{ type: "object", additionalProperties: true }
|
|
14382
|
+
]
|
|
14383
|
+
};
|
|
14384
|
+
break;
|
|
14385
|
+
case "icon":
|
|
14386
|
+
schema = { type: "string" };
|
|
14387
|
+
break;
|
|
14388
|
+
case "number":
|
|
14389
|
+
schema = { type: "number" };
|
|
14390
|
+
break;
|
|
14391
|
+
case "money":
|
|
14392
|
+
schema = { type: "integer", description: "Amount in minor units (for example kobo or cents)." };
|
|
14393
|
+
break;
|
|
14394
|
+
case "boolean":
|
|
14395
|
+
schema = { type: "boolean" };
|
|
14396
|
+
break;
|
|
14397
|
+
case "date":
|
|
14398
|
+
schema = { type: "string", format: "date" };
|
|
14399
|
+
break;
|
|
14400
|
+
case "datetime":
|
|
14401
|
+
schema = { type: "string", format: "date-time" };
|
|
14402
|
+
break;
|
|
14403
|
+
case "time":
|
|
14404
|
+
schema = { type: "string", format: "time" };
|
|
14405
|
+
break;
|
|
14406
|
+
case "select":
|
|
14407
|
+
case "radio":
|
|
14408
|
+
schema = {
|
|
14409
|
+
type: "string",
|
|
14410
|
+
enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0
|
|
14411
|
+
};
|
|
14412
|
+
break;
|
|
14413
|
+
case "multiSelect":
|
|
14414
|
+
schema = {
|
|
14415
|
+
type: "array",
|
|
14416
|
+
items: {
|
|
14417
|
+
type: "string",
|
|
14418
|
+
enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0
|
|
14419
|
+
}
|
|
14420
|
+
};
|
|
14421
|
+
break;
|
|
14422
|
+
case "relationship":
|
|
14423
|
+
case "image": {
|
|
14424
|
+
const valueSchema = {
|
|
14425
|
+
type: "string",
|
|
14426
|
+
description: `ID of a ${field.relationTo} record`
|
|
14427
|
+
};
|
|
14428
|
+
schema = field.hasMany ? { type: "array", items: valueSchema } : valueSchema;
|
|
14429
|
+
break;
|
|
14492
14430
|
}
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
}
|
|
14431
|
+
case "join":
|
|
14432
|
+
schema = {
|
|
14433
|
+
type: "array",
|
|
14434
|
+
readOnly: true,
|
|
14435
|
+
items: { type: "object", additionalProperties: true }
|
|
14436
|
+
};
|
|
14437
|
+
break;
|
|
14438
|
+
case "object": {
|
|
14439
|
+
const { properties, required } = fieldsToProperties(field.fields || []);
|
|
14440
|
+
schema = { type: "object", properties, required };
|
|
14441
|
+
break;
|
|
14442
|
+
}
|
|
14443
|
+
case "array": {
|
|
14444
|
+
const { properties, required } = fieldsToProperties(field.fields || []);
|
|
14445
|
+
schema = {
|
|
14446
|
+
type: "array",
|
|
14447
|
+
items: { type: "object", properties, required }
|
|
14448
|
+
};
|
|
14449
|
+
break;
|
|
14503
14450
|
}
|
|
14451
|
+
case "json":
|
|
14452
|
+
case "richText":
|
|
14453
|
+
schema = { type: "object", additionalProperties: true };
|
|
14454
|
+
break;
|
|
14455
|
+
case "blocks":
|
|
14456
|
+
schema = {
|
|
14457
|
+
type: "array",
|
|
14458
|
+
items: {
|
|
14459
|
+
oneOf: field.blocks?.map((block) => {
|
|
14460
|
+
const { properties, required } = fieldsToProperties(block.fields);
|
|
14461
|
+
return {
|
|
14462
|
+
type: "object",
|
|
14463
|
+
properties: {
|
|
14464
|
+
blockType: { type: "string", enum: [block.slug] },
|
|
14465
|
+
...properties
|
|
14466
|
+
},
|
|
14467
|
+
required: ["blockType", ...required]
|
|
14468
|
+
};
|
|
14469
|
+
})
|
|
14470
|
+
}
|
|
14471
|
+
};
|
|
14472
|
+
break;
|
|
14473
|
+
default:
|
|
14474
|
+
schema = { type: "string" };
|
|
14504
14475
|
}
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14476
|
+
if (field.label) schema.description = field.label;
|
|
14477
|
+
return schema;
|
|
14478
|
+
}
|
|
14479
|
+
|
|
14480
|
+
// src/utils/swagger.ts
|
|
14481
|
+
function getSwaggerHtml(specUrl) {
|
|
14482
|
+
return `
|
|
14483
|
+
<!DOCTYPE html>
|
|
14484
|
+
<html lang="en">
|
|
14485
|
+
<head>
|
|
14486
|
+
<meta charset="utf-8" />
|
|
14487
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
14488
|
+
<meta name="description" content="SwaggerUI" />
|
|
14489
|
+
<title>Dyrected API Documentation</title>
|
|
14490
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
|
14491
|
+
</head>
|
|
14492
|
+
<body>
|
|
14493
|
+
<div id="swagger-ui"></div>
|
|
14494
|
+
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" charset="UTF-8"></script>
|
|
14495
|
+
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js" charset="UTF-8"></script>
|
|
14496
|
+
<script>
|
|
14497
|
+
window.onload = () => {
|
|
14498
|
+
// Forward the apikey query param when loading the spec and making API calls
|
|
14499
|
+
const params = new URLSearchParams(window.location.search);
|
|
14500
|
+
const apiKey = params.get('apikey');
|
|
14501
|
+
// Resolve the spec relative to this docs page so any mount prefix
|
|
14502
|
+
// (e.g. "/dyrected/api/docs") is preserved. Falls back to an explicit URL
|
|
14503
|
+
// when one is supplied by the caller.
|
|
14504
|
+
const explicitSpecUrl = ${JSON.stringify(specUrl ?? "")};
|
|
14505
|
+
const specUrl = explicitSpecUrl || (window.location.pathname.replace(/\\/docs\\/?$/, '') + '/openapi.json');
|
|
14506
|
+
const specUrlWithKey = apiKey ? specUrl + '?apikey=' + encodeURIComponent(apiKey) : specUrl;
|
|
14507
|
+
|
|
14508
|
+
window.ui = SwaggerUIBundle({
|
|
14509
|
+
url: specUrlWithKey,
|
|
14510
|
+
dom_id: '#swagger-ui',
|
|
14511
|
+
presets: [
|
|
14512
|
+
SwaggerUIBundle.presets.apis,
|
|
14513
|
+
SwaggerUIStandalonePreset
|
|
14514
|
+
],
|
|
14515
|
+
layout: "BaseLayout",
|
|
14516
|
+
deepLinking: true,
|
|
14517
|
+
showExtensions: true,
|
|
14518
|
+
showCommonExtensions: true,
|
|
14519
|
+
// Inject x-api-key header on every request made from the Swagger UI
|
|
14520
|
+
requestInterceptor: (request) => {
|
|
14521
|
+
if (apiKey) {
|
|
14522
|
+
request.headers['x-api-key'] = apiKey;
|
|
14523
|
+
}
|
|
14524
|
+
return request;
|
|
14525
|
+
}
|
|
14526
|
+
});
|
|
14508
14527
|
};
|
|
14528
|
+
</script>
|
|
14529
|
+
</body>
|
|
14530
|
+
</html>
|
|
14531
|
+
`;
|
|
14509
14532
|
}
|
|
14510
14533
|
|
|
14511
14534
|
// src/router.ts
|
|
@@ -15506,10 +15529,11 @@ function registerRoutes(app, config) {
|
|
|
15506
15529
|
app.post(`${path}/media`, (c) => controller.create(c));
|
|
15507
15530
|
app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
|
|
15508
15531
|
app.post(`${path}/aggregate`, (c) => controller.aggregate(c));
|
|
15509
|
-
|
|
15532
|
+
const allViews = resolveAllCollectionViews(collection, config);
|
|
15533
|
+
if (allViews.length) {
|
|
15510
15534
|
app.post(`${path}/views/:viewSlug/actions/:action`, optionalAuth(config), (c) => controller.runViewAction(c));
|
|
15511
15535
|
}
|
|
15512
|
-
if (
|
|
15536
|
+
if (allViews.length || collection.actions?.length) {
|
|
15513
15537
|
app.post(`${path}/actions/:action`, optionalAuth(config), (c) => controller.runViewAction(c));
|
|
15514
15538
|
}
|
|
15515
15539
|
if (collection.audit) {
|