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