@harborclient/team-hub 0.4.5 → 0.4.6
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/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +12 -10
- package/dist/cli.js +1915 -305
- package/dist/cli.js.map +1 -1
- package/package.json +24 -15
package/dist/cli.js
CHANGED
|
@@ -404,6 +404,7 @@ var ENVIRONMENTS_COLLECTION = "environments";
|
|
|
404
404
|
var SNIPPETS_COLLECTION = "snippets";
|
|
405
405
|
var FOLDERS_COLLECTION = "folders";
|
|
406
406
|
var REQUESTS_COLLECTION = "requests";
|
|
407
|
+
var DOCUMENTS_COLLECTION = "documents";
|
|
407
408
|
var AUDIT_LOG_COLLECTION = "auditLog";
|
|
408
409
|
var LLM_USAGE_COLLECTION = "llmUsage";
|
|
409
410
|
var LLM_USAGE_LOG_COLLECTION = "llmUsageLog";
|
|
@@ -442,9 +443,25 @@ var firestoreConfigSchema = z2.object({
|
|
|
442
443
|
keyFilename: z2.string().trim().min(1).optional()
|
|
443
444
|
});
|
|
444
445
|
|
|
446
|
+
// src/db/sidebarMarker.ts
|
|
447
|
+
function serializeSidebarMarker(marker) {
|
|
448
|
+
if (marker == null) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
const trimmed = marker.trim();
|
|
452
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
453
|
+
}
|
|
454
|
+
function readSidebarMarker(value) {
|
|
455
|
+
if (typeof value !== "string") {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
const trimmed = value.trim();
|
|
459
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
460
|
+
}
|
|
461
|
+
|
|
445
462
|
// src/db/firestore/utils.ts
|
|
446
463
|
function parseAuditEntityType(value) {
|
|
447
|
-
if (value === "user" || value === "api_token" || value === "invitation" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
|
|
464
|
+
if (value === "user" || value === "api_token" || value === "invitation" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "document" || value === "run_result") {
|
|
448
465
|
return value;
|
|
449
466
|
}
|
|
450
467
|
throw new Error(`Invalid audit entity type: ${value}`);
|
|
@@ -538,7 +555,8 @@ function mapFirestoreCollection(id, data) {
|
|
|
538
555
|
updatedAt: data.updatedAt ?? data.createdAt,
|
|
539
556
|
createdByUserId: data.createdByUserId ?? null,
|
|
540
557
|
updatedByUserId: data.updatedByUserId ?? null,
|
|
541
|
-
deletionLocked: data.deletionLocked ?? false
|
|
558
|
+
deletionLocked: data.deletionLocked ?? false,
|
|
559
|
+
marker: readSidebarMarker(data.marker)
|
|
542
560
|
};
|
|
543
561
|
}
|
|
544
562
|
function mapFirestoreEnvironment(id, data) {
|
|
@@ -550,7 +568,8 @@ function mapFirestoreEnvironment(id, data) {
|
|
|
550
568
|
updatedAt: data.updatedAt ?? data.createdAt,
|
|
551
569
|
createdByUserId: data.createdByUserId ?? null,
|
|
552
570
|
updatedByUserId: data.updatedByUserId ?? null,
|
|
553
|
-
deletionLocked: data.deletionLocked ?? false
|
|
571
|
+
deletionLocked: data.deletionLocked ?? false,
|
|
572
|
+
marker: readSidebarMarker(data.marker)
|
|
554
573
|
};
|
|
555
574
|
}
|
|
556
575
|
function mapFirestoreSnippet(id, data) {
|
|
@@ -571,12 +590,14 @@ function mapFirestoreFolder(id, data) {
|
|
|
571
590
|
return {
|
|
572
591
|
id,
|
|
573
592
|
collectionId: data.collectionId,
|
|
593
|
+
parentFolderId: data.parentFolderId ?? null,
|
|
574
594
|
name: data.name,
|
|
575
595
|
sortOrder: data.sortOrder,
|
|
576
596
|
createdAt: data.createdAt,
|
|
577
597
|
updatedAt: data.updatedAt ?? data.createdAt,
|
|
578
598
|
createdByUserId: data.createdByUserId ?? null,
|
|
579
|
-
updatedByUserId: data.updatedByUserId ?? null
|
|
599
|
+
updatedByUserId: data.updatedByUserId ?? null,
|
|
600
|
+
marker: readSidebarMarker(data.marker)
|
|
580
601
|
};
|
|
581
602
|
}
|
|
582
603
|
function mapFirestoreRequest(id, data) {
|
|
@@ -599,7 +620,23 @@ function mapFirestoreRequest(id, data) {
|
|
|
599
620
|
createdAt: data.createdAt,
|
|
600
621
|
updatedAt: data.updatedAt,
|
|
601
622
|
createdByUserId: data.createdByUserId ?? null,
|
|
602
|
-
updatedByUserId: data.updatedByUserId ?? null
|
|
623
|
+
updatedByUserId: data.updatedByUserId ?? null,
|
|
624
|
+
marker: readSidebarMarker(data.marker)
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function mapFirestoreDocument(id, data) {
|
|
628
|
+
return {
|
|
629
|
+
id,
|
|
630
|
+
collectionId: data.collectionId,
|
|
631
|
+
folderId: data.folderId,
|
|
632
|
+
name: data.name,
|
|
633
|
+
content: data.content,
|
|
634
|
+
sortOrder: data.sortOrder,
|
|
635
|
+
createdAt: data.createdAt,
|
|
636
|
+
updatedAt: data.updatedAt,
|
|
637
|
+
createdByUserId: data.createdByUserId ?? null,
|
|
638
|
+
updatedByUserId: data.updatedByUserId ?? null,
|
|
639
|
+
marker: readSidebarMarker(data.marker)
|
|
603
640
|
};
|
|
604
641
|
}
|
|
605
642
|
function mapFirestoreRunResult(id, data) {
|
|
@@ -1450,7 +1487,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1450
1487
|
*
|
|
1451
1488
|
* @param actingUserId - User performing the update action.
|
|
1452
1489
|
*/
|
|
1453
|
-
async updateCollection(id, name, variables, headers, preRequestScript, postRequestScript, auth, actingUserId) {
|
|
1490
|
+
async updateCollection(id, name, variables, headers, preRequestScript, postRequestScript, auth, actingUserId, marker) {
|
|
1454
1491
|
const trimmedName = trimRequiredName(name, "Collection name");
|
|
1455
1492
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
1456
1493
|
const docRef = this.requireClient().collection(COLLECTIONS_COLLECTION).doc(id);
|
|
@@ -1459,6 +1496,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1459
1496
|
throw new Error("Collection not found");
|
|
1460
1497
|
}
|
|
1461
1498
|
const existing = snapshot.data();
|
|
1499
|
+
const serializedMarker = marker !== void 0 ? serializeSidebarMarker(marker) : existing.marker;
|
|
1462
1500
|
const updated = {
|
|
1463
1501
|
...existing,
|
|
1464
1502
|
name: trimmedName,
|
|
@@ -1468,7 +1506,8 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1468
1506
|
preRequestScript,
|
|
1469
1507
|
postRequestScript,
|
|
1470
1508
|
updatedAt,
|
|
1471
|
-
updatedByUserId: actingUserId
|
|
1509
|
+
updatedByUserId: actingUserId,
|
|
1510
|
+
...marker !== void 0 ? { marker: serializedMarker } : {}
|
|
1472
1511
|
};
|
|
1473
1512
|
await docRef.update({
|
|
1474
1513
|
name: trimmedName,
|
|
@@ -1478,7 +1517,8 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1478
1517
|
preRequestScript,
|
|
1479
1518
|
postRequestScript,
|
|
1480
1519
|
updatedAt,
|
|
1481
|
-
updatedByUserId: actingUserId
|
|
1520
|
+
updatedByUserId: actingUserId,
|
|
1521
|
+
...marker !== void 0 ? { marker: serializedMarker } : {}
|
|
1482
1522
|
});
|
|
1483
1523
|
await this.recordAuditEntry(actingUserId, "update", "collection", id);
|
|
1484
1524
|
return mapFirestoreCollection(id, updated);
|
|
@@ -1493,9 +1533,11 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1493
1533
|
await this.recordAuditEntry(actingUserId, "delete", "collection", id);
|
|
1494
1534
|
const client = this.requireClient();
|
|
1495
1535
|
const requestsSnap = await client.collection(REQUESTS_COLLECTION).where("collectionId", "==", id).get();
|
|
1536
|
+
const documentsSnap = await client.collection(DOCUMENTS_COLLECTION).where("collectionId", "==", id).get();
|
|
1496
1537
|
const foldersSnap = await client.collection(FOLDERS_COLLECTION).where("collectionId", "==", id).get();
|
|
1497
1538
|
const refs = [
|
|
1498
1539
|
...requestsSnap.docs.map((requestDoc) => requestDoc.ref),
|
|
1540
|
+
...documentsSnap.docs.map((documentDoc) => documentDoc.ref),
|
|
1499
1541
|
...foldersSnap.docs.map((folderDoc) => folderDoc.ref),
|
|
1500
1542
|
client.collection(COLLECTIONS_COLLECTION).doc(id)
|
|
1501
1543
|
];
|
|
@@ -1578,7 +1620,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1578
1620
|
*
|
|
1579
1621
|
* @param actingUserId - User performing the update action.
|
|
1580
1622
|
*/
|
|
1581
|
-
async updateEnvironment(id, name, variables, actingUserId) {
|
|
1623
|
+
async updateEnvironment(id, name, variables, actingUserId, marker) {
|
|
1582
1624
|
const trimmedName = trimRequiredName(name, "Environment name");
|
|
1583
1625
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
1584
1626
|
const docRef = this.requireClient().collection(ENVIRONMENTS_COLLECTION).doc(id);
|
|
@@ -1587,18 +1629,21 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1587
1629
|
throw new Error("Environment not found");
|
|
1588
1630
|
}
|
|
1589
1631
|
const existing = snapshot.data();
|
|
1632
|
+
const serializedMarker = marker !== void 0 ? serializeSidebarMarker(marker) : existing.marker;
|
|
1590
1633
|
const updated = {
|
|
1591
1634
|
...existing,
|
|
1592
1635
|
name: trimmedName,
|
|
1593
1636
|
variables,
|
|
1594
1637
|
updatedAt,
|
|
1595
|
-
updatedByUserId: actingUserId
|
|
1638
|
+
updatedByUserId: actingUserId,
|
|
1639
|
+
...marker !== void 0 ? { marker: serializedMarker } : {}
|
|
1596
1640
|
};
|
|
1597
1641
|
await docRef.update({
|
|
1598
1642
|
name: trimmedName,
|
|
1599
1643
|
variables,
|
|
1600
1644
|
updatedAt,
|
|
1601
|
-
updatedByUserId: actingUserId
|
|
1645
|
+
updatedByUserId: actingUserId,
|
|
1646
|
+
...marker !== void 0 ? { marker: serializedMarker } : {}
|
|
1602
1647
|
});
|
|
1603
1648
|
await this.recordAuditEntry(actingUserId, "update", "environment", id);
|
|
1604
1649
|
return mapFirestoreEnvironment(id, updated);
|
|
@@ -1811,6 +1856,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1811
1856
|
async saveRequest(input, actingUserId) {
|
|
1812
1857
|
const trimmedName = trimRequiredName(input.name, "Request name");
|
|
1813
1858
|
const folderId = input.folderId ?? null;
|
|
1859
|
+
const serializedMarker = serializeSidebarMarker(input.marker ?? null);
|
|
1814
1860
|
const now = /* @__PURE__ */ new Date();
|
|
1815
1861
|
const client = this.requireClient();
|
|
1816
1862
|
if (folderId != null) {
|
|
@@ -1843,6 +1889,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1843
1889
|
preRequestScript: input.preRequestScript,
|
|
1844
1890
|
postRequestScript: input.postRequestScript,
|
|
1845
1891
|
comment: input.comment,
|
|
1892
|
+
marker: serializedMarker,
|
|
1846
1893
|
updatedAt: now,
|
|
1847
1894
|
updatedByUserId: actingUserId
|
|
1848
1895
|
};
|
|
@@ -1860,6 +1907,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1860
1907
|
preRequestScript: input.preRequestScript,
|
|
1861
1908
|
postRequestScript: input.postRequestScript,
|
|
1862
1909
|
comment: input.comment,
|
|
1910
|
+
marker: serializedMarker,
|
|
1863
1911
|
updatedAt: now,
|
|
1864
1912
|
updatedByUserId: actingUserId
|
|
1865
1913
|
});
|
|
@@ -1884,6 +1932,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1884
1932
|
preRequestScript: input.preRequestScript,
|
|
1885
1933
|
postRequestScript: input.postRequestScript,
|
|
1886
1934
|
comment: input.comment,
|
|
1935
|
+
marker: serializedMarker,
|
|
1887
1936
|
sortOrder: maxOrder + 1,
|
|
1888
1937
|
createdAt: now,
|
|
1889
1938
|
updatedAt: now,
|
|
@@ -1937,14 +1986,21 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1937
1986
|
* @param name - Display name for the folder.
|
|
1938
1987
|
* @param actingUserId - User performing the create action.
|
|
1939
1988
|
*/
|
|
1940
|
-
async createFolder(collectionId, name, actingUserId) {
|
|
1989
|
+
async createFolder(collectionId, name, actingUserId, parentFolderId = null) {
|
|
1941
1990
|
const trimmedName = trimRequiredName(name, "Folder name");
|
|
1991
|
+
if (parentFolderId != null) {
|
|
1992
|
+
const parent = await this.findFolderById(parentFolderId);
|
|
1993
|
+
if (!parent || parent.collectionId !== collectionId) {
|
|
1994
|
+
throw new Error("Parent folder not found in collection");
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1942
1997
|
const id = randomUUID3();
|
|
1943
1998
|
const now = /* @__PURE__ */ new Date();
|
|
1944
1999
|
const existingFolders = await this.listFolders(collectionId);
|
|
1945
|
-
const maxOrder = existingFolders.reduce((max, folder) => Math.max(max, folder.sortOrder), -1);
|
|
2000
|
+
const maxOrder = existingFolders.filter((folder) => folder.parentFolderId === parentFolderId).reduce((max, folder) => Math.max(max, folder.sortOrder), -1);
|
|
1946
2001
|
const data = {
|
|
1947
2002
|
collectionId,
|
|
2003
|
+
parentFolderId,
|
|
1948
2004
|
name: trimmedName,
|
|
1949
2005
|
sortOrder: maxOrder + 1,
|
|
1950
2006
|
createdAt: now,
|
|
@@ -1963,7 +2019,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1963
2019
|
* @param name - New display name.
|
|
1964
2020
|
* @param actingUserId - User performing the rename action.
|
|
1965
2021
|
*/
|
|
1966
|
-
async renameFolder(id, name, actingUserId) {
|
|
2022
|
+
async renameFolder(id, name, actingUserId, marker) {
|
|
1967
2023
|
const trimmedName = trimRequiredName(name, "Folder name");
|
|
1968
2024
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
1969
2025
|
const docRef = this.requireClient().collection(FOLDERS_COLLECTION).doc(id);
|
|
@@ -1972,17 +2028,24 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1972
2028
|
throw new Error("Folder not found");
|
|
1973
2029
|
}
|
|
1974
2030
|
const existing = snapshot.data();
|
|
1975
|
-
|
|
2031
|
+
const serializedMarker = marker !== void 0 ? serializeSidebarMarker(marker) : existing.marker;
|
|
2032
|
+
await docRef.update({
|
|
2033
|
+
name: trimmedName,
|
|
2034
|
+
updatedAt,
|
|
2035
|
+
updatedByUserId: actingUserId,
|
|
2036
|
+
...marker !== void 0 ? { marker: serializedMarker } : {}
|
|
2037
|
+
});
|
|
1976
2038
|
await this.recordAuditEntry(actingUserId, "update", "folder", id);
|
|
1977
2039
|
return mapFirestoreFolder(id, {
|
|
1978
2040
|
...existing,
|
|
1979
2041
|
name: trimmedName,
|
|
1980
2042
|
updatedAt,
|
|
1981
|
-
updatedByUserId: actingUserId
|
|
2043
|
+
updatedByUserId: actingUserId,
|
|
2044
|
+
...marker !== void 0 ? { marker: serializedMarker } : {}
|
|
1982
2045
|
});
|
|
1983
2046
|
}
|
|
1984
2047
|
/**
|
|
1985
|
-
* Deletes a folder
|
|
2048
|
+
* Deletes a folder, its descendants, and their contents.
|
|
1986
2049
|
*
|
|
1987
2050
|
* @param id - Folder ID to delete.
|
|
1988
2051
|
* @param actingUserId - User performing the delete action.
|
|
@@ -1990,21 +2053,101 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1990
2053
|
async deleteFolder(id, actingUserId) {
|
|
1991
2054
|
await this.recordAuditEntry(actingUserId, "delete", "folder", id);
|
|
1992
2055
|
const client = this.requireClient();
|
|
1993
|
-
const
|
|
2056
|
+
const root = await this.findFolderById(id);
|
|
2057
|
+
if (!root) {
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
const folders = await this.listFolders(root.collectionId);
|
|
2061
|
+
const descendantIds = /* @__PURE__ */ new Set([id]);
|
|
2062
|
+
let changed = true;
|
|
2063
|
+
while (changed) {
|
|
2064
|
+
changed = false;
|
|
2065
|
+
for (const folder of folders) {
|
|
2066
|
+
if (folder.parentFolderId != null && descendantIds.has(folder.parentFolderId) && !descendantIds.has(folder.id)) {
|
|
2067
|
+
descendantIds.add(folder.id);
|
|
2068
|
+
changed = true;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
const ids = [...descendantIds];
|
|
2073
|
+
const requestSnapshots = await Promise.all(
|
|
2074
|
+
ids.map(
|
|
2075
|
+
(folderId) => client.collection(REQUESTS_COLLECTION).where("folderId", "==", folderId).get()
|
|
2076
|
+
)
|
|
2077
|
+
);
|
|
2078
|
+
const documentSnapshots = await Promise.all(
|
|
2079
|
+
ids.map(
|
|
2080
|
+
(folderId) => client.collection(DOCUMENTS_COLLECTION).where("folderId", "==", folderId).get()
|
|
2081
|
+
)
|
|
2082
|
+
);
|
|
1994
2083
|
const refs = [
|
|
1995
|
-
...
|
|
1996
|
-
|
|
2084
|
+
...requestSnapshots.flatMap((snapshot) => snapshot.docs.map((requestDoc) => requestDoc.ref)),
|
|
2085
|
+
...documentSnapshots.flatMap(
|
|
2086
|
+
(snapshot) => snapshot.docs.map((documentDoc) => documentDoc.ref)
|
|
2087
|
+
),
|
|
2088
|
+
...ids.map((folderId) => client.collection(FOLDERS_COLLECTION).doc(folderId))
|
|
1997
2089
|
];
|
|
1998
2090
|
await this.commitBatchedDeletes(refs);
|
|
1999
2091
|
}
|
|
2000
2092
|
/**
|
|
2001
|
-
*
|
|
2093
|
+
* Moves a folder to a new parent and optional sibling position.
|
|
2094
|
+
*
|
|
2095
|
+
* @param id - Folder ID to move.
|
|
2096
|
+
* @param parentFolderId - Destination parent, or null for collection root.
|
|
2097
|
+
* @param sortOrder - Optional zero-based destination sibling index.
|
|
2098
|
+
* @param actingUserId - User performing the move action.
|
|
2099
|
+
*/
|
|
2100
|
+
async moveFolder(id, parentFolderId, sortOrder, actingUserId) {
|
|
2101
|
+
const folder = await this.findFolderById(id);
|
|
2102
|
+
if (!folder) {
|
|
2103
|
+
throw new Error("Folder not found");
|
|
2104
|
+
}
|
|
2105
|
+
const folders = await this.listFolders(folder.collectionId);
|
|
2106
|
+
if (parentFolderId != null) {
|
|
2107
|
+
let ancestor = folders.find((entry) => entry.id === parentFolderId);
|
|
2108
|
+
if (!ancestor) {
|
|
2109
|
+
throw new Error("Parent folder not found in collection");
|
|
2110
|
+
}
|
|
2111
|
+
while (ancestor) {
|
|
2112
|
+
if (ancestor.id === id) {
|
|
2113
|
+
throw new Error("Cannot move a folder inside itself or its descendants");
|
|
2114
|
+
}
|
|
2115
|
+
ancestor = ancestor.parentFolderId == null ? void 0 : folders.find((entry) => entry.id === ancestor?.parentFolderId);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
const siblings = folders.filter((entry) => entry.id !== id && entry.parentFolderId === parentFolderId).sort(
|
|
2119
|
+
(left, right) => left.sortOrder - right.sortOrder || left.name.localeCompare(right.name)
|
|
2120
|
+
);
|
|
2121
|
+
const index = Math.max(0, Math.min(sortOrder ?? siblings.length, siblings.length));
|
|
2122
|
+
siblings.splice(index, 0, { ...folder, parentFolderId });
|
|
2123
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
2124
|
+
await this.requireClient().collection(FOLDERS_COLLECTION).doc(id).update({
|
|
2125
|
+
parentFolderId,
|
|
2126
|
+
updatedAt,
|
|
2127
|
+
updatedByUserId: actingUserId
|
|
2128
|
+
});
|
|
2129
|
+
await this.reorderFolders(
|
|
2130
|
+
folder.collectionId,
|
|
2131
|
+
parentFolderId,
|
|
2132
|
+
siblings.map((entry) => entry.id),
|
|
2133
|
+
actingUserId
|
|
2134
|
+
);
|
|
2135
|
+
return {
|
|
2136
|
+
...folder,
|
|
2137
|
+
parentFolderId,
|
|
2138
|
+
sortOrder: index,
|
|
2139
|
+
updatedAt,
|
|
2140
|
+
updatedByUserId: actingUserId
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Reorders sibling folders within a collection.
|
|
2002
2145
|
*
|
|
2003
2146
|
* @param collectionId - Collection containing the folders.
|
|
2004
2147
|
* @param orderedFolderIds - Folder IDs in desired order.
|
|
2005
2148
|
* @param actingUserId - User performing the reorder action.
|
|
2006
2149
|
*/
|
|
2007
|
-
async reorderFolders(collectionId, orderedFolderIds, actingUserId) {
|
|
2150
|
+
async reorderFolders(collectionId, parentFolderId, orderedFolderIds, actingUserId) {
|
|
2008
2151
|
const client = this.requireClient();
|
|
2009
2152
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
2010
2153
|
const batch = client.batch();
|
|
@@ -2019,6 +2162,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
2019
2162
|
}
|
|
2020
2163
|
await batch.commit();
|
|
2021
2164
|
await this.recordAuditEntry(actingUserId, "reorder", "folder", collectionId, {
|
|
2165
|
+
parentFolderId,
|
|
2022
2166
|
orderedFolderIds
|
|
2023
2167
|
});
|
|
2024
2168
|
}
|
|
@@ -2117,6 +2261,206 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
2117
2261
|
index
|
|
2118
2262
|
});
|
|
2119
2263
|
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Lists all documents in a collection.
|
|
2266
|
+
*
|
|
2267
|
+
* @param collectionId - Collection to query.
|
|
2268
|
+
*/
|
|
2269
|
+
async listDocuments(collectionId) {
|
|
2270
|
+
const snapshot = await this.requireClient().collection(DOCUMENTS_COLLECTION).where("collectionId", "==", collectionId).get();
|
|
2271
|
+
return snapshot.docs.map((doc) => mapFirestoreDocument(doc.id, doc.data())).sort((left, right) => {
|
|
2272
|
+
if (left.sortOrder !== right.sortOrder) {
|
|
2273
|
+
return left.sortOrder - right.sortOrder;
|
|
2274
|
+
}
|
|
2275
|
+
return left.name.localeCompare(right.name);
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Finds a document by id.
|
|
2280
|
+
*
|
|
2281
|
+
* @param id - Document identifier to look up.
|
|
2282
|
+
*/
|
|
2283
|
+
async findDocumentById(id) {
|
|
2284
|
+
const snapshot = await this.requireClient().collection(DOCUMENTS_COLLECTION).doc(id).get();
|
|
2285
|
+
if (!snapshot.exists) {
|
|
2286
|
+
return null;
|
|
2287
|
+
}
|
|
2288
|
+
return mapFirestoreDocument(id, snapshot.data());
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Inserts a new document or updates an existing one.
|
|
2292
|
+
*
|
|
2293
|
+
* @param input - Document fields to persist.
|
|
2294
|
+
* @param actingUserId - User performing the save action.
|
|
2295
|
+
*/
|
|
2296
|
+
async saveDocument(input, actingUserId) {
|
|
2297
|
+
const trimmedName = trimRequiredName(input.name, "Document name");
|
|
2298
|
+
const folderId = input.folderId ?? null;
|
|
2299
|
+
const serializedMarker = serializeSidebarMarker(input.marker ?? null);
|
|
2300
|
+
const now = /* @__PURE__ */ new Date();
|
|
2301
|
+
const client = this.requireClient();
|
|
2302
|
+
if (folderId != null) {
|
|
2303
|
+
const folderSnap = await client.collection(FOLDERS_COLLECTION).doc(folderId).get();
|
|
2304
|
+
if (!folderSnap.exists) {
|
|
2305
|
+
throw new Error("Folder not found");
|
|
2306
|
+
}
|
|
2307
|
+
const folder = folderSnap.data();
|
|
2308
|
+
if (folder.collectionId !== input.collectionId) {
|
|
2309
|
+
throw new Error("Folder not found");
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
if (input.id) {
|
|
2313
|
+
const docRef = client.collection(DOCUMENTS_COLLECTION).doc(input.id);
|
|
2314
|
+
const snapshot = await docRef.get();
|
|
2315
|
+
if (snapshot.exists) {
|
|
2316
|
+
const existing = snapshot.data();
|
|
2317
|
+
const updated = {
|
|
2318
|
+
...existing,
|
|
2319
|
+
collectionId: input.collectionId,
|
|
2320
|
+
folderId,
|
|
2321
|
+
name: trimmedName,
|
|
2322
|
+
content: input.content,
|
|
2323
|
+
marker: serializedMarker,
|
|
2324
|
+
updatedAt: now,
|
|
2325
|
+
updatedByUserId: actingUserId
|
|
2326
|
+
};
|
|
2327
|
+
await docRef.update({
|
|
2328
|
+
collectionId: input.collectionId,
|
|
2329
|
+
folderId,
|
|
2330
|
+
name: trimmedName,
|
|
2331
|
+
content: input.content,
|
|
2332
|
+
marker: serializedMarker,
|
|
2333
|
+
updatedAt: now,
|
|
2334
|
+
updatedByUserId: actingUserId
|
|
2335
|
+
});
|
|
2336
|
+
await this.recordAuditEntry(actingUserId, "update", "document", input.id);
|
|
2337
|
+
return mapFirestoreDocument(input.id, updated);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
const existingDocuments = await this.listDocuments(input.collectionId);
|
|
2341
|
+
const maxOrder = existingDocuments.filter((document) => document.folderId === folderId).reduce((max, document) => Math.max(max, document.sortOrder), -1);
|
|
2342
|
+
const id = randomUUID3();
|
|
2343
|
+
const data = {
|
|
2344
|
+
collectionId: input.collectionId,
|
|
2345
|
+
folderId,
|
|
2346
|
+
name: trimmedName,
|
|
2347
|
+
content: input.content,
|
|
2348
|
+
marker: serializedMarker,
|
|
2349
|
+
sortOrder: maxOrder + 1,
|
|
2350
|
+
createdAt: now,
|
|
2351
|
+
updatedAt: now,
|
|
2352
|
+
createdByUserId: actingUserId,
|
|
2353
|
+
updatedByUserId: actingUserId
|
|
2354
|
+
};
|
|
2355
|
+
await client.collection(DOCUMENTS_COLLECTION).doc(id).set(data);
|
|
2356
|
+
await this.recordAuditEntry(actingUserId, "create", "document", id);
|
|
2357
|
+
return mapFirestoreDocument(id, data);
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Deletes a document by ID.
|
|
2361
|
+
*
|
|
2362
|
+
* @param id - Document ID to delete.
|
|
2363
|
+
* @param actingUserId - User performing the delete action.
|
|
2364
|
+
*/
|
|
2365
|
+
async deleteDocument(id, actingUserId) {
|
|
2366
|
+
await this.recordAuditEntry(actingUserId, "delete", "document", id);
|
|
2367
|
+
await this.requireClient().collection(DOCUMENTS_COLLECTION).doc(id).delete();
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Reorders documents within a folder or at collection root.
|
|
2371
|
+
*
|
|
2372
|
+
* @param actingUserId - User performing the reorder action.
|
|
2373
|
+
*/
|
|
2374
|
+
async reorderDocuments(collectionId, folderId, orderedDocumentIds, actingUserId) {
|
|
2375
|
+
const client = this.requireClient();
|
|
2376
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
2377
|
+
const batch = client.batch();
|
|
2378
|
+
for (let index = 0; index < orderedDocumentIds.length; index++) {
|
|
2379
|
+
const docRef = client.collection(DOCUMENTS_COLLECTION).doc(orderedDocumentIds[index]);
|
|
2380
|
+
batch.update(docRef, {
|
|
2381
|
+
sortOrder: index,
|
|
2382
|
+
folderId,
|
|
2383
|
+
collectionId,
|
|
2384
|
+
updatedAt,
|
|
2385
|
+
updatedByUserId: actingUserId
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
await batch.commit();
|
|
2389
|
+
await this.recordAuditEntry(actingUserId, "reorder", "document", collectionId, {
|
|
2390
|
+
folderId,
|
|
2391
|
+
orderedDocumentIds
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Moves a document to another folder or collection root at a given index.
|
|
2396
|
+
*
|
|
2397
|
+
* @param actingUserId - User performing the move action.
|
|
2398
|
+
*/
|
|
2399
|
+
async moveDocument(documentId, folderId, index, actingUserId) {
|
|
2400
|
+
const client = this.requireClient();
|
|
2401
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
2402
|
+
const documentSnap = await client.collection(DOCUMENTS_COLLECTION).doc(documentId).get();
|
|
2403
|
+
if (!documentSnap.exists) {
|
|
2404
|
+
throw new Error("Document not found");
|
|
2405
|
+
}
|
|
2406
|
+
const document = mapFirestoreDocument(
|
|
2407
|
+
documentSnap.id,
|
|
2408
|
+
documentSnap.data()
|
|
2409
|
+
);
|
|
2410
|
+
const collectionId = document.collectionId;
|
|
2411
|
+
const oldFolderId = document.folderId;
|
|
2412
|
+
if (folderId != null) {
|
|
2413
|
+
const folderSnap = await client.collection(FOLDERS_COLLECTION).doc(folderId).get();
|
|
2414
|
+
if (!folderSnap.exists) {
|
|
2415
|
+
throw new Error("Folder not found");
|
|
2416
|
+
}
|
|
2417
|
+
const folder = folderSnap.data();
|
|
2418
|
+
if (folder.collectionId !== collectionId) {
|
|
2419
|
+
throw new Error("Folder not found");
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
const listInContainer = async (targetFolderId) => {
|
|
2423
|
+
const documents = await this.listDocuments(collectionId);
|
|
2424
|
+
return documents.filter((item) => item.folderId === targetFolderId).sort((left, right) => {
|
|
2425
|
+
if (left.sortOrder !== right.sortOrder) {
|
|
2426
|
+
return left.sortOrder - right.sortOrder;
|
|
2427
|
+
}
|
|
2428
|
+
return left.name.localeCompare(right.name);
|
|
2429
|
+
}).map((item) => item.id);
|
|
2430
|
+
};
|
|
2431
|
+
const reindexContainer = async (targetFolderId, orderedIds) => {
|
|
2432
|
+
const batch = client.batch();
|
|
2433
|
+
for (let sortIndex = 0; sortIndex < orderedIds.length; sortIndex++) {
|
|
2434
|
+
const docRef = client.collection(DOCUMENTS_COLLECTION).doc(orderedIds[sortIndex]);
|
|
2435
|
+
batch.update(docRef, {
|
|
2436
|
+
sortOrder: sortIndex,
|
|
2437
|
+
folderId: targetFolderId,
|
|
2438
|
+
updatedAt,
|
|
2439
|
+
updatedByUserId: actingUserId
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
await batch.commit();
|
|
2443
|
+
};
|
|
2444
|
+
if (oldFolderId === folderId) {
|
|
2445
|
+
const siblings = (await listInContainer(folderId)).filter((id) => id !== documentId);
|
|
2446
|
+
siblings.splice(index, 0, documentId);
|
|
2447
|
+
await reindexContainer(folderId, siblings);
|
|
2448
|
+
await this.recordAuditEntry(actingUserId, "move", "document", documentId, {
|
|
2449
|
+
folderId,
|
|
2450
|
+
index
|
|
2451
|
+
});
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
const oldIds = (await listInContainer(oldFolderId)).filter((id) => id !== documentId);
|
|
2455
|
+
await reindexContainer(oldFolderId, oldIds);
|
|
2456
|
+
const newIds = (await listInContainer(folderId)).filter((id) => id !== documentId);
|
|
2457
|
+
newIds.splice(index, 0, documentId);
|
|
2458
|
+
await reindexContainer(folderId, newIds);
|
|
2459
|
+
await this.recordAuditEntry(actingUserId, "move", "document", documentId, {
|
|
2460
|
+
folderId,
|
|
2461
|
+
index
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2120
2464
|
/**
|
|
2121
2465
|
* Returns monthly LLM usage for a user, or null when no usage has been recorded.
|
|
2122
2466
|
*
|
|
@@ -2441,7 +2785,7 @@ function parseAuditAction(value) {
|
|
|
2441
2785
|
throw new Error(`Invalid audit action: ${value}`);
|
|
2442
2786
|
}
|
|
2443
2787
|
function parseAuditEntityType2(value) {
|
|
2444
|
-
if (value === "user" || value === "api_token" || value === "invitation" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
|
|
2788
|
+
if (value === "user" || value === "api_token" || value === "invitation" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "document" || value === "run_result") {
|
|
2445
2789
|
return value;
|
|
2446
2790
|
}
|
|
2447
2791
|
throw new Error(`Invalid audit entity type: ${value}`);
|
|
@@ -2503,7 +2847,8 @@ function mapCollectionSqlRow(row) {
|
|
|
2503
2847
|
updatedAt: row.updated_at ?? row.created_at,
|
|
2504
2848
|
createdByUserId: row.created_by_user_id ?? null,
|
|
2505
2849
|
updatedByUserId: row.updated_by_user_id ?? null,
|
|
2506
|
-
deletionLocked: Boolean(row.deletion_locked)
|
|
2850
|
+
deletionLocked: Boolean(row.deletion_locked),
|
|
2851
|
+
marker: readSidebarMarker(row.marker)
|
|
2507
2852
|
};
|
|
2508
2853
|
}
|
|
2509
2854
|
function mapEnvironmentSqlRow(row) {
|
|
@@ -2515,7 +2860,8 @@ function mapEnvironmentSqlRow(row) {
|
|
|
2515
2860
|
updatedAt: row.updated_at ?? row.created_at,
|
|
2516
2861
|
createdByUserId: row.created_by_user_id ?? null,
|
|
2517
2862
|
updatedByUserId: row.updated_by_user_id ?? null,
|
|
2518
|
-
deletionLocked: Boolean(row.deletion_locked)
|
|
2863
|
+
deletionLocked: Boolean(row.deletion_locked),
|
|
2864
|
+
marker: readSidebarMarker(row.marker)
|
|
2519
2865
|
};
|
|
2520
2866
|
}
|
|
2521
2867
|
function parseSnippetScope(value) {
|
|
@@ -2559,12 +2905,14 @@ function mapFolderSqlRow(row) {
|
|
|
2559
2905
|
return {
|
|
2560
2906
|
id: row.id,
|
|
2561
2907
|
collectionId: row.collection_id,
|
|
2908
|
+
parentFolderId: row.parent_folder_id,
|
|
2562
2909
|
name: row.name,
|
|
2563
2910
|
sortOrder: row.sort_order,
|
|
2564
2911
|
createdAt: row.created_at,
|
|
2565
2912
|
updatedAt: row.updated_at ?? row.created_at,
|
|
2566
2913
|
createdByUserId: row.created_by_user_id ?? null,
|
|
2567
|
-
updatedByUserId: row.updated_by_user_id ?? null
|
|
2914
|
+
updatedByUserId: row.updated_by_user_id ?? null,
|
|
2915
|
+
marker: readSidebarMarker(row.marker)
|
|
2568
2916
|
};
|
|
2569
2917
|
}
|
|
2570
2918
|
function mapRequestSqlRow(row) {
|
|
@@ -2587,7 +2935,23 @@ function mapRequestSqlRow(row) {
|
|
|
2587
2935
|
createdAt: row.created_at,
|
|
2588
2936
|
updatedAt: row.updated_at,
|
|
2589
2937
|
createdByUserId: row.created_by_user_id ?? null,
|
|
2590
|
-
updatedByUserId: row.updated_by_user_id ?? null
|
|
2938
|
+
updatedByUserId: row.updated_by_user_id ?? null,
|
|
2939
|
+
marker: readSidebarMarker(row.marker)
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
function mapDocumentSqlRow(row) {
|
|
2943
|
+
return {
|
|
2944
|
+
id: row.id,
|
|
2945
|
+
collectionId: row.collection_id,
|
|
2946
|
+
folderId: row.folder_id,
|
|
2947
|
+
name: row.name,
|
|
2948
|
+
content: row.content,
|
|
2949
|
+
sortOrder: row.sort_order,
|
|
2950
|
+
createdAt: row.created_at,
|
|
2951
|
+
updatedAt: row.updated_at,
|
|
2952
|
+
createdByUserId: row.created_by_user_id ?? null,
|
|
2953
|
+
updatedByUserId: row.updated_by_user_id ?? null,
|
|
2954
|
+
marker: readSidebarMarker(row.marker)
|
|
2591
2955
|
};
|
|
2592
2956
|
}
|
|
2593
2957
|
|
|
@@ -2659,6 +3023,7 @@ var FOLDERS_MIGRATION_SQL = `
|
|
|
2659
3023
|
CREATE TABLE IF NOT EXISTS folders (
|
|
2660
3024
|
id VARCHAR(36) PRIMARY KEY,
|
|
2661
3025
|
collection_id VARCHAR(36) NOT NULL,
|
|
3026
|
+
parent_folder_id VARCHAR(36) NULL,
|
|
2662
3027
|
name VARCHAR(255) NOT NULL,
|
|
2663
3028
|
sort_order INT NOT NULL DEFAULT 0,
|
|
2664
3029
|
created_at DATETIME NOT NULL,
|
|
@@ -2666,6 +3031,7 @@ CREATE TABLE IF NOT EXISTS folders (
|
|
|
2666
3031
|
created_by_user_id VARCHAR(36) NULL,
|
|
2667
3032
|
updated_by_user_id VARCHAR(36) NULL,
|
|
2668
3033
|
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE,
|
|
3034
|
+
FOREIGN KEY (parent_folder_id) REFERENCES folders(id) ON DELETE CASCADE,
|
|
2669
3035
|
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
|
2670
3036
|
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
|
|
2671
3037
|
)
|
|
@@ -2697,6 +3063,24 @@ CREATE TABLE IF NOT EXISTS requests (
|
|
|
2697
3063
|
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
|
|
2698
3064
|
)
|
|
2699
3065
|
`.trim();
|
|
3066
|
+
var DOCUMENTS_MIGRATION_SQL = `
|
|
3067
|
+
CREATE TABLE IF NOT EXISTS documents (
|
|
3068
|
+
id VARCHAR(36) PRIMARY KEY,
|
|
3069
|
+
collection_id VARCHAR(36) NOT NULL,
|
|
3070
|
+
folder_id VARCHAR(36) NULL,
|
|
3071
|
+
name VARCHAR(255) NOT NULL,
|
|
3072
|
+
content LONGTEXT NOT NULL,
|
|
3073
|
+
sort_order INT NOT NULL DEFAULT 0,
|
|
3074
|
+
created_at DATETIME NOT NULL,
|
|
3075
|
+
updated_at DATETIME NOT NULL,
|
|
3076
|
+
created_by_user_id VARCHAR(36) NULL,
|
|
3077
|
+
updated_by_user_id VARCHAR(36) NULL,
|
|
3078
|
+
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE,
|
|
3079
|
+
FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE CASCADE,
|
|
3080
|
+
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
|
3081
|
+
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
|
|
3082
|
+
)
|
|
3083
|
+
`.trim();
|
|
2700
3084
|
var USERS_MIGRATION_SQL = `
|
|
2701
3085
|
CREATE TABLE IF NOT EXISTS users (
|
|
2702
3086
|
id VARCHAR(36) PRIMARY KEY,
|
|
@@ -2752,6 +3136,10 @@ ALTER TABLE folders
|
|
|
2752
3136
|
ADD COLUMN IF NOT EXISTS created_by_user_id VARCHAR(36) NULL,
|
|
2753
3137
|
ADD COLUMN IF NOT EXISTS updated_by_user_id VARCHAR(36) NULL
|
|
2754
3138
|
`.trim();
|
|
3139
|
+
var FOLDERS_PARENT_MIGRATION_SQL = `
|
|
3140
|
+
ALTER TABLE folders
|
|
3141
|
+
ADD COLUMN IF NOT EXISTS parent_folder_id VARCHAR(36) NULL
|
|
3142
|
+
`.trim();
|
|
2755
3143
|
var REQUESTS_ATTRIBUTION_MIGRATION_SQL = `
|
|
2756
3144
|
ALTER TABLE requests
|
|
2757
3145
|
ADD COLUMN IF NOT EXISTS created_by_user_id VARCHAR(36) NULL,
|
|
@@ -2866,6 +3254,17 @@ CREATE TABLE IF NOT EXISTS user_invitations (
|
|
|
2866
3254
|
CONSTRAINT user_invitations_updated_by_fk FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
|
|
2867
3255
|
)
|
|
2868
3256
|
`.trim();
|
|
3257
|
+
var MARKER_TABLES = ["collections", "folders", "requests", "documents", "environments"];
|
|
3258
|
+
function buildMarkerRenameSql(table) {
|
|
3259
|
+
return `ALTER TABLE ${table} RENAME COLUMN IF EXISTS color TO marker`;
|
|
3260
|
+
}
|
|
3261
|
+
function buildMarkerAddSql(table) {
|
|
3262
|
+
return `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS marker TEXT`;
|
|
3263
|
+
}
|
|
3264
|
+
var MARKER_MIGRATIONS_SQL = [
|
|
3265
|
+
...MARKER_TABLES.map(buildMarkerRenameSql),
|
|
3266
|
+
...MARKER_TABLES.map(buildMarkerAddSql)
|
|
3267
|
+
];
|
|
2869
3268
|
var MYSQL_MIGRATIONS = [
|
|
2870
3269
|
USERS_MIGRATION_SQL,
|
|
2871
3270
|
API_TOKENS_MIGRATION_SQL,
|
|
@@ -2874,12 +3273,14 @@ var MYSQL_MIGRATIONS = [
|
|
|
2874
3273
|
SNIPPETS_MIGRATION_SQL,
|
|
2875
3274
|
FOLDERS_MIGRATION_SQL,
|
|
2876
3275
|
REQUESTS_MIGRATION_SQL,
|
|
3276
|
+
DOCUMENTS_MIGRATION_SQL,
|
|
2877
3277
|
AUDIT_LOG_MIGRATION_SQL,
|
|
2878
3278
|
API_TOKENS_USER_ID_MIGRATION_SQL,
|
|
2879
3279
|
API_TOKENS_ATTRIBUTION_MIGRATION_SQL,
|
|
2880
3280
|
COLLECTIONS_ATTRIBUTION_MIGRATION_SQL,
|
|
2881
3281
|
ENVIRONMENTS_ATTRIBUTION_MIGRATION_SQL,
|
|
2882
3282
|
FOLDERS_ATTRIBUTION_MIGRATION_SQL,
|
|
3283
|
+
FOLDERS_PARENT_MIGRATION_SQL,
|
|
2883
3284
|
REQUESTS_ATTRIBUTION_MIGRATION_SQL,
|
|
2884
3285
|
USERS_ATTRIBUTION_MIGRATION_SQL,
|
|
2885
3286
|
COLLECTIONS_BACKFILL_UPDATED_AT_SQL,
|
|
@@ -2893,7 +3294,8 @@ var MYSQL_MIGRATIONS = [
|
|
|
2893
3294
|
USERS_SNIPPET_ACCESS_MIGRATION_SQL,
|
|
2894
3295
|
USERS_SNIPPET_ACCESS_BACKFILL_SQL,
|
|
2895
3296
|
RUN_RESULTS_MIGRATION_SQL,
|
|
2896
|
-
USER_INVITATIONS_MIGRATION_SQL
|
|
3297
|
+
USER_INVITATIONS_MIGRATION_SQL,
|
|
3298
|
+
...MARKER_MIGRATIONS_SQL
|
|
2897
3299
|
];
|
|
2898
3300
|
|
|
2899
3301
|
// src/db/mysql/schemas.ts
|
|
@@ -2951,11 +3353,12 @@ function serializeAccessList(access) {
|
|
|
2951
3353
|
return JSON.stringify(access);
|
|
2952
3354
|
}
|
|
2953
3355
|
var USER_SELECT_COLUMNS = `id, name, role, collection_access, environment_access, snippet_access, llm_access, llm_models, llm_monthly_token_limit, created_at, updated_at, created_by_user_id, updated_by_user_id`;
|
|
2954
|
-
var COLLECTION_SELECT_COLUMNS = `id, name, variables, headers, auth, pre_request_script, post_request_script, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
|
|
2955
|
-
var ENVIRONMENT_SELECT_COLUMNS = `id, name, variables, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
|
|
3356
|
+
var COLLECTION_SELECT_COLUMNS = `id, name, variables, headers, auth, pre_request_script, post_request_script, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked, marker`;
|
|
3357
|
+
var ENVIRONMENT_SELECT_COLUMNS = `id, name, variables, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked, marker`;
|
|
2956
3358
|
var SNIPPET_SELECT_COLUMNS = `id, name, code, scope, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
|
|
2957
|
-
var FOLDER_SELECT_COLUMNS = `id, collection_id, name, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id`;
|
|
2958
|
-
var REQUEST_SELECT_COLUMNS = `id, collection_id, folder_id, name, method, url, headers, params, auth, body, body_type, pre_request_script, post_request_script, comment, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id`;
|
|
3359
|
+
var FOLDER_SELECT_COLUMNS = `id, collection_id, parent_folder_id, name, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id, marker`;
|
|
3360
|
+
var REQUEST_SELECT_COLUMNS = `id, collection_id, folder_id, name, method, url, headers, params, auth, body, body_type, pre_request_script, post_request_script, comment, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id, marker`;
|
|
3361
|
+
var DOCUMENT_SELECT_COLUMNS = `id, collection_id, folder_id, name, content, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id, marker`;
|
|
2959
3362
|
var API_TOKEN_SELECT_COLUMNS = `id, user_id, name, token_hash, token_prefix, created_at, last_used_at, revoked_at, created_by_user_id, updated_by_user_id`;
|
|
2960
3363
|
var AUDIT_LOG_SELECT_COLUMNS = `id, user_id, user_name, action, entity_type, entity_id, created_at, metadata`;
|
|
2961
3364
|
|
|
@@ -3003,6 +3406,7 @@ var USER_SELECT = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
|
|
|
3003
3406
|
var API_TOKEN_SELECT = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
|
|
3004
3407
|
var FOLDER_SELECT = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
|
|
3005
3408
|
var REQUEST_SELECT = `SELECT ${REQUEST_SELECT_COLUMNS} FROM requests`;
|
|
3409
|
+
var DOCUMENT_SELECT = `SELECT ${DOCUMENT_SELECT_COLUMNS} FROM documents`;
|
|
3006
3410
|
var AUDIT_LOG_SELECT = `SELECT ${AUDIT_LOG_SELECT_COLUMNS} FROM audit_log`;
|
|
3007
3411
|
var LLM_USAGE_SELECT = `SELECT ${LLM_USAGE_SELECT_COLUMNS} FROM llm_usage`;
|
|
3008
3412
|
var LLM_USAGE_LOG_SELECT = `SELECT ${LLM_USAGE_LOG_SELECT_COLUMNS} FROM llm_usage_log`;
|
|
@@ -3794,10 +4198,10 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
3794
4198
|
*
|
|
3795
4199
|
* @param actingUserId - User performing the update action.
|
|
3796
4200
|
*/
|
|
3797
|
-
async updateCollection(id, name, variables, headers, preRequestScript, postRequestScript, auth, actingUserId) {
|
|
4201
|
+
async updateCollection(id, name, variables, headers, preRequestScript, postRequestScript, auth, actingUserId, marker) {
|
|
3798
4202
|
const trimmedName = trimRequiredName(name, "Collection name");
|
|
3799
4203
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
3800
|
-
const result = await this.executeStatement(
|
|
4204
|
+
const result = marker !== void 0 ? await this.executeStatement(
|
|
3801
4205
|
`UPDATE collections
|
|
3802
4206
|
SET name = ?,
|
|
3803
4207
|
variables = ?,
|
|
@@ -3806,7 +4210,8 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
3806
4210
|
pre_request_script = ?,
|
|
3807
4211
|
post_request_script = ?,
|
|
3808
4212
|
updated_at = ?,
|
|
3809
|
-
updated_by_user_id =
|
|
4213
|
+
updated_by_user_id = ?,
|
|
4214
|
+
marker = ?
|
|
3810
4215
|
WHERE id = ?`,
|
|
3811
4216
|
[
|
|
3812
4217
|
trimmedName,
|
|
@@ -3817,9 +4222,32 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
3817
4222
|
postRequestScript,
|
|
3818
4223
|
updatedAt,
|
|
3819
4224
|
actingUserId,
|
|
4225
|
+
serializeSidebarMarker(marker),
|
|
3820
4226
|
id
|
|
3821
4227
|
]
|
|
3822
|
-
)
|
|
4228
|
+
) : await this.executeStatement(
|
|
4229
|
+
`UPDATE collections
|
|
4230
|
+
SET name = ?,
|
|
4231
|
+
variables = ?,
|
|
4232
|
+
headers = ?,
|
|
4233
|
+
auth = ?,
|
|
4234
|
+
pre_request_script = ?,
|
|
4235
|
+
post_request_script = ?,
|
|
4236
|
+
updated_at = ?,
|
|
4237
|
+
updated_by_user_id = ?
|
|
4238
|
+
WHERE id = ?`,
|
|
4239
|
+
[
|
|
4240
|
+
trimmedName,
|
|
4241
|
+
JSON.stringify(variables),
|
|
4242
|
+
JSON.stringify(headers),
|
|
4243
|
+
JSON.stringify(auth),
|
|
4244
|
+
preRequestScript,
|
|
4245
|
+
postRequestScript,
|
|
4246
|
+
updatedAt,
|
|
4247
|
+
actingUserId,
|
|
4248
|
+
id
|
|
4249
|
+
]
|
|
4250
|
+
);
|
|
3823
4251
|
if ((result.affectedRows ?? 0) === 0) {
|
|
3824
4252
|
throw new Error("Collection not found");
|
|
3825
4253
|
}
|
|
@@ -3935,10 +4363,26 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
3935
4363
|
*
|
|
3936
4364
|
* @param actingUserId - User performing the update action.
|
|
3937
4365
|
*/
|
|
3938
|
-
async updateEnvironment(id, name, variables, actingUserId) {
|
|
4366
|
+
async updateEnvironment(id, name, variables, actingUserId, marker) {
|
|
3939
4367
|
const trimmedName = trimRequiredName(name, "Environment name");
|
|
3940
4368
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
3941
|
-
const result = await this.executeStatement(
|
|
4369
|
+
const result = marker !== void 0 ? await this.executeStatement(
|
|
4370
|
+
`UPDATE environments
|
|
4371
|
+
SET name = ?,
|
|
4372
|
+
variables = ?,
|
|
4373
|
+
updated_at = ?,
|
|
4374
|
+
updated_by_user_id = ?,
|
|
4375
|
+
marker = ?
|
|
4376
|
+
WHERE id = ?`,
|
|
4377
|
+
[
|
|
4378
|
+
trimmedName,
|
|
4379
|
+
JSON.stringify(variables),
|
|
4380
|
+
updatedAt,
|
|
4381
|
+
actingUserId,
|
|
4382
|
+
serializeSidebarMarker(marker),
|
|
4383
|
+
id
|
|
4384
|
+
]
|
|
4385
|
+
) : await this.executeStatement(
|
|
3942
4386
|
`UPDATE environments
|
|
3943
4387
|
SET name = ?,
|
|
3944
4388
|
variables = ?,
|
|
@@ -4189,6 +4633,7 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4189
4633
|
const params = JSON.stringify(input.params);
|
|
4190
4634
|
const auth = JSON.stringify(input.auth);
|
|
4191
4635
|
const folderId = input.folderId ?? null;
|
|
4636
|
+
const serializedMarker = serializeSidebarMarker(input.marker ?? null);
|
|
4192
4637
|
const now = /* @__PURE__ */ new Date();
|
|
4193
4638
|
if (folderId != null) {
|
|
4194
4639
|
const folderRows = await this.queryRows(
|
|
@@ -4216,6 +4661,7 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4216
4661
|
pre_request_script = ?,
|
|
4217
4662
|
post_request_script = ?,
|
|
4218
4663
|
comment = ?,
|
|
4664
|
+
marker = ?,
|
|
4219
4665
|
updated_at = ?,
|
|
4220
4666
|
updated_by_user_id = ?
|
|
4221
4667
|
WHERE id = ?`,
|
|
@@ -4233,6 +4679,7 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4233
4679
|
input.preRequestScript,
|
|
4234
4680
|
input.postRequestScript,
|
|
4235
4681
|
input.comment,
|
|
4682
|
+
serializedMarker,
|
|
4236
4683
|
now,
|
|
4237
4684
|
actingUserId,
|
|
4238
4685
|
input.id
|
|
@@ -4274,12 +4721,13 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4274
4721
|
pre_request_script,
|
|
4275
4722
|
post_request_script,
|
|
4276
4723
|
comment,
|
|
4724
|
+
marker,
|
|
4277
4725
|
sort_order,
|
|
4278
4726
|
created_at,
|
|
4279
4727
|
updated_at,
|
|
4280
4728
|
created_by_user_id,
|
|
4281
4729
|
updated_by_user_id
|
|
4282
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4730
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4283
4731
|
[
|
|
4284
4732
|
id,
|
|
4285
4733
|
input.collectionId,
|
|
@@ -4295,6 +4743,7 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4295
4743
|
input.preRequestScript,
|
|
4296
4744
|
input.postRequestScript,
|
|
4297
4745
|
input.comment,
|
|
4746
|
+
serializedMarker,
|
|
4298
4747
|
maxOrder + 1,
|
|
4299
4748
|
now,
|
|
4300
4749
|
now,
|
|
@@ -4355,27 +4804,46 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4355
4804
|
* @param name - Display name for the folder.
|
|
4356
4805
|
* @param actingUserId - User performing the create action.
|
|
4357
4806
|
*/
|
|
4358
|
-
async createFolder(collectionId, name, actingUserId) {
|
|
4807
|
+
async createFolder(collectionId, name, actingUserId, parentFolderId = null) {
|
|
4359
4808
|
const trimmedName = trimRequiredName(name, "Folder name");
|
|
4809
|
+
if (parentFolderId != null) {
|
|
4810
|
+
const parent = await this.findFolderById(parentFolderId);
|
|
4811
|
+
if (!parent || parent.collectionId !== collectionId) {
|
|
4812
|
+
throw new Error("Parent folder not found in collection");
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4360
4815
|
const id = randomUUID4();
|
|
4361
4816
|
const now = /* @__PURE__ */ new Date();
|
|
4362
4817
|
const maxRows = await this.queryRows(
|
|
4363
|
-
|
|
4364
|
-
|
|
4818
|
+
`SELECT COALESCE(MAX(sort_order), -1) AS max_order
|
|
4819
|
+
FROM folders
|
|
4820
|
+
WHERE collection_id = ? AND parent_folder_id <=> ?`,
|
|
4821
|
+
[collectionId, parentFolderId]
|
|
4365
4822
|
);
|
|
4366
4823
|
const maxOrder = maxRows[0]?.max_order ?? -1;
|
|
4367
4824
|
await this.executeStatement(
|
|
4368
4825
|
`INSERT INTO folders (
|
|
4369
4826
|
id,
|
|
4370
4827
|
collection_id,
|
|
4828
|
+
parent_folder_id,
|
|
4371
4829
|
name,
|
|
4372
4830
|
sort_order,
|
|
4373
4831
|
created_at,
|
|
4374
4832
|
updated_at,
|
|
4375
4833
|
created_by_user_id,
|
|
4376
4834
|
updated_by_user_id
|
|
4377
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4378
|
-
[
|
|
4835
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4836
|
+
[
|
|
4837
|
+
id,
|
|
4838
|
+
collectionId,
|
|
4839
|
+
parentFolderId,
|
|
4840
|
+
trimmedName,
|
|
4841
|
+
maxOrder + 1,
|
|
4842
|
+
now,
|
|
4843
|
+
now,
|
|
4844
|
+
actingUserId,
|
|
4845
|
+
actingUserId
|
|
4846
|
+
]
|
|
4379
4847
|
);
|
|
4380
4848
|
await this.recordAuditEntry(actingUserId, "create", "folder", id);
|
|
4381
4849
|
const rows = await this.queryRows(
|
|
@@ -4395,10 +4863,18 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4395
4863
|
* @param name - New display name.
|
|
4396
4864
|
* @param actingUserId - User performing the rename action.
|
|
4397
4865
|
*/
|
|
4398
|
-
async renameFolder(id, name, actingUserId) {
|
|
4866
|
+
async renameFolder(id, name, actingUserId, marker) {
|
|
4399
4867
|
const trimmedName = trimRequiredName(name, "Folder name");
|
|
4400
4868
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
4401
|
-
const result = await this.executeStatement(
|
|
4869
|
+
const result = marker !== void 0 ? await this.executeStatement(
|
|
4870
|
+
`UPDATE folders
|
|
4871
|
+
SET name = ?,
|
|
4872
|
+
updated_at = ?,
|
|
4873
|
+
updated_by_user_id = ?,
|
|
4874
|
+
marker = ?
|
|
4875
|
+
WHERE id = ?`,
|
|
4876
|
+
[trimmedName, updatedAt, actingUserId, serializeSidebarMarker(marker), id]
|
|
4877
|
+
) : await this.executeStatement(
|
|
4402
4878
|
`UPDATE folders
|
|
4403
4879
|
SET name = ?,
|
|
4404
4880
|
updated_at = ?,
|
|
@@ -4421,18 +4897,39 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4421
4897
|
return mapFolderSqlRow(row);
|
|
4422
4898
|
}
|
|
4423
4899
|
/**
|
|
4424
|
-
* Deletes a folder
|
|
4900
|
+
* Deletes a folder, its descendants, and their contents.
|
|
4425
4901
|
*
|
|
4426
4902
|
* @param id - Folder ID to delete.
|
|
4427
4903
|
* @param actingUserId - User performing the delete action.
|
|
4428
4904
|
*/
|
|
4429
4905
|
async deleteFolder(id, actingUserId) {
|
|
4430
4906
|
await this.recordAuditEntry(actingUserId, "delete", "folder", id);
|
|
4907
|
+
const root = await this.findFolderById(id);
|
|
4908
|
+
if (!root) {
|
|
4909
|
+
return;
|
|
4910
|
+
}
|
|
4911
|
+
const folders = await this.listFolders(root.collectionId);
|
|
4912
|
+
const descendantIds = /* @__PURE__ */ new Set([id]);
|
|
4913
|
+
let changed = true;
|
|
4914
|
+
while (changed) {
|
|
4915
|
+
changed = false;
|
|
4916
|
+
for (const folder of folders) {
|
|
4917
|
+
if (folder.parentFolderId != null && descendantIds.has(folder.parentFolderId) && !descendantIds.has(folder.id)) {
|
|
4918
|
+
descendantIds.add(folder.id);
|
|
4919
|
+
changed = true;
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
const ids = [...descendantIds];
|
|
4924
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
4431
4925
|
const connection = await this.requirePool().getConnection();
|
|
4432
4926
|
try {
|
|
4433
4927
|
await connection.beginTransaction();
|
|
4434
|
-
await connection.execute(
|
|
4435
|
-
await connection.execute(
|
|
4928
|
+
await connection.execute(`DELETE FROM documents WHERE folder_id IN (${placeholders})`, ids);
|
|
4929
|
+
await connection.execute(`DELETE FROM requests WHERE folder_id IN (${placeholders})`, ids);
|
|
4930
|
+
for (const folderId of ids.reverse()) {
|
|
4931
|
+
await connection.execute("DELETE FROM folders WHERE id = ?", [folderId]);
|
|
4932
|
+
}
|
|
4436
4933
|
await connection.commit();
|
|
4437
4934
|
} catch (err) {
|
|
4438
4935
|
await connection.rollback();
|
|
@@ -4442,13 +4939,62 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4442
4939
|
}
|
|
4443
4940
|
}
|
|
4444
4941
|
/**
|
|
4445
|
-
*
|
|
4942
|
+
* Moves a folder to a new parent and optional sibling position.
|
|
4943
|
+
*
|
|
4944
|
+
* @param id - Folder ID to move.
|
|
4945
|
+
* @param parentFolderId - Destination parent, or null for collection root.
|
|
4946
|
+
* @param sortOrder - Optional zero-based destination sibling index.
|
|
4947
|
+
* @param actingUserId - User performing the move action.
|
|
4948
|
+
*/
|
|
4949
|
+
async moveFolder(id, parentFolderId, sortOrder, actingUserId) {
|
|
4950
|
+
const folder = await this.findFolderById(id);
|
|
4951
|
+
if (!folder) {
|
|
4952
|
+
throw new Error("Folder not found");
|
|
4953
|
+
}
|
|
4954
|
+
const folders = await this.listFolders(folder.collectionId);
|
|
4955
|
+
if (parentFolderId != null) {
|
|
4956
|
+
let ancestor = folders.find((entry) => entry.id === parentFolderId);
|
|
4957
|
+
if (!ancestor) {
|
|
4958
|
+
throw new Error("Parent folder not found in collection");
|
|
4959
|
+
}
|
|
4960
|
+
while (ancestor) {
|
|
4961
|
+
if (ancestor.id === id) {
|
|
4962
|
+
throw new Error("Cannot move a folder inside itself or its descendants");
|
|
4963
|
+
}
|
|
4964
|
+
ancestor = ancestor.parentFolderId == null ? void 0 : folders.find((entry) => entry.id === ancestor?.parentFolderId);
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
const siblings = folders.filter((entry) => entry.id !== id && entry.parentFolderId === parentFolderId).sort(
|
|
4968
|
+
(left, right) => left.sortOrder - right.sortOrder || left.name.localeCompare(right.name)
|
|
4969
|
+
);
|
|
4970
|
+
const index = Math.max(0, Math.min(sortOrder ?? siblings.length, siblings.length));
|
|
4971
|
+
siblings.splice(index, 0, { ...folder, parentFolderId });
|
|
4972
|
+
await this.executeStatement(
|
|
4973
|
+
`UPDATE folders
|
|
4974
|
+
SET parent_folder_id = ?, updated_at = ?, updated_by_user_id = ?
|
|
4975
|
+
WHERE id = ?`,
|
|
4976
|
+
[parentFolderId, /* @__PURE__ */ new Date(), actingUserId, id]
|
|
4977
|
+
);
|
|
4978
|
+
await this.reorderFolders(
|
|
4979
|
+
folder.collectionId,
|
|
4980
|
+
parentFolderId,
|
|
4981
|
+
siblings.map((entry) => entry.id),
|
|
4982
|
+
actingUserId
|
|
4983
|
+
);
|
|
4984
|
+
const moved = await this.findFolderById(id);
|
|
4985
|
+
if (!moved) {
|
|
4986
|
+
throw new Error("Folder not found after move");
|
|
4987
|
+
}
|
|
4988
|
+
return moved;
|
|
4989
|
+
}
|
|
4990
|
+
/**
|
|
4991
|
+
* Reorders sibling folders within a collection.
|
|
4446
4992
|
*
|
|
4447
4993
|
* @param collectionId - Collection containing the folders.
|
|
4448
4994
|
* @param orderedFolderIds - Folder IDs in desired order.
|
|
4449
4995
|
* @param actingUserId - User performing the reorder action.
|
|
4450
4996
|
*/
|
|
4451
|
-
async reorderFolders(collectionId, orderedFolderIds, actingUserId) {
|
|
4997
|
+
async reorderFolders(collectionId, parentFolderId, orderedFolderIds, actingUserId) {
|
|
4452
4998
|
const connection = await this.requirePool().getConnection();
|
|
4453
4999
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
4454
5000
|
try {
|
|
@@ -4459,8 +5005,8 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4459
5005
|
SET sort_order = ?,
|
|
4460
5006
|
updated_at = ?,
|
|
4461
5007
|
updated_by_user_id = ?
|
|
4462
|
-
WHERE id = ? AND collection_id = ?`,
|
|
4463
|
-
[index, updatedAt, actingUserId, orderedFolderIds[index], collectionId]
|
|
5008
|
+
WHERE id = ? AND collection_id = ? AND parent_folder_id <=> ?`,
|
|
5009
|
+
[index, updatedAt, actingUserId, orderedFolderIds[index], collectionId, parentFolderId]
|
|
4464
5010
|
);
|
|
4465
5011
|
}
|
|
4466
5012
|
await connection.commit();
|
|
@@ -4471,6 +5017,7 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4471
5017
|
connection.release();
|
|
4472
5018
|
}
|
|
4473
5019
|
await this.recordAuditEntry(actingUserId, "reorder", "collection", collectionId, {
|
|
5020
|
+
parentFolderId,
|
|
4474
5021
|
orderedFolderIds
|
|
4475
5022
|
});
|
|
4476
5023
|
}
|
|
@@ -4587,122 +5134,374 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
4587
5134
|
});
|
|
4588
5135
|
}
|
|
4589
5136
|
/**
|
|
4590
|
-
*
|
|
5137
|
+
* Lists all documents in a collection.
|
|
4591
5138
|
*
|
|
4592
|
-
* @param
|
|
4593
|
-
* @param period - UTC calendar month key (`YYYY-MM`).
|
|
5139
|
+
* @param collectionId - Collection to query.
|
|
4594
5140
|
*/
|
|
4595
|
-
async
|
|
4596
|
-
const
|
|
4597
|
-
`${
|
|
4598
|
-
[
|
|
5141
|
+
async listDocuments(collectionId) {
|
|
5142
|
+
const rows = await this.queryRows(
|
|
5143
|
+
`${DOCUMENT_SELECT} WHERE collection_id = ? ORDER BY sort_order ASC, name ASC`,
|
|
5144
|
+
[collectionId]
|
|
4599
5145
|
);
|
|
4600
|
-
|
|
4601
|
-
return row ? mapLlmUsageSqlRow(row) : null;
|
|
5146
|
+
return rows.map(mapDocumentSqlRow);
|
|
4602
5147
|
}
|
|
4603
5148
|
/**
|
|
4604
|
-
*
|
|
5149
|
+
* Finds a document by id.
|
|
4605
5150
|
*
|
|
4606
|
-
* @param
|
|
4607
|
-
* @param period - UTC calendar month key (`YYYY-MM`).
|
|
4608
|
-
* @param promptTokens - Prompt tokens to add.
|
|
4609
|
-
* @param completionTokens - Completion tokens to add.
|
|
5151
|
+
* @param id - Document identifier to look up.
|
|
4610
5152
|
*/
|
|
4611
|
-
async
|
|
4612
|
-
const
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
await this.executeStatement(
|
|
4616
|
-
`INSERT INTO llm_usage (
|
|
4617
|
-
id,
|
|
4618
|
-
user_id,
|
|
4619
|
-
period,
|
|
4620
|
-
prompt_tokens,
|
|
4621
|
-
completion_tokens,
|
|
4622
|
-
total_tokens,
|
|
4623
|
-
updated_at
|
|
4624
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4625
|
-
ON DUPLICATE KEY UPDATE
|
|
4626
|
-
prompt_tokens = prompt_tokens + VALUES(prompt_tokens),
|
|
4627
|
-
completion_tokens = completion_tokens + VALUES(completion_tokens),
|
|
4628
|
-
total_tokens = total_tokens + VALUES(total_tokens),
|
|
4629
|
-
updated_at = VALUES(updated_at)`,
|
|
4630
|
-
[id, userId, period, promptTokens, completionTokens, totalDelta, now]
|
|
5153
|
+
async findDocumentById(id) {
|
|
5154
|
+
const rows = await this.queryRows(
|
|
5155
|
+
`${DOCUMENT_SELECT} WHERE id = ? LIMIT 1`,
|
|
5156
|
+
[id]
|
|
4631
5157
|
);
|
|
4632
|
-
const
|
|
4633
|
-
|
|
4634
|
-
throw new Error("LLM usage not found after upsert");
|
|
4635
|
-
}
|
|
4636
|
-
return usage;
|
|
5158
|
+
const row = rows[0];
|
|
5159
|
+
return row ? mapDocumentSqlRow(row) : null;
|
|
4637
5160
|
}
|
|
4638
5161
|
/**
|
|
4639
|
-
* Inserts a
|
|
5162
|
+
* Inserts a new document or updates an existing one.
|
|
4640
5163
|
*
|
|
4641
|
-
* @param input -
|
|
5164
|
+
* @param input - Document fields to persist.
|
|
5165
|
+
* @param actingUserId - User performing the save action.
|
|
4642
5166
|
*/
|
|
4643
|
-
async
|
|
4644
|
-
const
|
|
5167
|
+
async saveDocument(input, actingUserId) {
|
|
5168
|
+
const trimmedName = trimRequiredName(input.name, "Document name");
|
|
5169
|
+
const folderId = input.folderId ?? null;
|
|
5170
|
+
const serializedMarker = serializeSidebarMarker(input.marker ?? null);
|
|
4645
5171
|
const now = /* @__PURE__ */ new Date();
|
|
5172
|
+
if (folderId != null) {
|
|
5173
|
+
const folderRows = await this.queryRows(
|
|
5174
|
+
"SELECT collection_id FROM folders WHERE id = ?",
|
|
5175
|
+
[folderId]
|
|
5176
|
+
);
|
|
5177
|
+
const folderRow = folderRows[0];
|
|
5178
|
+
if (!folderRow || folderRow.collection_id !== input.collectionId) {
|
|
5179
|
+
throw new Error("Folder not found");
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
if (input.id) {
|
|
5183
|
+
const result = await this.executeStatement(
|
|
5184
|
+
`UPDATE documents SET
|
|
5185
|
+
collection_id = ?,
|
|
5186
|
+
folder_id = ?,
|
|
5187
|
+
name = ?,
|
|
5188
|
+
content = ?,
|
|
5189
|
+
marker = ?,
|
|
5190
|
+
updated_at = ?,
|
|
5191
|
+
updated_by_user_id = ?
|
|
5192
|
+
WHERE id = ?`,
|
|
5193
|
+
[
|
|
5194
|
+
input.collectionId,
|
|
5195
|
+
folderId,
|
|
5196
|
+
trimmedName,
|
|
5197
|
+
input.content,
|
|
5198
|
+
serializedMarker,
|
|
5199
|
+
now,
|
|
5200
|
+
actingUserId,
|
|
5201
|
+
input.id
|
|
5202
|
+
]
|
|
5203
|
+
);
|
|
5204
|
+
if ((result.affectedRows ?? 0) > 0) {
|
|
5205
|
+
await this.recordAuditEntry(actingUserId, "update", "document", input.id);
|
|
5206
|
+
const rows2 = await this.queryRows(
|
|
5207
|
+
`${DOCUMENT_SELECT} WHERE id = ?`,
|
|
5208
|
+
[input.id]
|
|
5209
|
+
);
|
|
5210
|
+
const row2 = rows2[0];
|
|
5211
|
+
if (row2) {
|
|
5212
|
+
return mapDocumentSqlRow(row2);
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
5216
|
+
const maxRows = await this.queryRows(
|
|
5217
|
+
`SELECT COALESCE(MAX(sort_order), -1) AS max_order FROM documents
|
|
5218
|
+
WHERE collection_id = ?
|
|
5219
|
+
AND ((? IS NULL AND folder_id IS NULL) OR folder_id = ?)`,
|
|
5220
|
+
[input.collectionId, folderId, folderId]
|
|
5221
|
+
);
|
|
5222
|
+
const maxOrder = maxRows[0]?.max_order ?? -1;
|
|
5223
|
+
const id = randomUUID4();
|
|
4646
5224
|
await this.executeStatement(
|
|
4647
|
-
`INSERT INTO
|
|
5225
|
+
`INSERT INTO documents (
|
|
4648
5226
|
id,
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
created_at
|
|
4661
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5227
|
+
collection_id,
|
|
5228
|
+
folder_id,
|
|
5229
|
+
name,
|
|
5230
|
+
content,
|
|
5231
|
+
marker,
|
|
5232
|
+
sort_order,
|
|
5233
|
+
created_at,
|
|
5234
|
+
updated_at,
|
|
5235
|
+
created_by_user_id,
|
|
5236
|
+
updated_by_user_id
|
|
5237
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4662
5238
|
[
|
|
4663
5239
|
id,
|
|
4664
|
-
input.
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
input.
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
input.messageCount,
|
|
4675
|
-
now
|
|
5240
|
+
input.collectionId,
|
|
5241
|
+
folderId,
|
|
5242
|
+
trimmedName,
|
|
5243
|
+
input.content,
|
|
5244
|
+
serializedMarker,
|
|
5245
|
+
maxOrder + 1,
|
|
5246
|
+
now,
|
|
5247
|
+
now,
|
|
5248
|
+
actingUserId,
|
|
5249
|
+
actingUserId
|
|
4676
5250
|
]
|
|
4677
5251
|
);
|
|
5252
|
+
await this.recordAuditEntry(actingUserId, "create", "document", id);
|
|
4678
5253
|
const rows = await this.queryRows(
|
|
4679
|
-
`${
|
|
5254
|
+
`${DOCUMENT_SELECT} WHERE id = ?`,
|
|
4680
5255
|
[id]
|
|
4681
5256
|
);
|
|
4682
5257
|
const row = rows[0];
|
|
4683
5258
|
if (!row) {
|
|
4684
|
-
throw new Error("
|
|
5259
|
+
throw new Error("Document not found after insert");
|
|
4685
5260
|
}
|
|
4686
|
-
return
|
|
5261
|
+
return mapDocumentSqlRow(row);
|
|
4687
5262
|
}
|
|
4688
5263
|
/**
|
|
4689
|
-
*
|
|
5264
|
+
* Deletes a document by ID.
|
|
5265
|
+
*
|
|
5266
|
+
* @param id - Document ID to delete.
|
|
5267
|
+
* @param actingUserId - User performing the delete action.
|
|
4690
5268
|
*/
|
|
4691
|
-
async
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
);
|
|
4695
|
-
return rows.map(mapLlmUsageLogSqlRow);
|
|
5269
|
+
async deleteDocument(id, actingUserId) {
|
|
5270
|
+
await this.recordAuditEntry(actingUserId, "delete", "document", id);
|
|
5271
|
+
await this.executeStatement("DELETE FROM documents WHERE id = ?", [id]);
|
|
4696
5272
|
}
|
|
4697
5273
|
/**
|
|
4698
|
-
*
|
|
5274
|
+
* Reorders documents within a folder or at collection root.
|
|
5275
|
+
*
|
|
5276
|
+
* @param actingUserId - User performing the reorder action.
|
|
4699
5277
|
*/
|
|
4700
|
-
async
|
|
4701
|
-
const
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
5278
|
+
async reorderDocuments(collectionId, folderId, orderedDocumentIds, actingUserId) {
|
|
5279
|
+
const connection = await this.requirePool().getConnection();
|
|
5280
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
5281
|
+
try {
|
|
5282
|
+
await connection.beginTransaction();
|
|
5283
|
+
for (let index = 0; index < orderedDocumentIds.length; index++) {
|
|
5284
|
+
await connection.execute(
|
|
5285
|
+
`UPDATE documents
|
|
5286
|
+
SET sort_order = ?,
|
|
5287
|
+
folder_id = ?,
|
|
5288
|
+
updated_at = ?,
|
|
5289
|
+
updated_by_user_id = ?
|
|
5290
|
+
WHERE id = ? AND collection_id = ?`,
|
|
5291
|
+
[index, folderId, updatedAt, actingUserId, orderedDocumentIds[index], collectionId]
|
|
5292
|
+
);
|
|
5293
|
+
}
|
|
5294
|
+
await connection.commit();
|
|
5295
|
+
} catch (err) {
|
|
5296
|
+
await connection.rollback();
|
|
5297
|
+
throw err;
|
|
5298
|
+
} finally {
|
|
5299
|
+
connection.release();
|
|
5300
|
+
}
|
|
5301
|
+
await this.recordAuditEntry(actingUserId, "reorder", "document", collectionId, {
|
|
5302
|
+
folderId,
|
|
5303
|
+
orderedDocumentIds
|
|
5304
|
+
});
|
|
5305
|
+
}
|
|
5306
|
+
/**
|
|
5307
|
+
* Moves a document to another folder or collection root at a given index.
|
|
5308
|
+
*
|
|
5309
|
+
* @param actingUserId - User performing the move action.
|
|
5310
|
+
*/
|
|
5311
|
+
async moveDocument(documentId, folderId, index, actingUserId) {
|
|
5312
|
+
const connection = await this.requirePool().getConnection();
|
|
5313
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
5314
|
+
const listInContainer = async (collectionId, targetFolderId) => {
|
|
5315
|
+
const rows = await this.queryRows(
|
|
5316
|
+
`SELECT id FROM documents WHERE collection_id = ?
|
|
5317
|
+
AND ((? IS NULL AND folder_id IS NULL) OR folder_id = ?)
|
|
5318
|
+
ORDER BY sort_order ASC, name ASC`,
|
|
5319
|
+
[collectionId, targetFolderId, targetFolderId]
|
|
5320
|
+
);
|
|
5321
|
+
return rows.map((row) => row.id);
|
|
5322
|
+
};
|
|
5323
|
+
const reindexContainer = async (targetFolderId, orderedIds) => {
|
|
5324
|
+
for (let sortIndex = 0; sortIndex < orderedIds.length; sortIndex++) {
|
|
5325
|
+
await connection.execute(
|
|
5326
|
+
`UPDATE documents
|
|
5327
|
+
SET sort_order = ?,
|
|
5328
|
+
folder_id = ?,
|
|
5329
|
+
updated_at = ?,
|
|
5330
|
+
updated_by_user_id = ?
|
|
5331
|
+
WHERE id = ?`,
|
|
5332
|
+
[sortIndex, targetFolderId, updatedAt, actingUserId, orderedIds[sortIndex]]
|
|
5333
|
+
);
|
|
5334
|
+
}
|
|
5335
|
+
};
|
|
5336
|
+
try {
|
|
5337
|
+
await connection.beginTransaction();
|
|
5338
|
+
const documentRows = await this.queryRows(
|
|
5339
|
+
`${DOCUMENT_SELECT} WHERE id = ?`,
|
|
5340
|
+
[documentId]
|
|
5341
|
+
);
|
|
5342
|
+
const documentRow = documentRows[0];
|
|
5343
|
+
if (!documentRow) {
|
|
5344
|
+
throw new Error("Document not found");
|
|
5345
|
+
}
|
|
5346
|
+
const document = mapDocumentSqlRow(documentRow);
|
|
5347
|
+
const collectionId = document.collectionId;
|
|
5348
|
+
const oldFolderId = document.folderId;
|
|
5349
|
+
if (folderId != null) {
|
|
5350
|
+
const folderRows = await this.queryRows(
|
|
5351
|
+
"SELECT collection_id FROM folders WHERE id = ?",
|
|
5352
|
+
[folderId]
|
|
5353
|
+
);
|
|
5354
|
+
const folderRow = folderRows[0];
|
|
5355
|
+
if (!folderRow || folderRow.collection_id !== collectionId) {
|
|
5356
|
+
throw new Error("Folder not found");
|
|
5357
|
+
}
|
|
5358
|
+
}
|
|
5359
|
+
if (oldFolderId === folderId) {
|
|
5360
|
+
const siblings = (await listInContainer(collectionId, folderId)).filter(
|
|
5361
|
+
(id) => id !== documentId
|
|
5362
|
+
);
|
|
5363
|
+
siblings.splice(index, 0, documentId);
|
|
5364
|
+
await reindexContainer(folderId, siblings);
|
|
5365
|
+
} else {
|
|
5366
|
+
const oldIds = (await listInContainer(collectionId, oldFolderId)).filter(
|
|
5367
|
+
(id) => id !== documentId
|
|
5368
|
+
);
|
|
5369
|
+
await reindexContainer(oldFolderId, oldIds);
|
|
5370
|
+
const newIds = (await listInContainer(collectionId, folderId)).filter(
|
|
5371
|
+
(id) => id !== documentId
|
|
5372
|
+
);
|
|
5373
|
+
newIds.splice(index, 0, documentId);
|
|
5374
|
+
await reindexContainer(folderId, newIds);
|
|
5375
|
+
}
|
|
5376
|
+
await connection.commit();
|
|
5377
|
+
} catch (err) {
|
|
5378
|
+
await connection.rollback();
|
|
5379
|
+
throw err;
|
|
5380
|
+
} finally {
|
|
5381
|
+
connection.release();
|
|
5382
|
+
}
|
|
5383
|
+
await this.recordAuditEntry(actingUserId, "move", "document", documentId, {
|
|
5384
|
+
folderId,
|
|
5385
|
+
index
|
|
5386
|
+
});
|
|
5387
|
+
}
|
|
5388
|
+
/**
|
|
5389
|
+
* Returns monthly LLM usage for a user, or null when no usage has been recorded.
|
|
5390
|
+
*
|
|
5391
|
+
* @param userId - Owning user identifier.
|
|
5392
|
+
* @param period - UTC calendar month key (`YYYY-MM`).
|
|
5393
|
+
*/
|
|
5394
|
+
async getLlmUsage(userId, period) {
|
|
5395
|
+
const [rows] = await this.requirePool().execute(
|
|
5396
|
+
`${LLM_USAGE_SELECT} WHERE user_id = ? AND period = ? LIMIT 1`,
|
|
5397
|
+
[userId, period]
|
|
5398
|
+
);
|
|
5399
|
+
const row = rows[0];
|
|
5400
|
+
return row ? mapLlmUsageSqlRow(row) : null;
|
|
5401
|
+
}
|
|
5402
|
+
/**
|
|
5403
|
+
* Atomically increments monthly LLM token usage for a user.
|
|
5404
|
+
*
|
|
5405
|
+
* @param userId - Owning user identifier.
|
|
5406
|
+
* @param period - UTC calendar month key (`YYYY-MM`).
|
|
5407
|
+
* @param promptTokens - Prompt tokens to add.
|
|
5408
|
+
* @param completionTokens - Completion tokens to add.
|
|
5409
|
+
*/
|
|
5410
|
+
async addLlmUsage(userId, period, promptTokens, completionTokens) {
|
|
5411
|
+
const totalDelta = promptTokens + completionTokens;
|
|
5412
|
+
const now = /* @__PURE__ */ new Date();
|
|
5413
|
+
const id = randomUUID4();
|
|
5414
|
+
await this.executeStatement(
|
|
5415
|
+
`INSERT INTO llm_usage (
|
|
5416
|
+
id,
|
|
5417
|
+
user_id,
|
|
5418
|
+
period,
|
|
5419
|
+
prompt_tokens,
|
|
5420
|
+
completion_tokens,
|
|
5421
|
+
total_tokens,
|
|
5422
|
+
updated_at
|
|
5423
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
5424
|
+
ON DUPLICATE KEY UPDATE
|
|
5425
|
+
prompt_tokens = prompt_tokens + VALUES(prompt_tokens),
|
|
5426
|
+
completion_tokens = completion_tokens + VALUES(completion_tokens),
|
|
5427
|
+
total_tokens = total_tokens + VALUES(total_tokens),
|
|
5428
|
+
updated_at = VALUES(updated_at)`,
|
|
5429
|
+
[id, userId, period, promptTokens, completionTokens, totalDelta, now]
|
|
5430
|
+
);
|
|
5431
|
+
const usage = await this.getLlmUsage(userId, period);
|
|
5432
|
+
if (!usage) {
|
|
5433
|
+
throw new Error("LLM usage not found after upsert");
|
|
5434
|
+
}
|
|
5435
|
+
return usage;
|
|
5436
|
+
}
|
|
5437
|
+
/**
|
|
5438
|
+
* Inserts a per-request LLM usage log entry.
|
|
5439
|
+
*
|
|
5440
|
+
* @param input - Usage details for one successful completion step.
|
|
5441
|
+
*/
|
|
5442
|
+
async createLlmUsageLog(input) {
|
|
5443
|
+
const id = randomUUID4();
|
|
5444
|
+
const now = /* @__PURE__ */ new Date();
|
|
5445
|
+
await this.executeStatement(
|
|
5446
|
+
`INSERT INTO llm_usage_log (
|
|
5447
|
+
id,
|
|
5448
|
+
user_id,
|
|
5449
|
+
api_token_id,
|
|
5450
|
+
period,
|
|
5451
|
+
model,
|
|
5452
|
+
provider,
|
|
5453
|
+
prompt_tokens,
|
|
5454
|
+
completion_tokens,
|
|
5455
|
+
total_tokens,
|
|
5456
|
+
is_new_turn,
|
|
5457
|
+
had_tool_calls,
|
|
5458
|
+
message_count,
|
|
5459
|
+
created_at
|
|
5460
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5461
|
+
[
|
|
5462
|
+
id,
|
|
5463
|
+
input.userId,
|
|
5464
|
+
input.apiTokenId,
|
|
5465
|
+
input.period,
|
|
5466
|
+
input.model,
|
|
5467
|
+
input.provider,
|
|
5468
|
+
input.promptTokens,
|
|
5469
|
+
input.completionTokens,
|
|
5470
|
+
input.totalTokens,
|
|
5471
|
+
input.isNewTurn ? 1 : 0,
|
|
5472
|
+
input.hadToolCalls ? 1 : 0,
|
|
5473
|
+
input.messageCount,
|
|
5474
|
+
now
|
|
5475
|
+
]
|
|
5476
|
+
);
|
|
5477
|
+
const rows = await this.queryRows(
|
|
5478
|
+
`${LLM_USAGE_LOG_SELECT} WHERE id = ?`,
|
|
5479
|
+
[id]
|
|
5480
|
+
);
|
|
5481
|
+
const row = rows[0];
|
|
5482
|
+
if (!row) {
|
|
5483
|
+
throw new Error("LLM usage log not found after insert");
|
|
5484
|
+
}
|
|
5485
|
+
return mapLlmUsageLogSqlRow(row);
|
|
5486
|
+
}
|
|
5487
|
+
/**
|
|
5488
|
+
* Lists all per-request LLM usage log entries, newest first.
|
|
5489
|
+
*/
|
|
5490
|
+
async listLlmUsageLogs() {
|
|
5491
|
+
const rows = await this.queryRows(
|
|
5492
|
+
`${LLM_USAGE_LOG_SELECT} ORDER BY created_at DESC`
|
|
5493
|
+
);
|
|
5494
|
+
return rows.map(mapLlmUsageLogSqlRow);
|
|
5495
|
+
}
|
|
5496
|
+
/**
|
|
5497
|
+
* Lists run results saved by the given user, newest first.
|
|
5498
|
+
*/
|
|
5499
|
+
async listRunResultsForUser(userId) {
|
|
5500
|
+
const rows = await this.queryRows(
|
|
5501
|
+
`${RUN_RESULT_SELECT} WHERE created_by_user_id = ? ORDER BY created_at DESC`,
|
|
5502
|
+
[userId]
|
|
5503
|
+
);
|
|
5504
|
+
return rows.map(mapRunResultSqlRow);
|
|
4706
5505
|
}
|
|
4707
5506
|
/**
|
|
4708
5507
|
* Lists all run results for admin inspection, newest first.
|
|
@@ -4978,6 +5777,7 @@ var FOLDERS_MIGRATION_SQL2 = `
|
|
|
4978
5777
|
CREATE TABLE IF NOT EXISTS folders (
|
|
4979
5778
|
id TEXT PRIMARY KEY,
|
|
4980
5779
|
collection_id TEXT NOT NULL,
|
|
5780
|
+
parent_folder_id TEXT REFERENCES folders(id) ON DELETE CASCADE,
|
|
4981
5781
|
name TEXT NOT NULL,
|
|
4982
5782
|
sort_order INT NOT NULL DEFAULT 0,
|
|
4983
5783
|
created_at TIMESTAMPTZ NOT NULL,
|
|
@@ -5012,6 +5812,22 @@ CREATE TABLE IF NOT EXISTS requests (
|
|
|
5012
5812
|
FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE CASCADE
|
|
5013
5813
|
);
|
|
5014
5814
|
`.trim();
|
|
5815
|
+
var DOCUMENTS_MIGRATION_SQL2 = `
|
|
5816
|
+
CREATE TABLE IF NOT EXISTS documents (
|
|
5817
|
+
id TEXT PRIMARY KEY,
|
|
5818
|
+
collection_id TEXT NOT NULL,
|
|
5819
|
+
folder_id TEXT,
|
|
5820
|
+
name TEXT NOT NULL,
|
|
5821
|
+
content TEXT NOT NULL DEFAULT '',
|
|
5822
|
+
sort_order INT NOT NULL DEFAULT 0,
|
|
5823
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
5824
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
5825
|
+
created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
5826
|
+
updated_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
5827
|
+
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE,
|
|
5828
|
+
FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE CASCADE
|
|
5829
|
+
);
|
|
5830
|
+
`.trim();
|
|
5015
5831
|
var USERS_MIGRATION_SQL2 = `
|
|
5016
5832
|
CREATE TABLE IF NOT EXISTS users (
|
|
5017
5833
|
id TEXT PRIMARY KEY,
|
|
@@ -5065,6 +5881,10 @@ ALTER TABLE folders
|
|
|
5065
5881
|
ADD COLUMN IF NOT EXISTS created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
5066
5882
|
ADD COLUMN IF NOT EXISTS updated_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL;
|
|
5067
5883
|
`.trim();
|
|
5884
|
+
var FOLDERS_PARENT_MIGRATION_SQL2 = `
|
|
5885
|
+
ALTER TABLE folders
|
|
5886
|
+
ADD COLUMN IF NOT EXISTS parent_folder_id TEXT REFERENCES folders(id) ON DELETE CASCADE;
|
|
5887
|
+
`.trim();
|
|
5068
5888
|
var REQUESTS_ATTRIBUTION_MIGRATION_SQL2 = `
|
|
5069
5889
|
ALTER TABLE requests
|
|
5070
5890
|
ADD COLUMN IF NOT EXISTS created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
@@ -5174,6 +5994,34 @@ CREATE TABLE IF NOT EXISTS user_invitations (
|
|
|
5174
5994
|
CREATE INDEX IF NOT EXISTS user_invitations_user_id_idx ON user_invitations (user_id);
|
|
5175
5995
|
CREATE INDEX IF NOT EXISTS user_invitations_expires_at_idx ON user_invitations (expires_at);
|
|
5176
5996
|
`.trim();
|
|
5997
|
+
function buildMarkerMigrationSql(table) {
|
|
5998
|
+
return `
|
|
5999
|
+
DO $$
|
|
6000
|
+
BEGIN
|
|
6001
|
+
IF EXISTS (
|
|
6002
|
+
SELECT 1 FROM information_schema.columns
|
|
6003
|
+
WHERE table_schema = current_schema() AND table_name = '${table}' AND column_name = 'color'
|
|
6004
|
+
) AND NOT EXISTS (
|
|
6005
|
+
SELECT 1 FROM information_schema.columns
|
|
6006
|
+
WHERE table_schema = current_schema() AND table_name = '${table}' AND column_name = 'marker'
|
|
6007
|
+
) THEN
|
|
6008
|
+
ALTER TABLE ${table} RENAME COLUMN color TO marker;
|
|
6009
|
+
END IF;
|
|
6010
|
+
|
|
6011
|
+
IF NOT EXISTS (
|
|
6012
|
+
SELECT 1 FROM information_schema.columns
|
|
6013
|
+
WHERE table_schema = current_schema() AND table_name = '${table}' AND column_name = 'marker'
|
|
6014
|
+
) THEN
|
|
6015
|
+
ALTER TABLE ${table} ADD COLUMN marker TEXT;
|
|
6016
|
+
END IF;
|
|
6017
|
+
END $$;
|
|
6018
|
+
`.trim();
|
|
6019
|
+
}
|
|
6020
|
+
var COLLECTIONS_MARKER_MIGRATION_SQL = buildMarkerMigrationSql("collections");
|
|
6021
|
+
var FOLDERS_MARKER_MIGRATION_SQL = buildMarkerMigrationSql("folders");
|
|
6022
|
+
var REQUESTS_MARKER_MIGRATION_SQL = buildMarkerMigrationSql("requests");
|
|
6023
|
+
var DOCUMENTS_MARKER_MIGRATION_SQL = buildMarkerMigrationSql("documents");
|
|
6024
|
+
var ENVIRONMENTS_MARKER_MIGRATION_SQL = buildMarkerMigrationSql("environments");
|
|
5177
6025
|
var POSTGRES_MIGRATIONS = [
|
|
5178
6026
|
USERS_MIGRATION_SQL2,
|
|
5179
6027
|
API_TOKENS_MIGRATION_SQL2,
|
|
@@ -5182,12 +6030,14 @@ var POSTGRES_MIGRATIONS = [
|
|
|
5182
6030
|
SNIPPETS_MIGRATION_SQL2,
|
|
5183
6031
|
FOLDERS_MIGRATION_SQL2,
|
|
5184
6032
|
REQUESTS_MIGRATION_SQL2,
|
|
6033
|
+
DOCUMENTS_MIGRATION_SQL2,
|
|
5185
6034
|
AUDIT_LOG_MIGRATION_SQL2,
|
|
5186
6035
|
API_TOKENS_USER_ID_MIGRATION_SQL2,
|
|
5187
6036
|
API_TOKENS_ATTRIBUTION_MIGRATION_SQL2,
|
|
5188
6037
|
COLLECTIONS_ATTRIBUTION_MIGRATION_SQL2,
|
|
5189
6038
|
ENVIRONMENTS_ATTRIBUTION_MIGRATION_SQL2,
|
|
5190
6039
|
FOLDERS_ATTRIBUTION_MIGRATION_SQL2,
|
|
6040
|
+
FOLDERS_PARENT_MIGRATION_SQL2,
|
|
5191
6041
|
REQUESTS_ATTRIBUTION_MIGRATION_SQL2,
|
|
5192
6042
|
USERS_ATTRIBUTION_MIGRATION_SQL2,
|
|
5193
6043
|
COLLECTIONS_BACKFILL_UPDATED_AT_SQL2,
|
|
@@ -5201,7 +6051,12 @@ var POSTGRES_MIGRATIONS = [
|
|
|
5201
6051
|
USERS_SNIPPET_ACCESS_MIGRATION_SQL2,
|
|
5202
6052
|
USERS_SNIPPET_ACCESS_BACKFILL_SQL2,
|
|
5203
6053
|
RUN_RESULTS_MIGRATION_SQL2,
|
|
5204
|
-
USER_INVITATIONS_MIGRATION_SQL2
|
|
6054
|
+
USER_INVITATIONS_MIGRATION_SQL2,
|
|
6055
|
+
COLLECTIONS_MARKER_MIGRATION_SQL,
|
|
6056
|
+
FOLDERS_MARKER_MIGRATION_SQL,
|
|
6057
|
+
REQUESTS_MARKER_MIGRATION_SQL,
|
|
6058
|
+
DOCUMENTS_MARKER_MIGRATION_SQL,
|
|
6059
|
+
ENVIRONMENTS_MARKER_MIGRATION_SQL
|
|
5205
6060
|
];
|
|
5206
6061
|
|
|
5207
6062
|
// src/db/postgres/schemas.ts
|
|
@@ -5232,6 +6087,7 @@ var USER_SELECT2 = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
|
|
|
5232
6087
|
var API_TOKEN_SELECT2 = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
|
|
5233
6088
|
var FOLDER_SELECT2 = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
|
|
5234
6089
|
var REQUEST_SELECT2 = `SELECT ${REQUEST_SELECT_COLUMNS} FROM requests`;
|
|
6090
|
+
var DOCUMENT_SELECT2 = `SELECT ${DOCUMENT_SELECT_COLUMNS} FROM documents`;
|
|
5235
6091
|
var LLM_USAGE_SELECT2 = `SELECT ${LLM_USAGE_SELECT_COLUMNS} FROM llm_usage`;
|
|
5236
6092
|
var LLM_USAGE_LOG_SELECT2 = `SELECT ${LLM_USAGE_LOG_SELECT_COLUMNS} FROM llm_usage_log`;
|
|
5237
6093
|
var PostgresDatabase = class _PostgresDatabase {
|
|
@@ -6003,10 +6859,34 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6003
6859
|
*
|
|
6004
6860
|
* @param actingUserId - User performing the update action.
|
|
6005
6861
|
*/
|
|
6006
|
-
async updateCollection(id, name, variables, headers, preRequestScript, postRequestScript, auth, actingUserId) {
|
|
6862
|
+
async updateCollection(id, name, variables, headers, preRequestScript, postRequestScript, auth, actingUserId, marker) {
|
|
6007
6863
|
const trimmedName = trimRequiredName(name, "Collection name");
|
|
6008
6864
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
6009
|
-
const result = await this.query(
|
|
6865
|
+
const result = marker !== void 0 ? await this.query(
|
|
6866
|
+
`UPDATE collections
|
|
6867
|
+
SET name = $1,
|
|
6868
|
+
variables = $2,
|
|
6869
|
+
headers = $3,
|
|
6870
|
+
auth = $4,
|
|
6871
|
+
pre_request_script = $5,
|
|
6872
|
+
post_request_script = $6,
|
|
6873
|
+
updated_at = $7,
|
|
6874
|
+
updated_by_user_id = $8,
|
|
6875
|
+
marker = $9
|
|
6876
|
+
WHERE id = $10`,
|
|
6877
|
+
[
|
|
6878
|
+
trimmedName,
|
|
6879
|
+
JSON.stringify(variables),
|
|
6880
|
+
JSON.stringify(headers),
|
|
6881
|
+
JSON.stringify(auth),
|
|
6882
|
+
preRequestScript,
|
|
6883
|
+
postRequestScript,
|
|
6884
|
+
updatedAt,
|
|
6885
|
+
actingUserId,
|
|
6886
|
+
serializeSidebarMarker(marker),
|
|
6887
|
+
id
|
|
6888
|
+
]
|
|
6889
|
+
) : await this.query(
|
|
6010
6890
|
`UPDATE collections
|
|
6011
6891
|
SET name = $1,
|
|
6012
6892
|
variables = $2,
|
|
@@ -6134,10 +7014,26 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6134
7014
|
*
|
|
6135
7015
|
* @param actingUserId - User performing the update action.
|
|
6136
7016
|
*/
|
|
6137
|
-
async updateEnvironment(id, name, variables, actingUserId) {
|
|
7017
|
+
async updateEnvironment(id, name, variables, actingUserId, marker) {
|
|
6138
7018
|
const trimmedName = trimRequiredName(name, "Environment name");
|
|
6139
7019
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
6140
|
-
const result = await this.query(
|
|
7020
|
+
const result = marker !== void 0 ? await this.query(
|
|
7021
|
+
`UPDATE environments
|
|
7022
|
+
SET name = $1,
|
|
7023
|
+
variables = $2,
|
|
7024
|
+
updated_at = $3,
|
|
7025
|
+
updated_by_user_id = $4,
|
|
7026
|
+
marker = $5
|
|
7027
|
+
WHERE id = $6`,
|
|
7028
|
+
[
|
|
7029
|
+
trimmedName,
|
|
7030
|
+
JSON.stringify(variables),
|
|
7031
|
+
updatedAt,
|
|
7032
|
+
actingUserId,
|
|
7033
|
+
serializeSidebarMarker(marker),
|
|
7034
|
+
id
|
|
7035
|
+
]
|
|
7036
|
+
) : await this.query(
|
|
6141
7037
|
`UPDATE environments
|
|
6142
7038
|
SET name = $1,
|
|
6143
7039
|
variables = $2,
|
|
@@ -6369,6 +7265,7 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6369
7265
|
const params = JSON.stringify(input.params);
|
|
6370
7266
|
const auth = JSON.stringify(input.auth);
|
|
6371
7267
|
const folderId = input.folderId ?? null;
|
|
7268
|
+
const serializedMarker = serializeSidebarMarker(input.marker ?? null);
|
|
6372
7269
|
const now = /* @__PURE__ */ new Date();
|
|
6373
7270
|
if (folderId != null) {
|
|
6374
7271
|
const folderResult = await this.query(
|
|
@@ -6396,9 +7293,10 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6396
7293
|
pre_request_script = $11,
|
|
6397
7294
|
post_request_script = $12,
|
|
6398
7295
|
comment = $13,
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
7296
|
+
marker = $14,
|
|
7297
|
+
updated_at = $15,
|
|
7298
|
+
updated_by_user_id = $16
|
|
7299
|
+
WHERE id = $17`,
|
|
6402
7300
|
[
|
|
6403
7301
|
input.collectionId,
|
|
6404
7302
|
folderId,
|
|
@@ -6413,6 +7311,7 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6413
7311
|
input.preRequestScript,
|
|
6414
7312
|
input.postRequestScript,
|
|
6415
7313
|
input.comment,
|
|
7314
|
+
serializedMarker,
|
|
6416
7315
|
now,
|
|
6417
7316
|
actingUserId,
|
|
6418
7317
|
input.id
|
|
@@ -6453,12 +7352,13 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6453
7352
|
pre_request_script,
|
|
6454
7353
|
post_request_script,
|
|
6455
7354
|
comment,
|
|
7355
|
+
marker,
|
|
6456
7356
|
sort_order,
|
|
6457
7357
|
created_at,
|
|
6458
7358
|
updated_at,
|
|
6459
7359
|
created_by_user_id,
|
|
6460
7360
|
updated_by_user_id
|
|
6461
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
|
7361
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
|
|
6462
7362
|
RETURNING ${REQUEST_SELECT_COLUMNS}`,
|
|
6463
7363
|
[
|
|
6464
7364
|
id,
|
|
@@ -6475,6 +7375,7 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6475
7375
|
input.preRequestScript,
|
|
6476
7376
|
input.postRequestScript,
|
|
6477
7377
|
input.comment,
|
|
7378
|
+
serializedMarker,
|
|
6478
7379
|
maxOrder + 1,
|
|
6479
7380
|
now,
|
|
6480
7381
|
now,
|
|
@@ -6500,163 +7401,479 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6500
7401
|
await this.query("DELETE FROM requests WHERE id = $1", [id]);
|
|
6501
7402
|
}
|
|
6502
7403
|
/**
|
|
6503
|
-
* Lists all folders in a collection.
|
|
7404
|
+
* Lists all folders in a collection.
|
|
7405
|
+
*
|
|
7406
|
+
* @param collectionId - Collection to query.
|
|
7407
|
+
*/
|
|
7408
|
+
async listFolders(collectionId) {
|
|
7409
|
+
const result = await this.query(
|
|
7410
|
+
`${FOLDER_SELECT2} WHERE collection_id = $1 ORDER BY sort_order ASC, name ASC`,
|
|
7411
|
+
[collectionId]
|
|
7412
|
+
);
|
|
7413
|
+
return result.rows.map(mapFolderSqlRow);
|
|
7414
|
+
}
|
|
7415
|
+
/**
|
|
7416
|
+
* Finds a folder by id.
|
|
7417
|
+
*
|
|
7418
|
+
* @param id - Folder identifier to look up.
|
|
7419
|
+
*/
|
|
7420
|
+
async findFolderById(id) {
|
|
7421
|
+
const result = await this.query(`${FOLDER_SELECT2} WHERE id = $1 LIMIT 1`, [id]);
|
|
7422
|
+
const row = result.rows[0];
|
|
7423
|
+
return row ? mapFolderSqlRow(row) : null;
|
|
7424
|
+
}
|
|
7425
|
+
/**
|
|
7426
|
+
* Creates a new folder in a collection.
|
|
7427
|
+
*
|
|
7428
|
+
* @param collectionId - Collection to add the folder to.
|
|
7429
|
+
* @param name - Display name for the folder.
|
|
7430
|
+
* @param actingUserId - User performing the create action.
|
|
7431
|
+
*/
|
|
7432
|
+
async createFolder(collectionId, name, actingUserId, parentFolderId = null) {
|
|
7433
|
+
const trimmedName = trimRequiredName(name, "Folder name");
|
|
7434
|
+
if (parentFolderId != null) {
|
|
7435
|
+
const parent = await this.findFolderById(parentFolderId);
|
|
7436
|
+
if (!parent || parent.collectionId !== collectionId) {
|
|
7437
|
+
throw new Error("Parent folder not found in collection");
|
|
7438
|
+
}
|
|
7439
|
+
}
|
|
7440
|
+
const id = randomUUID5();
|
|
7441
|
+
const now = /* @__PURE__ */ new Date();
|
|
7442
|
+
const maxResult = await this.query(
|
|
7443
|
+
`SELECT COALESCE(MAX(sort_order), -1) AS max_order
|
|
7444
|
+
FROM folders
|
|
7445
|
+
WHERE collection_id = $1 AND parent_folder_id IS NOT DISTINCT FROM $2`,
|
|
7446
|
+
[collectionId, parentFolderId]
|
|
7447
|
+
);
|
|
7448
|
+
const maxOrder = maxResult.rows[0]?.max_order ?? -1;
|
|
7449
|
+
const result = await this.query(
|
|
7450
|
+
`INSERT INTO folders (
|
|
7451
|
+
id,
|
|
7452
|
+
collection_id,
|
|
7453
|
+
parent_folder_id,
|
|
7454
|
+
name,
|
|
7455
|
+
sort_order,
|
|
7456
|
+
created_at,
|
|
7457
|
+
updated_at,
|
|
7458
|
+
created_by_user_id,
|
|
7459
|
+
updated_by_user_id
|
|
7460
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
7461
|
+
RETURNING ${FOLDER_SELECT_COLUMNS}`,
|
|
7462
|
+
[
|
|
7463
|
+
id,
|
|
7464
|
+
collectionId,
|
|
7465
|
+
parentFolderId,
|
|
7466
|
+
trimmedName,
|
|
7467
|
+
maxOrder + 1,
|
|
7468
|
+
now,
|
|
7469
|
+
now,
|
|
7470
|
+
actingUserId,
|
|
7471
|
+
actingUserId
|
|
7472
|
+
]
|
|
7473
|
+
);
|
|
7474
|
+
const row = result.rows[0];
|
|
7475
|
+
if (!row) {
|
|
7476
|
+
throw new Error("Folder not found after insert");
|
|
7477
|
+
}
|
|
7478
|
+
await this.recordAuditEntry(actingUserId, "create", "folder", id);
|
|
7479
|
+
return mapFolderSqlRow(row);
|
|
7480
|
+
}
|
|
7481
|
+
/**
|
|
7482
|
+
* Renames a folder.
|
|
7483
|
+
*
|
|
7484
|
+
* @param id - Folder ID to rename.
|
|
7485
|
+
* @param name - New display name.
|
|
7486
|
+
* @param actingUserId - User performing the rename action.
|
|
7487
|
+
*/
|
|
7488
|
+
async renameFolder(id, name, actingUserId, marker) {
|
|
7489
|
+
const trimmedName = trimRequiredName(name, "Folder name");
|
|
7490
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
7491
|
+
const result = marker !== void 0 ? await this.query(
|
|
7492
|
+
`UPDATE folders
|
|
7493
|
+
SET name = $1,
|
|
7494
|
+
updated_at = $2,
|
|
7495
|
+
updated_by_user_id = $3,
|
|
7496
|
+
marker = $4
|
|
7497
|
+
WHERE id = $5
|
|
7498
|
+
RETURNING ${FOLDER_SELECT_COLUMNS}`,
|
|
7499
|
+
[trimmedName, updatedAt, actingUserId, serializeSidebarMarker(marker), id]
|
|
7500
|
+
) : await this.query(
|
|
7501
|
+
`UPDATE folders
|
|
7502
|
+
SET name = $1,
|
|
7503
|
+
updated_at = $2,
|
|
7504
|
+
updated_by_user_id = $3
|
|
7505
|
+
WHERE id = $4
|
|
7506
|
+
RETURNING ${FOLDER_SELECT_COLUMNS}`,
|
|
7507
|
+
[trimmedName, updatedAt, actingUserId, id]
|
|
7508
|
+
);
|
|
7509
|
+
const row = result.rows[0];
|
|
7510
|
+
if (!row) {
|
|
7511
|
+
throw new Error("Folder not found");
|
|
7512
|
+
}
|
|
7513
|
+
await this.recordAuditEntry(actingUserId, "update", "folder", id);
|
|
7514
|
+
return mapFolderSqlRow(row);
|
|
7515
|
+
}
|
|
7516
|
+
/**
|
|
7517
|
+
* Deletes a folder and its descendants.
|
|
7518
|
+
*
|
|
7519
|
+
* @param id - Folder ID to delete.
|
|
7520
|
+
* @param actingUserId - User performing the delete action.
|
|
7521
|
+
*/
|
|
7522
|
+
async deleteFolder(id, actingUserId) {
|
|
7523
|
+
await this.recordAuditEntry(actingUserId, "delete", "folder", id);
|
|
7524
|
+
await this.query("DELETE FROM folders WHERE id = $1", [id]);
|
|
7525
|
+
}
|
|
7526
|
+
/**
|
|
7527
|
+
* Moves a folder to a new parent and optional sibling position.
|
|
7528
|
+
*
|
|
7529
|
+
* @param id - Folder ID to move.
|
|
7530
|
+
* @param parentFolderId - Destination parent, or null for collection root.
|
|
7531
|
+
* @param sortOrder - Optional zero-based destination sibling index.
|
|
7532
|
+
* @param actingUserId - User performing the move action.
|
|
7533
|
+
*/
|
|
7534
|
+
async moveFolder(id, parentFolderId, sortOrder, actingUserId) {
|
|
7535
|
+
const folder = await this.findFolderById(id);
|
|
7536
|
+
if (!folder) {
|
|
7537
|
+
throw new Error("Folder not found");
|
|
7538
|
+
}
|
|
7539
|
+
const folders = await this.listFolders(folder.collectionId);
|
|
7540
|
+
if (parentFolderId != null) {
|
|
7541
|
+
const parent = folders.find((entry) => entry.id === parentFolderId);
|
|
7542
|
+
if (!parent) {
|
|
7543
|
+
throw new Error("Parent folder not found in collection");
|
|
7544
|
+
}
|
|
7545
|
+
let ancestor = parent;
|
|
7546
|
+
while (ancestor) {
|
|
7547
|
+
if (ancestor.id === id) {
|
|
7548
|
+
throw new Error("Cannot move a folder inside itself or its descendants");
|
|
7549
|
+
}
|
|
7550
|
+
ancestor = ancestor.parentFolderId == null ? void 0 : folders.find((entry) => entry.id === ancestor?.parentFolderId);
|
|
7551
|
+
}
|
|
7552
|
+
}
|
|
7553
|
+
const siblings = folders.filter((entry) => entry.id !== id && entry.parentFolderId === parentFolderId).sort(
|
|
7554
|
+
(left, right) => left.sortOrder - right.sortOrder || left.name.localeCompare(right.name)
|
|
7555
|
+
);
|
|
7556
|
+
const index = Math.max(0, Math.min(sortOrder ?? siblings.length, siblings.length));
|
|
7557
|
+
siblings.splice(index, 0, { ...folder, parentFolderId });
|
|
7558
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
7559
|
+
await this.query(
|
|
7560
|
+
`UPDATE folders SET parent_folder_id = $1, updated_at = $2, updated_by_user_id = $3
|
|
7561
|
+
WHERE id = $4`,
|
|
7562
|
+
[parentFolderId, updatedAt, actingUserId, id]
|
|
7563
|
+
);
|
|
7564
|
+
await this.reorderFolders(
|
|
7565
|
+
folder.collectionId,
|
|
7566
|
+
parentFolderId,
|
|
7567
|
+
siblings.map((entry) => entry.id),
|
|
7568
|
+
actingUserId
|
|
7569
|
+
);
|
|
7570
|
+
const moved = await this.findFolderById(id);
|
|
7571
|
+
if (!moved) {
|
|
7572
|
+
throw new Error("Folder not found after move");
|
|
7573
|
+
}
|
|
7574
|
+
return moved;
|
|
7575
|
+
}
|
|
7576
|
+
/**
|
|
7577
|
+
* Reorders sibling folders within a collection.
|
|
7578
|
+
*
|
|
7579
|
+
* @param collectionId - Collection containing the folders.
|
|
7580
|
+
* @param orderedFolderIds - Folder IDs in desired order.
|
|
7581
|
+
* @param actingUserId - User performing the reorder action.
|
|
7582
|
+
*/
|
|
7583
|
+
async reorderFolders(collectionId, parentFolderId, orderedFolderIds, actingUserId) {
|
|
7584
|
+
const client = await this.requirePool().connect();
|
|
7585
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
7586
|
+
try {
|
|
7587
|
+
await client.query("BEGIN");
|
|
7588
|
+
for (let index = 0; index < orderedFolderIds.length; index++) {
|
|
7589
|
+
await client.query(
|
|
7590
|
+
`UPDATE folders
|
|
7591
|
+
SET sort_order = $1,
|
|
7592
|
+
updated_at = $2,
|
|
7593
|
+
updated_by_user_id = $3
|
|
7594
|
+
WHERE id = $4
|
|
7595
|
+
AND collection_id = $5
|
|
7596
|
+
AND parent_folder_id IS NOT DISTINCT FROM $6`,
|
|
7597
|
+
[index, updatedAt, actingUserId, orderedFolderIds[index], collectionId, parentFolderId]
|
|
7598
|
+
);
|
|
7599
|
+
}
|
|
7600
|
+
await client.query("COMMIT");
|
|
7601
|
+
} catch (err) {
|
|
7602
|
+
await client.query("ROLLBACK");
|
|
7603
|
+
throw err;
|
|
7604
|
+
} finally {
|
|
7605
|
+
client.release();
|
|
7606
|
+
}
|
|
7607
|
+
await this.recordAuditEntry(actingUserId, "reorder", "folder", collectionId, {
|
|
7608
|
+
parentFolderId,
|
|
7609
|
+
orderedFolderIds
|
|
7610
|
+
});
|
|
7611
|
+
}
|
|
7612
|
+
/**
|
|
7613
|
+
* Reorders requests within a folder or at collection root.
|
|
7614
|
+
*
|
|
7615
|
+
* @param actingUserId - User performing the reorder action.
|
|
7616
|
+
*/
|
|
7617
|
+
async reorderRequests(collectionId, folderId, orderedRequestIds, actingUserId) {
|
|
7618
|
+
const client = await this.requirePool().connect();
|
|
7619
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
7620
|
+
try {
|
|
7621
|
+
await client.query("BEGIN");
|
|
7622
|
+
for (let index = 0; index < orderedRequestIds.length; index++) {
|
|
7623
|
+
await client.query(
|
|
7624
|
+
`UPDATE requests
|
|
7625
|
+
SET sort_order = $1,
|
|
7626
|
+
folder_id = $2,
|
|
7627
|
+
updated_at = $3,
|
|
7628
|
+
updated_by_user_id = $4
|
|
7629
|
+
WHERE id = $5 AND collection_id = $6`,
|
|
7630
|
+
[index, folderId, updatedAt, actingUserId, orderedRequestIds[index], collectionId]
|
|
7631
|
+
);
|
|
7632
|
+
}
|
|
7633
|
+
await client.query("COMMIT");
|
|
7634
|
+
} catch (err) {
|
|
7635
|
+
await client.query("ROLLBACK");
|
|
7636
|
+
throw err;
|
|
7637
|
+
} finally {
|
|
7638
|
+
client.release();
|
|
7639
|
+
}
|
|
7640
|
+
await this.recordAuditEntry(actingUserId, "reorder", "request", collectionId, {
|
|
7641
|
+
folderId,
|
|
7642
|
+
orderedRequestIds
|
|
7643
|
+
});
|
|
7644
|
+
}
|
|
7645
|
+
/**
|
|
7646
|
+
* Moves a request to another folder or collection root at a given index.
|
|
7647
|
+
*
|
|
7648
|
+
* @param actingUserId - User performing the move action.
|
|
7649
|
+
*/
|
|
7650
|
+
async moveRequest(requestId, folderId, index, actingUserId) {
|
|
7651
|
+
const client = await this.requirePool().connect();
|
|
7652
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
7653
|
+
const listInContainer = async (collectionId, targetFolderId) => {
|
|
7654
|
+
const result = await client.query(
|
|
7655
|
+
`SELECT id FROM requests WHERE collection_id = $1
|
|
7656
|
+
AND (($2::text IS NULL AND folder_id IS NULL) OR folder_id = $2)
|
|
7657
|
+
ORDER BY sort_order ASC, name ASC`,
|
|
7658
|
+
[collectionId, targetFolderId]
|
|
7659
|
+
);
|
|
7660
|
+
return result.rows.map((row) => row.id);
|
|
7661
|
+
};
|
|
7662
|
+
const reindexContainer = async (targetFolderId, orderedIds) => {
|
|
7663
|
+
for (let sortIndex = 0; sortIndex < orderedIds.length; sortIndex++) {
|
|
7664
|
+
await client.query(
|
|
7665
|
+
`UPDATE requests
|
|
7666
|
+
SET sort_order = $1,
|
|
7667
|
+
folder_id = $2,
|
|
7668
|
+
updated_at = $3,
|
|
7669
|
+
updated_by_user_id = $4
|
|
7670
|
+
WHERE id = $5`,
|
|
7671
|
+
[sortIndex, targetFolderId, updatedAt, actingUserId, orderedIds[sortIndex]]
|
|
7672
|
+
);
|
|
7673
|
+
}
|
|
7674
|
+
};
|
|
7675
|
+
try {
|
|
7676
|
+
await client.query("BEGIN");
|
|
7677
|
+
const requestResult = await client.query(`${REQUEST_SELECT2} WHERE id = $1`, [
|
|
7678
|
+
requestId
|
|
7679
|
+
]);
|
|
7680
|
+
const requestRow = requestResult.rows[0];
|
|
7681
|
+
if (!requestRow) {
|
|
7682
|
+
throw new Error("Request not found");
|
|
7683
|
+
}
|
|
7684
|
+
const request = mapRequestSqlRow(requestRow);
|
|
7685
|
+
const collectionId = request.collectionId;
|
|
7686
|
+
const oldFolderId = request.folderId;
|
|
7687
|
+
if (folderId != null) {
|
|
7688
|
+
const folderResult = await client.query(
|
|
7689
|
+
"SELECT collection_id FROM folders WHERE id = $1",
|
|
7690
|
+
[folderId]
|
|
7691
|
+
);
|
|
7692
|
+
const folderRow = folderResult.rows[0];
|
|
7693
|
+
if (!folderRow || folderRow.collection_id !== collectionId) {
|
|
7694
|
+
throw new Error("Folder not found");
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7697
|
+
if (oldFolderId === folderId) {
|
|
7698
|
+
const siblings = (await listInContainer(collectionId, folderId)).filter(
|
|
7699
|
+
(id) => id !== requestId
|
|
7700
|
+
);
|
|
7701
|
+
siblings.splice(index, 0, requestId);
|
|
7702
|
+
await reindexContainer(folderId, siblings);
|
|
7703
|
+
} else {
|
|
7704
|
+
const oldIds = (await listInContainer(collectionId, oldFolderId)).filter(
|
|
7705
|
+
(id) => id !== requestId
|
|
7706
|
+
);
|
|
7707
|
+
await reindexContainer(oldFolderId, oldIds);
|
|
7708
|
+
const newIds = (await listInContainer(collectionId, folderId)).filter(
|
|
7709
|
+
(id) => id !== requestId
|
|
7710
|
+
);
|
|
7711
|
+
newIds.splice(index, 0, requestId);
|
|
7712
|
+
await reindexContainer(folderId, newIds);
|
|
7713
|
+
}
|
|
7714
|
+
await client.query("COMMIT");
|
|
7715
|
+
} catch (err) {
|
|
7716
|
+
await client.query("ROLLBACK");
|
|
7717
|
+
throw err;
|
|
7718
|
+
} finally {
|
|
7719
|
+
client.release();
|
|
7720
|
+
}
|
|
7721
|
+
await this.recordAuditEntry(actingUserId, "move", "request", requestId, {
|
|
7722
|
+
folderId,
|
|
7723
|
+
index
|
|
7724
|
+
});
|
|
7725
|
+
}
|
|
7726
|
+
/**
|
|
7727
|
+
* Lists all documents in a collection.
|
|
6504
7728
|
*
|
|
6505
7729
|
* @param collectionId - Collection to query.
|
|
6506
7730
|
*/
|
|
6507
|
-
async
|
|
7731
|
+
async listDocuments(collectionId) {
|
|
6508
7732
|
const result = await this.query(
|
|
6509
|
-
`${
|
|
7733
|
+
`${DOCUMENT_SELECT2} WHERE collection_id = $1 ORDER BY sort_order ASC, name ASC`,
|
|
6510
7734
|
[collectionId]
|
|
6511
7735
|
);
|
|
6512
|
-
return result.rows.map(
|
|
7736
|
+
return result.rows.map(mapDocumentSqlRow);
|
|
6513
7737
|
}
|
|
6514
7738
|
/**
|
|
6515
|
-
* Finds a
|
|
7739
|
+
* Finds a document by id.
|
|
6516
7740
|
*
|
|
6517
|
-
* @param id -
|
|
7741
|
+
* @param id - Document identifier to look up.
|
|
6518
7742
|
*/
|
|
6519
|
-
async
|
|
6520
|
-
const result = await this.query(`${
|
|
7743
|
+
async findDocumentById(id) {
|
|
7744
|
+
const result = await this.query(`${DOCUMENT_SELECT2} WHERE id = $1 LIMIT 1`, [
|
|
7745
|
+
id
|
|
7746
|
+
]);
|
|
6521
7747
|
const row = result.rows[0];
|
|
6522
|
-
return row ?
|
|
7748
|
+
return row ? mapDocumentSqlRow(row) : null;
|
|
6523
7749
|
}
|
|
6524
7750
|
/**
|
|
6525
|
-
*
|
|
7751
|
+
* Inserts a new document or updates an existing one.
|
|
6526
7752
|
*
|
|
6527
|
-
* @param
|
|
6528
|
-
* @param
|
|
6529
|
-
* @param actingUserId - User performing the create action.
|
|
7753
|
+
* @param input - Document fields to persist.
|
|
7754
|
+
* @param actingUserId - User performing the save action.
|
|
6530
7755
|
*/
|
|
6531
|
-
async
|
|
6532
|
-
const trimmedName = trimRequiredName(name, "
|
|
6533
|
-
const
|
|
7756
|
+
async saveDocument(input, actingUserId) {
|
|
7757
|
+
const trimmedName = trimRequiredName(input.name, "Document name");
|
|
7758
|
+
const folderId = input.folderId ?? null;
|
|
7759
|
+
const serializedMarker = serializeSidebarMarker(input.marker ?? null);
|
|
6534
7760
|
const now = /* @__PURE__ */ new Date();
|
|
7761
|
+
if (folderId != null) {
|
|
7762
|
+
const folderResult = await this.query(
|
|
7763
|
+
"SELECT collection_id FROM folders WHERE id = $1",
|
|
7764
|
+
[folderId]
|
|
7765
|
+
);
|
|
7766
|
+
const folderRow = folderResult.rows[0];
|
|
7767
|
+
if (!folderRow || folderRow.collection_id !== input.collectionId) {
|
|
7768
|
+
throw new Error("Folder not found");
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
if (input.id) {
|
|
7772
|
+
const result2 = await this.query(
|
|
7773
|
+
`UPDATE documents SET
|
|
7774
|
+
collection_id = $1,
|
|
7775
|
+
folder_id = $2,
|
|
7776
|
+
name = $3,
|
|
7777
|
+
content = $4,
|
|
7778
|
+
marker = $5,
|
|
7779
|
+
updated_at = $6,
|
|
7780
|
+
updated_by_user_id = $7
|
|
7781
|
+
WHERE id = $8`,
|
|
7782
|
+
[
|
|
7783
|
+
input.collectionId,
|
|
7784
|
+
folderId,
|
|
7785
|
+
trimmedName,
|
|
7786
|
+
input.content,
|
|
7787
|
+
serializedMarker,
|
|
7788
|
+
now,
|
|
7789
|
+
actingUserId,
|
|
7790
|
+
input.id
|
|
7791
|
+
]
|
|
7792
|
+
);
|
|
7793
|
+
if ((result2.rowCount ?? 0) > 0) {
|
|
7794
|
+
await this.recordAuditEntry(actingUserId, "update", "document", input.id);
|
|
7795
|
+
const selectResult = await this.query(`${DOCUMENT_SELECT2} WHERE id = $1`, [
|
|
7796
|
+
input.id
|
|
7797
|
+
]);
|
|
7798
|
+
const row2 = selectResult.rows[0];
|
|
7799
|
+
if (row2) {
|
|
7800
|
+
return mapDocumentSqlRow(row2);
|
|
7801
|
+
}
|
|
7802
|
+
}
|
|
7803
|
+
}
|
|
6535
7804
|
const maxResult = await this.query(
|
|
6536
|
-
|
|
6537
|
-
|
|
7805
|
+
`SELECT COALESCE(MAX(sort_order), -1) AS max_order FROM documents
|
|
7806
|
+
WHERE collection_id = $1
|
|
7807
|
+
AND (($2::text IS NULL AND folder_id IS NULL) OR folder_id = $2)`,
|
|
7808
|
+
[input.collectionId, folderId]
|
|
6538
7809
|
);
|
|
6539
7810
|
const maxOrder = maxResult.rows[0]?.max_order ?? -1;
|
|
7811
|
+
const id = randomUUID5();
|
|
6540
7812
|
const result = await this.query(
|
|
6541
|
-
`INSERT INTO
|
|
7813
|
+
`INSERT INTO documents (
|
|
6542
7814
|
id,
|
|
6543
7815
|
collection_id,
|
|
7816
|
+
folder_id,
|
|
6544
7817
|
name,
|
|
7818
|
+
content,
|
|
7819
|
+
marker,
|
|
6545
7820
|
sort_order,
|
|
6546
7821
|
created_at,
|
|
6547
7822
|
updated_at,
|
|
6548
7823
|
created_by_user_id,
|
|
6549
7824
|
updated_by_user_id
|
|
6550
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
6551
|
-
RETURNING ${
|
|
6552
|
-
[
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
* @param name - New display name.
|
|
6566
|
-
* @param actingUserId - User performing the rename action.
|
|
6567
|
-
*/
|
|
6568
|
-
async renameFolder(id, name, actingUserId) {
|
|
6569
|
-
const trimmedName = trimRequiredName(name, "Folder name");
|
|
6570
|
-
const updatedAt = /* @__PURE__ */ new Date();
|
|
6571
|
-
const result = await this.query(
|
|
6572
|
-
`UPDATE folders
|
|
6573
|
-
SET name = $1,
|
|
6574
|
-
updated_at = $2,
|
|
6575
|
-
updated_by_user_id = $3
|
|
6576
|
-
WHERE id = $4
|
|
6577
|
-
RETURNING ${FOLDER_SELECT_COLUMNS}`,
|
|
6578
|
-
[trimmedName, updatedAt, actingUserId, id]
|
|
7825
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
7826
|
+
RETURNING ${DOCUMENT_SELECT_COLUMNS}`,
|
|
7827
|
+
[
|
|
7828
|
+
id,
|
|
7829
|
+
input.collectionId,
|
|
7830
|
+
folderId,
|
|
7831
|
+
trimmedName,
|
|
7832
|
+
input.content,
|
|
7833
|
+
serializedMarker,
|
|
7834
|
+
maxOrder + 1,
|
|
7835
|
+
now,
|
|
7836
|
+
now,
|
|
7837
|
+
actingUserId,
|
|
7838
|
+
actingUserId
|
|
7839
|
+
]
|
|
6579
7840
|
);
|
|
6580
7841
|
const row = result.rows[0];
|
|
6581
7842
|
if (!row) {
|
|
6582
|
-
throw new Error("
|
|
7843
|
+
throw new Error("Document not found after insert");
|
|
6583
7844
|
}
|
|
6584
|
-
await this.recordAuditEntry(actingUserId, "
|
|
6585
|
-
return
|
|
7845
|
+
await this.recordAuditEntry(actingUserId, "create", "document", id);
|
|
7846
|
+
return mapDocumentSqlRow(row);
|
|
6586
7847
|
}
|
|
6587
7848
|
/**
|
|
6588
|
-
* Deletes a
|
|
7849
|
+
* Deletes a document by ID.
|
|
6589
7850
|
*
|
|
6590
|
-
* @param id -
|
|
7851
|
+
* @param id - Document ID to delete.
|
|
6591
7852
|
* @param actingUserId - User performing the delete action.
|
|
6592
7853
|
*/
|
|
6593
|
-
async
|
|
6594
|
-
await this.recordAuditEntry(actingUserId, "delete", "
|
|
6595
|
-
|
|
6596
|
-
try {
|
|
6597
|
-
await client.query("BEGIN");
|
|
6598
|
-
await client.query("DELETE FROM requests WHERE folder_id = $1", [id]);
|
|
6599
|
-
await client.query("DELETE FROM folders WHERE id = $1", [id]);
|
|
6600
|
-
await client.query("COMMIT");
|
|
6601
|
-
} catch (err) {
|
|
6602
|
-
await client.query("ROLLBACK");
|
|
6603
|
-
throw err;
|
|
6604
|
-
} finally {
|
|
6605
|
-
client.release();
|
|
6606
|
-
}
|
|
6607
|
-
}
|
|
6608
|
-
/**
|
|
6609
|
-
* Reorders folders within a collection.
|
|
6610
|
-
*
|
|
6611
|
-
* @param collectionId - Collection containing the folders.
|
|
6612
|
-
* @param orderedFolderIds - Folder IDs in desired order.
|
|
6613
|
-
* @param actingUserId - User performing the reorder action.
|
|
6614
|
-
*/
|
|
6615
|
-
async reorderFolders(collectionId, orderedFolderIds, actingUserId) {
|
|
6616
|
-
const client = await this.requirePool().connect();
|
|
6617
|
-
const updatedAt = /* @__PURE__ */ new Date();
|
|
6618
|
-
try {
|
|
6619
|
-
await client.query("BEGIN");
|
|
6620
|
-
for (let index = 0; index < orderedFolderIds.length; index++) {
|
|
6621
|
-
await client.query(
|
|
6622
|
-
`UPDATE folders
|
|
6623
|
-
SET sort_order = $1,
|
|
6624
|
-
updated_at = $2,
|
|
6625
|
-
updated_by_user_id = $3
|
|
6626
|
-
WHERE id = $4 AND collection_id = $5`,
|
|
6627
|
-
[index, updatedAt, actingUserId, orderedFolderIds[index], collectionId]
|
|
6628
|
-
);
|
|
6629
|
-
}
|
|
6630
|
-
await client.query("COMMIT");
|
|
6631
|
-
} catch (err) {
|
|
6632
|
-
await client.query("ROLLBACK");
|
|
6633
|
-
throw err;
|
|
6634
|
-
} finally {
|
|
6635
|
-
client.release();
|
|
6636
|
-
}
|
|
6637
|
-
await this.recordAuditEntry(actingUserId, "reorder", "folder", collectionId, {
|
|
6638
|
-
orderedFolderIds
|
|
6639
|
-
});
|
|
7854
|
+
async deleteDocument(id, actingUserId) {
|
|
7855
|
+
await this.recordAuditEntry(actingUserId, "delete", "document", id);
|
|
7856
|
+
await this.query("DELETE FROM documents WHERE id = $1", [id]);
|
|
6640
7857
|
}
|
|
6641
7858
|
/**
|
|
6642
|
-
* Reorders
|
|
7859
|
+
* Reorders documents within a folder or at collection root.
|
|
6643
7860
|
*
|
|
6644
7861
|
* @param actingUserId - User performing the reorder action.
|
|
6645
7862
|
*/
|
|
6646
|
-
async
|
|
7863
|
+
async reorderDocuments(collectionId, folderId, orderedDocumentIds, actingUserId) {
|
|
6647
7864
|
const client = await this.requirePool().connect();
|
|
6648
7865
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
6649
7866
|
try {
|
|
6650
7867
|
await client.query("BEGIN");
|
|
6651
|
-
for (let index = 0; index <
|
|
7868
|
+
for (let index = 0; index < orderedDocumentIds.length; index++) {
|
|
6652
7869
|
await client.query(
|
|
6653
|
-
`UPDATE
|
|
7870
|
+
`UPDATE documents
|
|
6654
7871
|
SET sort_order = $1,
|
|
6655
7872
|
folder_id = $2,
|
|
6656
7873
|
updated_at = $3,
|
|
6657
7874
|
updated_by_user_id = $4
|
|
6658
7875
|
WHERE id = $5 AND collection_id = $6`,
|
|
6659
|
-
[index, folderId, updatedAt, actingUserId,
|
|
7876
|
+
[index, folderId, updatedAt, actingUserId, orderedDocumentIds[index], collectionId]
|
|
6660
7877
|
);
|
|
6661
7878
|
}
|
|
6662
7879
|
await client.query("COMMIT");
|
|
@@ -6666,22 +7883,22 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6666
7883
|
} finally {
|
|
6667
7884
|
client.release();
|
|
6668
7885
|
}
|
|
6669
|
-
await this.recordAuditEntry(actingUserId, "reorder", "
|
|
7886
|
+
await this.recordAuditEntry(actingUserId, "reorder", "document", collectionId, {
|
|
6670
7887
|
folderId,
|
|
6671
|
-
|
|
7888
|
+
orderedDocumentIds
|
|
6672
7889
|
});
|
|
6673
7890
|
}
|
|
6674
7891
|
/**
|
|
6675
|
-
* Moves a
|
|
7892
|
+
* Moves a document to another folder or collection root at a given index.
|
|
6676
7893
|
*
|
|
6677
7894
|
* @param actingUserId - User performing the move action.
|
|
6678
7895
|
*/
|
|
6679
|
-
async
|
|
7896
|
+
async moveDocument(documentId, folderId, index, actingUserId) {
|
|
6680
7897
|
const client = await this.requirePool().connect();
|
|
6681
7898
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
6682
7899
|
const listInContainer = async (collectionId, targetFolderId) => {
|
|
6683
7900
|
const result = await client.query(
|
|
6684
|
-
`SELECT id FROM
|
|
7901
|
+
`SELECT id FROM documents WHERE collection_id = $1
|
|
6685
7902
|
AND (($2::text IS NULL AND folder_id IS NULL) OR folder_id = $2)
|
|
6686
7903
|
ORDER BY sort_order ASC, name ASC`,
|
|
6687
7904
|
[collectionId, targetFolderId]
|
|
@@ -6691,7 +7908,7 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6691
7908
|
const reindexContainer = async (targetFolderId, orderedIds) => {
|
|
6692
7909
|
for (let sortIndex = 0; sortIndex < orderedIds.length; sortIndex++) {
|
|
6693
7910
|
await client.query(
|
|
6694
|
-
`UPDATE
|
|
7911
|
+
`UPDATE documents
|
|
6695
7912
|
SET sort_order = $1,
|
|
6696
7913
|
folder_id = $2,
|
|
6697
7914
|
updated_at = $3,
|
|
@@ -6703,16 +7920,17 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6703
7920
|
};
|
|
6704
7921
|
try {
|
|
6705
7922
|
await client.query("BEGIN");
|
|
6706
|
-
const
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
7923
|
+
const documentResult = await client.query(
|
|
7924
|
+
`${DOCUMENT_SELECT2} WHERE id = $1`,
|
|
7925
|
+
[documentId]
|
|
7926
|
+
);
|
|
7927
|
+
const documentRow = documentResult.rows[0];
|
|
7928
|
+
if (!documentRow) {
|
|
7929
|
+
throw new Error("Document not found");
|
|
6712
7930
|
}
|
|
6713
|
-
const
|
|
6714
|
-
const collectionId =
|
|
6715
|
-
const oldFolderId =
|
|
7931
|
+
const document = mapDocumentSqlRow(documentRow);
|
|
7932
|
+
const collectionId = document.collectionId;
|
|
7933
|
+
const oldFolderId = document.folderId;
|
|
6716
7934
|
if (folderId != null) {
|
|
6717
7935
|
const folderResult = await client.query(
|
|
6718
7936
|
"SELECT collection_id FROM folders WHERE id = $1",
|
|
@@ -6725,19 +7943,19 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6725
7943
|
}
|
|
6726
7944
|
if (oldFolderId === folderId) {
|
|
6727
7945
|
const siblings = (await listInContainer(collectionId, folderId)).filter(
|
|
6728
|
-
(id) => id !==
|
|
7946
|
+
(id) => id !== documentId
|
|
6729
7947
|
);
|
|
6730
|
-
siblings.splice(index, 0,
|
|
7948
|
+
siblings.splice(index, 0, documentId);
|
|
6731
7949
|
await reindexContainer(folderId, siblings);
|
|
6732
7950
|
} else {
|
|
6733
7951
|
const oldIds = (await listInContainer(collectionId, oldFolderId)).filter(
|
|
6734
|
-
(id) => id !==
|
|
7952
|
+
(id) => id !== documentId
|
|
6735
7953
|
);
|
|
6736
7954
|
await reindexContainer(oldFolderId, oldIds);
|
|
6737
7955
|
const newIds = (await listInContainer(collectionId, folderId)).filter(
|
|
6738
|
-
(id) => id !==
|
|
7956
|
+
(id) => id !== documentId
|
|
6739
7957
|
);
|
|
6740
|
-
newIds.splice(index, 0,
|
|
7958
|
+
newIds.splice(index, 0, documentId);
|
|
6741
7959
|
await reindexContainer(folderId, newIds);
|
|
6742
7960
|
}
|
|
6743
7961
|
await client.query("COMMIT");
|
|
@@ -6747,7 +7965,7 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
6747
7965
|
} finally {
|
|
6748
7966
|
client.release();
|
|
6749
7967
|
}
|
|
6750
|
-
await this.recordAuditEntry(actingUserId, "move", "
|
|
7968
|
+
await this.recordAuditEntry(actingUserId, "move", "document", documentId, {
|
|
6751
7969
|
folderId,
|
|
6752
7970
|
index
|
|
6753
7971
|
});
|
|
@@ -8160,6 +9378,9 @@ function canDeleteCollection(user, collection) {
|
|
|
8160
9378
|
function canDeleteRequest(user, request) {
|
|
8161
9379
|
return canUseDataApi(user) && canAccessCollection(user, request.collectionId) && request.createdByUserId === user.id;
|
|
8162
9380
|
}
|
|
9381
|
+
function canDeleteDocument(user, document) {
|
|
9382
|
+
return canUseDataApi(user) && canAccessCollection(user, document.collectionId) && document.createdByUserId === user.id;
|
|
9383
|
+
}
|
|
8163
9384
|
function canDeleteRunResult(user, runResult) {
|
|
8164
9385
|
return canUseDataApi(user) && runResult.createdByUserId === user.id;
|
|
8165
9386
|
}
|
|
@@ -8418,6 +9639,7 @@ var llmUsageSummaryResponseSchema = z7.object({
|
|
|
8418
9639
|
|
|
8419
9640
|
// src/server/routes/schemas/entities.ts
|
|
8420
9641
|
import { z as z8 } from "zod/v4";
|
|
9642
|
+
var sidebarMarkerBodySchema = z8.union([z8.string().trim().min(1), z8.null()]).optional();
|
|
8421
9643
|
var collectionRecordSchema = z8.object({
|
|
8422
9644
|
id: z8.string(),
|
|
8423
9645
|
name: z8.string(),
|
|
@@ -8430,7 +9652,8 @@ var collectionRecordSchema = z8.object({
|
|
|
8430
9652
|
updatedAt: timestampSchema,
|
|
8431
9653
|
createdByUserId: z8.string().nullable(),
|
|
8432
9654
|
updatedByUserId: z8.string().nullable(),
|
|
8433
|
-
deletionLocked: z8.boolean()
|
|
9655
|
+
deletionLocked: z8.boolean(),
|
|
9656
|
+
marker: z8.string().nullable()
|
|
8434
9657
|
});
|
|
8435
9658
|
var environmentRecordSchema = z8.object({
|
|
8436
9659
|
id: z8.string(),
|
|
@@ -8440,7 +9663,8 @@ var environmentRecordSchema = z8.object({
|
|
|
8440
9663
|
updatedAt: timestampSchema,
|
|
8441
9664
|
createdByUserId: z8.string().nullable(),
|
|
8442
9665
|
updatedByUserId: z8.string().nullable(),
|
|
8443
|
-
deletionLocked: z8.boolean()
|
|
9666
|
+
deletionLocked: z8.boolean(),
|
|
9667
|
+
marker: z8.string().nullable()
|
|
8444
9668
|
});
|
|
8445
9669
|
var snippetScopeSchema = z8.enum(["pre-request", "post-request", "any"]);
|
|
8446
9670
|
var snippetRecordSchema = z8.object({
|
|
@@ -8458,12 +9682,14 @@ var snippetRecordSchema = z8.object({
|
|
|
8458
9682
|
var folderRecordSchema = z8.object({
|
|
8459
9683
|
id: z8.string(),
|
|
8460
9684
|
collectionId: z8.string(),
|
|
9685
|
+
parentFolderId: z8.string().nullable(),
|
|
8461
9686
|
name: z8.string(),
|
|
8462
9687
|
sortOrder: z8.number().int(),
|
|
8463
9688
|
createdAt: timestampSchema,
|
|
8464
9689
|
updatedAt: timestampSchema,
|
|
8465
9690
|
createdByUserId: z8.string().nullable(),
|
|
8466
|
-
updatedByUserId: z8.string().nullable()
|
|
9691
|
+
updatedByUserId: z8.string().nullable(),
|
|
9692
|
+
marker: z8.string().nullable()
|
|
8467
9693
|
});
|
|
8468
9694
|
var savedRequestRecordSchema = z8.object({
|
|
8469
9695
|
id: z8.string(),
|
|
@@ -8484,10 +9710,12 @@ var savedRequestRecordSchema = z8.object({
|
|
|
8484
9710
|
createdAt: timestampSchema,
|
|
8485
9711
|
updatedAt: timestampSchema,
|
|
8486
9712
|
createdByUserId: z8.string().nullable(),
|
|
8487
|
-
updatedByUserId: z8.string().nullable()
|
|
9713
|
+
updatedByUserId: z8.string().nullable(),
|
|
9714
|
+
marker: z8.string().nullable()
|
|
8488
9715
|
});
|
|
8489
9716
|
var createCollectionBodySchema = z8.object({
|
|
8490
|
-
name: z8.string().trim().min(1)
|
|
9717
|
+
name: z8.string().trim().min(1),
|
|
9718
|
+
marker: sidebarMarkerBodySchema
|
|
8491
9719
|
});
|
|
8492
9720
|
var updateCollectionBodySchema = z8.object({
|
|
8493
9721
|
name: z8.string().trim().min(1),
|
|
@@ -8495,14 +9723,17 @@ var updateCollectionBodySchema = z8.object({
|
|
|
8495
9723
|
headers: z8.array(keyValueSchema),
|
|
8496
9724
|
preRequestScript: z8.string(),
|
|
8497
9725
|
postRequestScript: z8.string(),
|
|
8498
|
-
auth: authConfigSchema
|
|
9726
|
+
auth: authConfigSchema,
|
|
9727
|
+
marker: sidebarMarkerBodySchema
|
|
8499
9728
|
});
|
|
8500
9729
|
var createEnvironmentBodySchema = z8.object({
|
|
8501
|
-
name: z8.string().trim().min(1)
|
|
9730
|
+
name: z8.string().trim().min(1),
|
|
9731
|
+
marker: sidebarMarkerBodySchema
|
|
8502
9732
|
});
|
|
8503
9733
|
var updateEnvironmentBodySchema = z8.object({
|
|
8504
9734
|
name: z8.string().trim().min(1),
|
|
8505
|
-
variables: z8.array(variableSchema)
|
|
9735
|
+
variables: z8.array(variableSchema),
|
|
9736
|
+
marker: sidebarMarkerBodySchema
|
|
8506
9737
|
});
|
|
8507
9738
|
var createSnippetBodySchema = z8.object({
|
|
8508
9739
|
name: z8.string().trim().min(1),
|
|
@@ -8515,14 +9746,22 @@ var updateSnippetBodySchema = z8.object({
|
|
|
8515
9746
|
scope: snippetScopeSchema
|
|
8516
9747
|
});
|
|
8517
9748
|
var createFolderBodySchema = z8.object({
|
|
8518
|
-
name: z8.string().trim().min(1)
|
|
9749
|
+
name: z8.string().trim().min(1),
|
|
9750
|
+
parentFolderId: z8.string().nullable().optional(),
|
|
9751
|
+
marker: sidebarMarkerBodySchema
|
|
8519
9752
|
});
|
|
8520
9753
|
var renameFolderBodySchema = z8.object({
|
|
8521
|
-
name: z8.string().trim().min(1)
|
|
9754
|
+
name: z8.string().trim().min(1),
|
|
9755
|
+
marker: sidebarMarkerBodySchema
|
|
8522
9756
|
});
|
|
8523
9757
|
var reorderFoldersBodySchema = z8.object({
|
|
9758
|
+
parentFolderId: z8.string().nullable(),
|
|
8524
9759
|
orderedFolderIds: z8.array(z8.string().trim().min(1))
|
|
8525
9760
|
});
|
|
9761
|
+
var moveFolderBodySchema = z8.object({
|
|
9762
|
+
parentFolderId: z8.string().nullable(),
|
|
9763
|
+
sortOrder: z8.number().int().min(0).optional()
|
|
9764
|
+
});
|
|
8526
9765
|
var saveRequestBodySchema = z8.object({
|
|
8527
9766
|
name: z8.string().trim().min(1),
|
|
8528
9767
|
method: httpMethodSchema,
|
|
@@ -8535,7 +9774,8 @@ var saveRequestBodySchema = z8.object({
|
|
|
8535
9774
|
preRequestScript: z8.string(),
|
|
8536
9775
|
postRequestScript: z8.string(),
|
|
8537
9776
|
comment: z8.string(),
|
|
8538
|
-
folderId: z8.string().nullable().optional()
|
|
9777
|
+
folderId: z8.string().nullable().optional(),
|
|
9778
|
+
marker: sidebarMarkerBodySchema
|
|
8539
9779
|
});
|
|
8540
9780
|
var updateSaveRequestBodySchema = saveRequestBodySchema.extend({
|
|
8541
9781
|
collectionId: z8.string().trim().min(1)
|
|
@@ -8563,6 +9803,39 @@ var listFoldersResponseSchema = z8.object({
|
|
|
8563
9803
|
var listRequestsResponseSchema = z8.object({
|
|
8564
9804
|
requests: z8.array(savedRequestRecordSchema)
|
|
8565
9805
|
});
|
|
9806
|
+
var documentRecordSchema = z8.object({
|
|
9807
|
+
id: z8.string(),
|
|
9808
|
+
collectionId: z8.string(),
|
|
9809
|
+
name: z8.string(),
|
|
9810
|
+
content: z8.string(),
|
|
9811
|
+
folderId: z8.string().nullable(),
|
|
9812
|
+
sortOrder: z8.number().int(),
|
|
9813
|
+
createdAt: timestampSchema,
|
|
9814
|
+
updatedAt: timestampSchema,
|
|
9815
|
+
createdByUserId: z8.string().nullable(),
|
|
9816
|
+
updatedByUserId: z8.string().nullable(),
|
|
9817
|
+
marker: z8.string().nullable()
|
|
9818
|
+
});
|
|
9819
|
+
var saveDocumentBodySchema = z8.object({
|
|
9820
|
+
name: z8.string().trim().min(1),
|
|
9821
|
+
content: z8.string(),
|
|
9822
|
+
folderId: z8.string().nullable().optional(),
|
|
9823
|
+
marker: sidebarMarkerBodySchema
|
|
9824
|
+
});
|
|
9825
|
+
var updateSaveDocumentBodySchema = saveDocumentBodySchema.extend({
|
|
9826
|
+
collectionId: z8.string().trim().min(1)
|
|
9827
|
+
});
|
|
9828
|
+
var reorderDocumentsBodySchema = z8.object({
|
|
9829
|
+
folderId: z8.string().nullable(),
|
|
9830
|
+
orderedDocumentIds: z8.array(z8.string().trim().min(1))
|
|
9831
|
+
});
|
|
9832
|
+
var moveDocumentBodySchema = z8.object({
|
|
9833
|
+
folderId: z8.string().nullable(),
|
|
9834
|
+
index: z8.number().int().min(0)
|
|
9835
|
+
});
|
|
9836
|
+
var listDocumentsResponseSchema = z8.object({
|
|
9837
|
+
documents: z8.array(documentRecordSchema)
|
|
9838
|
+
});
|
|
8566
9839
|
var emptyResponseSchema = z8.null();
|
|
8567
9840
|
function serializeCollection(record) {
|
|
8568
9841
|
return {
|
|
@@ -8599,6 +9872,13 @@ function serializeSavedRequest(record) {
|
|
|
8599
9872
|
updatedAt: record.updatedAt.toISOString()
|
|
8600
9873
|
};
|
|
8601
9874
|
}
|
|
9875
|
+
function serializeDocument(record) {
|
|
9876
|
+
return {
|
|
9877
|
+
...record,
|
|
9878
|
+
createdAt: record.createdAt.toISOString(),
|
|
9879
|
+
updatedAt: record.updatedAt.toISOString()
|
|
9880
|
+
};
|
|
9881
|
+
}
|
|
8602
9882
|
var runResultRecordSchema = z8.object({
|
|
8603
9883
|
id: z8.string(),
|
|
8604
9884
|
kind: z8.enum(["collection-run-results", "request-run-results"]),
|
|
@@ -9193,13 +10473,50 @@ async function registerAdminRoutes(app, options) {
|
|
|
9193
10473
|
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
9194
10474
|
return;
|
|
9195
10475
|
}
|
|
9196
|
-
const collections = await db.listCollections();
|
|
10476
|
+
const collections = await db.listCollections();
|
|
10477
|
+
return reply.send({
|
|
10478
|
+
collections: collections.map((collection) => ({
|
|
10479
|
+
id: collection.id,
|
|
10480
|
+
name: collection.name,
|
|
10481
|
+
deletionLocked: collection.deletionLocked
|
|
10482
|
+
}))
|
|
10483
|
+
});
|
|
10484
|
+
} catch (error) {
|
|
10485
|
+
if (handleDbError(reply, error)) {
|
|
10486
|
+
return;
|
|
10487
|
+
}
|
|
10488
|
+
throw error;
|
|
10489
|
+
}
|
|
10490
|
+
}
|
|
10491
|
+
});
|
|
10492
|
+
routes.route({
|
|
10493
|
+
method: "GET",
|
|
10494
|
+
url: "/admin/collections/:collectionId/folders",
|
|
10495
|
+
schema: {
|
|
10496
|
+
params: collectionIdParamSchema,
|
|
10497
|
+
response: {
|
|
10498
|
+
200: listFoldersResponseSchema,
|
|
10499
|
+
403: errorResponseSchema,
|
|
10500
|
+
404: errorResponseSchema
|
|
10501
|
+
}
|
|
10502
|
+
},
|
|
10503
|
+
/**
|
|
10504
|
+
* Lists folders in a collection for operator inspection.
|
|
10505
|
+
*/
|
|
10506
|
+
handler: async (request, reply) => {
|
|
10507
|
+
try {
|
|
10508
|
+
const user = requireAuthenticatedUser(request);
|
|
10509
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
10510
|
+
return;
|
|
10511
|
+
}
|
|
10512
|
+
const collection = await db.findCollectionById(request.params.collectionId);
|
|
10513
|
+
if (!collection) {
|
|
10514
|
+
void reply.code(404).send({ error: "Collection not found" });
|
|
10515
|
+
return;
|
|
10516
|
+
}
|
|
10517
|
+
const folders = await db.listFolders(request.params.collectionId);
|
|
9197
10518
|
return reply.send({
|
|
9198
|
-
|
|
9199
|
-
id: collection.id,
|
|
9200
|
-
name: collection.name,
|
|
9201
|
-
deletionLocked: collection.deletionLocked
|
|
9202
|
-
}))
|
|
10519
|
+
folders: folders.map((folder) => serializeFolder(folder))
|
|
9203
10520
|
});
|
|
9204
10521
|
} catch (error) {
|
|
9205
10522
|
if (handleDbError(reply, error)) {
|
|
@@ -9211,17 +10528,17 @@ async function registerAdminRoutes(app, options) {
|
|
|
9211
10528
|
});
|
|
9212
10529
|
routes.route({
|
|
9213
10530
|
method: "GET",
|
|
9214
|
-
url: "/admin/collections/:collectionId/
|
|
10531
|
+
url: "/admin/collections/:collectionId/requests",
|
|
9215
10532
|
schema: {
|
|
9216
10533
|
params: collectionIdParamSchema,
|
|
9217
10534
|
response: {
|
|
9218
|
-
200:
|
|
10535
|
+
200: listRequestsResponseSchema,
|
|
9219
10536
|
403: errorResponseSchema,
|
|
9220
10537
|
404: errorResponseSchema
|
|
9221
10538
|
}
|
|
9222
10539
|
},
|
|
9223
10540
|
/**
|
|
9224
|
-
* Lists
|
|
10541
|
+
* Lists saved requests in a collection for operator inspection.
|
|
9225
10542
|
*/
|
|
9226
10543
|
handler: async (request, reply) => {
|
|
9227
10544
|
try {
|
|
@@ -9234,9 +10551,9 @@ async function registerAdminRoutes(app, options) {
|
|
|
9234
10551
|
void reply.code(404).send({ error: "Collection not found" });
|
|
9235
10552
|
return;
|
|
9236
10553
|
}
|
|
9237
|
-
const
|
|
10554
|
+
const requests = await db.listRequests(request.params.collectionId);
|
|
9238
10555
|
return reply.send({
|
|
9239
|
-
|
|
10556
|
+
requests: requests.map((savedRequest) => serializeSavedRequest(savedRequest))
|
|
9240
10557
|
});
|
|
9241
10558
|
} catch (error) {
|
|
9242
10559
|
if (handleDbError(reply, error)) {
|
|
@@ -9248,17 +10565,17 @@ async function registerAdminRoutes(app, options) {
|
|
|
9248
10565
|
});
|
|
9249
10566
|
routes.route({
|
|
9250
10567
|
method: "GET",
|
|
9251
|
-
url: "/admin/collections/:collectionId/
|
|
10568
|
+
url: "/admin/collections/:collectionId/documents",
|
|
9252
10569
|
schema: {
|
|
9253
10570
|
params: collectionIdParamSchema,
|
|
9254
10571
|
response: {
|
|
9255
|
-
200:
|
|
10572
|
+
200: listDocumentsResponseSchema,
|
|
9256
10573
|
403: errorResponseSchema,
|
|
9257
10574
|
404: errorResponseSchema
|
|
9258
10575
|
}
|
|
9259
10576
|
},
|
|
9260
10577
|
/**
|
|
9261
|
-
* Lists
|
|
10578
|
+
* Lists collection documents in a collection for operator inspection.
|
|
9262
10579
|
*/
|
|
9263
10580
|
handler: async (request, reply) => {
|
|
9264
10581
|
try {
|
|
@@ -9271,9 +10588,9 @@ async function registerAdminRoutes(app, options) {
|
|
|
9271
10588
|
void reply.code(404).send({ error: "Collection not found" });
|
|
9272
10589
|
return;
|
|
9273
10590
|
}
|
|
9274
|
-
const
|
|
10591
|
+
const documents = await db.listDocuments(request.params.collectionId);
|
|
9275
10592
|
return reply.send({
|
|
9276
|
-
|
|
10593
|
+
documents: documents.map((document) => serializeDocument(document))
|
|
9277
10594
|
});
|
|
9278
10595
|
} catch (error) {
|
|
9279
10596
|
if (handleDbError(reply, error)) {
|
|
@@ -10316,7 +11633,8 @@ async function registerCollectionRoutes(app, db) {
|
|
|
10316
11633
|
request.body.preRequestScript,
|
|
10317
11634
|
request.body.postRequestScript,
|
|
10318
11635
|
request.body.auth,
|
|
10319
|
-
user.id
|
|
11636
|
+
user.id,
|
|
11637
|
+
request.body.marker
|
|
10320
11638
|
);
|
|
10321
11639
|
return reply.send(serializeCollection(collection));
|
|
10322
11640
|
} catch (error) {
|
|
@@ -10457,7 +11775,8 @@ async function registerEnvironmentRoutes(app, db) {
|
|
|
10457
11775
|
request.params.id,
|
|
10458
11776
|
request.body.name,
|
|
10459
11777
|
request.body.variables,
|
|
10460
|
-
user.id
|
|
11778
|
+
user.id,
|
|
11779
|
+
request.body.marker
|
|
10461
11780
|
);
|
|
10462
11781
|
return reply.send(serializeEnvironment(environment));
|
|
10463
11782
|
} catch (error) {
|
|
@@ -10716,6 +12035,50 @@ async function registerFolderRoutes(app, db) {
|
|
|
10716
12035
|
const folder = await db.createFolder(
|
|
10717
12036
|
request.params.collectionId,
|
|
10718
12037
|
request.body.name,
|
|
12038
|
+
user.id,
|
|
12039
|
+
request.body.parentFolderId
|
|
12040
|
+
);
|
|
12041
|
+
return reply.send(serializeFolder(folder));
|
|
12042
|
+
} catch (error) {
|
|
12043
|
+
if (handleDbError(reply, error)) {
|
|
12044
|
+
return;
|
|
12045
|
+
}
|
|
12046
|
+
throw error;
|
|
12047
|
+
}
|
|
12048
|
+
}
|
|
12049
|
+
});
|
|
12050
|
+
routes.route({
|
|
12051
|
+
method: "PUT",
|
|
12052
|
+
url: "/folders/:id/move",
|
|
12053
|
+
schema: {
|
|
12054
|
+
params: idParamSchema,
|
|
12055
|
+
body: moveFolderBodySchema,
|
|
12056
|
+
response: {
|
|
12057
|
+
200: folderRecordSchema,
|
|
12058
|
+
400: errorResponseSchema,
|
|
12059
|
+
404: errorResponseSchema
|
|
12060
|
+
}
|
|
12061
|
+
},
|
|
12062
|
+
/**
|
|
12063
|
+
* Moves a folder to another parent or collection root.
|
|
12064
|
+
*/
|
|
12065
|
+
handler: async (request, reply) => {
|
|
12066
|
+
try {
|
|
12067
|
+
const user = requireAuthenticatedUser(request);
|
|
12068
|
+
const existingFolder = await db.findFolderById(request.params.id);
|
|
12069
|
+
if (!existingFolder) {
|
|
12070
|
+
return reply.code(404).send({ error: "Folder not found" });
|
|
12071
|
+
}
|
|
12072
|
+
if (denyUnlessAllowed(
|
|
12073
|
+
reply,
|
|
12074
|
+
canUseDataApi(user) && canAccessCollection(user, existingFolder.collectionId)
|
|
12075
|
+
)) {
|
|
12076
|
+
return;
|
|
12077
|
+
}
|
|
12078
|
+
const folder = await db.moveFolder(
|
|
12079
|
+
request.params.id,
|
|
12080
|
+
request.body.parentFolderId,
|
|
12081
|
+
request.body.sortOrder,
|
|
10719
12082
|
user.id
|
|
10720
12083
|
);
|
|
10721
12084
|
return reply.send(serializeFolder(folder));
|
|
@@ -10755,7 +12118,12 @@ async function registerFolderRoutes(app, db) {
|
|
|
10755
12118
|
)) {
|
|
10756
12119
|
return;
|
|
10757
12120
|
}
|
|
10758
|
-
const folder = await db.renameFolder(
|
|
12121
|
+
const folder = await db.renameFolder(
|
|
12122
|
+
request.params.id,
|
|
12123
|
+
request.body.name,
|
|
12124
|
+
user.id,
|
|
12125
|
+
request.body.marker
|
|
12126
|
+
);
|
|
10759
12127
|
return reply.send(serializeFolder(folder));
|
|
10760
12128
|
} catch (error) {
|
|
10761
12129
|
if (handleDbError(reply, error)) {
|
|
@@ -10826,6 +12194,7 @@ async function registerFolderRoutes(app, db) {
|
|
|
10826
12194
|
}
|
|
10827
12195
|
await db.reorderFolders(
|
|
10828
12196
|
request.params.collectionId,
|
|
12197
|
+
request.body.parentFolderId,
|
|
10829
12198
|
request.body.orderedFolderIds,
|
|
10830
12199
|
user.id
|
|
10831
12200
|
);
|
|
@@ -10840,6 +12209,244 @@ async function registerFolderRoutes(app, db) {
|
|
|
10840
12209
|
});
|
|
10841
12210
|
}
|
|
10842
12211
|
|
|
12212
|
+
// src/server/routes/documents.ts
|
|
12213
|
+
async function registerDocumentRoutes(app, db) {
|
|
12214
|
+
const routes = app.withTypeProvider();
|
|
12215
|
+
routes.route({
|
|
12216
|
+
method: "GET",
|
|
12217
|
+
url: "/collections/:collectionId/documents",
|
|
12218
|
+
schema: {
|
|
12219
|
+
params: collectionIdParamSchema,
|
|
12220
|
+
response: {
|
|
12221
|
+
200: listDocumentsResponseSchema
|
|
12222
|
+
}
|
|
12223
|
+
},
|
|
12224
|
+
/**
|
|
12225
|
+
* Lists collection documents in a collection.
|
|
12226
|
+
*/
|
|
12227
|
+
handler: async (request, reply) => {
|
|
12228
|
+
try {
|
|
12229
|
+
const user = requireAuthenticatedUser(request);
|
|
12230
|
+
if (denyUnlessAllowed(
|
|
12231
|
+
reply,
|
|
12232
|
+
canUseDataApi(user) && canAccessCollection(user, request.params.collectionId)
|
|
12233
|
+
)) {
|
|
12234
|
+
return;
|
|
12235
|
+
}
|
|
12236
|
+
const documents = await db.listDocuments(request.params.collectionId);
|
|
12237
|
+
return reply.send({
|
|
12238
|
+
documents: documents.map((document) => serializeDocument(document))
|
|
12239
|
+
});
|
|
12240
|
+
} catch (error) {
|
|
12241
|
+
if (handleDbError(reply, error)) {
|
|
12242
|
+
return;
|
|
12243
|
+
}
|
|
12244
|
+
throw error;
|
|
12245
|
+
}
|
|
12246
|
+
}
|
|
12247
|
+
});
|
|
12248
|
+
routes.route({
|
|
12249
|
+
method: "POST",
|
|
12250
|
+
url: "/collections/:collectionId/documents",
|
|
12251
|
+
schema: {
|
|
12252
|
+
params: collectionIdParamSchema,
|
|
12253
|
+
body: saveDocumentBodySchema,
|
|
12254
|
+
response: {
|
|
12255
|
+
200: documentRecordSchema,
|
|
12256
|
+
400: errorResponseSchema,
|
|
12257
|
+
404: errorResponseSchema
|
|
12258
|
+
}
|
|
12259
|
+
},
|
|
12260
|
+
/**
|
|
12261
|
+
* Creates a new collection document in a collection.
|
|
12262
|
+
*/
|
|
12263
|
+
handler: async (request, reply) => {
|
|
12264
|
+
try {
|
|
12265
|
+
const user = requireAuthenticatedUser(request);
|
|
12266
|
+
if (denyUnlessAllowed(
|
|
12267
|
+
reply,
|
|
12268
|
+
canUseDataApi(user) && canAccessCollection(user, request.params.collectionId)
|
|
12269
|
+
)) {
|
|
12270
|
+
return;
|
|
12271
|
+
}
|
|
12272
|
+
const document = await db.saveDocument(
|
|
12273
|
+
{
|
|
12274
|
+
collectionId: request.params.collectionId,
|
|
12275
|
+
name: request.body.name,
|
|
12276
|
+
content: request.body.content,
|
|
12277
|
+
folderId: request.body.folderId ?? null,
|
|
12278
|
+
marker: request.body.marker
|
|
12279
|
+
},
|
|
12280
|
+
user.id
|
|
12281
|
+
);
|
|
12282
|
+
return reply.send(serializeDocument(document));
|
|
12283
|
+
} catch (error) {
|
|
12284
|
+
if (handleDbError(reply, error)) {
|
|
12285
|
+
return;
|
|
12286
|
+
}
|
|
12287
|
+
throw error;
|
|
12288
|
+
}
|
|
12289
|
+
}
|
|
12290
|
+
});
|
|
12291
|
+
routes.route({
|
|
12292
|
+
method: "PUT",
|
|
12293
|
+
url: "/documents/:id",
|
|
12294
|
+
schema: {
|
|
12295
|
+
params: idParamSchema,
|
|
12296
|
+
body: updateSaveDocumentBodySchema,
|
|
12297
|
+
response: {
|
|
12298
|
+
200: documentRecordSchema,
|
|
12299
|
+
400: errorResponseSchema,
|
|
12300
|
+
404: errorResponseSchema
|
|
12301
|
+
}
|
|
12302
|
+
},
|
|
12303
|
+
/**
|
|
12304
|
+
* Updates an existing collection document by id.
|
|
12305
|
+
*/
|
|
12306
|
+
handler: async (request, reply) => {
|
|
12307
|
+
try {
|
|
12308
|
+
const user = requireAuthenticatedUser(request);
|
|
12309
|
+
if (denyUnlessAllowed(
|
|
12310
|
+
reply,
|
|
12311
|
+
canUseDataApi(user) && canAccessCollection(user, request.body.collectionId)
|
|
12312
|
+
)) {
|
|
12313
|
+
return;
|
|
12314
|
+
}
|
|
12315
|
+
const document = await db.saveDocument(
|
|
12316
|
+
{
|
|
12317
|
+
id: request.params.id,
|
|
12318
|
+
collectionId: request.body.collectionId,
|
|
12319
|
+
name: request.body.name,
|
|
12320
|
+
content: request.body.content,
|
|
12321
|
+
folderId: request.body.folderId ?? null,
|
|
12322
|
+
marker: request.body.marker
|
|
12323
|
+
},
|
|
12324
|
+
user.id
|
|
12325
|
+
);
|
|
12326
|
+
return reply.send(serializeDocument(document));
|
|
12327
|
+
} catch (error) {
|
|
12328
|
+
if (handleDbError(reply, error)) {
|
|
12329
|
+
return;
|
|
12330
|
+
}
|
|
12331
|
+
throw error;
|
|
12332
|
+
}
|
|
12333
|
+
}
|
|
12334
|
+
});
|
|
12335
|
+
routes.route({
|
|
12336
|
+
method: "DELETE",
|
|
12337
|
+
url: "/documents/:id",
|
|
12338
|
+
schema: {
|
|
12339
|
+
params: idParamSchema,
|
|
12340
|
+
response: {
|
|
12341
|
+
204: emptyResponseSchema,
|
|
12342
|
+
404: errorResponseSchema
|
|
12343
|
+
}
|
|
12344
|
+
},
|
|
12345
|
+
/**
|
|
12346
|
+
* Deletes a collection document by id.
|
|
12347
|
+
*/
|
|
12348
|
+
handler: async (request, reply) => {
|
|
12349
|
+
try {
|
|
12350
|
+
const user = requireAuthenticatedUser(request);
|
|
12351
|
+
const existingDocument = await db.findDocumentById(request.params.id);
|
|
12352
|
+
if (!existingDocument) {
|
|
12353
|
+
return reply.code(404).send({ error: "Document not found" });
|
|
12354
|
+
}
|
|
12355
|
+
if (denyUnlessAllowed(reply, canDeleteDocument(user, existingDocument))) {
|
|
12356
|
+
return;
|
|
12357
|
+
}
|
|
12358
|
+
await db.deleteDocument(request.params.id, user.id);
|
|
12359
|
+
return reply.code(204).send(null);
|
|
12360
|
+
} catch (error) {
|
|
12361
|
+
if (handleDbError(reply, error)) {
|
|
12362
|
+
return;
|
|
12363
|
+
}
|
|
12364
|
+
throw error;
|
|
12365
|
+
}
|
|
12366
|
+
}
|
|
12367
|
+
});
|
|
12368
|
+
routes.route({
|
|
12369
|
+
method: "PUT",
|
|
12370
|
+
url: "/collections/:collectionId/documents/reorder",
|
|
12371
|
+
schema: {
|
|
12372
|
+
params: collectionIdParamSchema,
|
|
12373
|
+
body: reorderDocumentsBodySchema,
|
|
12374
|
+
response: {
|
|
12375
|
+
204: emptyResponseSchema,
|
|
12376
|
+
404: errorResponseSchema
|
|
12377
|
+
}
|
|
12378
|
+
},
|
|
12379
|
+
/**
|
|
12380
|
+
* Reorders collection documents within a folder or collection root.
|
|
12381
|
+
*/
|
|
12382
|
+
handler: async (request, reply) => {
|
|
12383
|
+
try {
|
|
12384
|
+
const user = requireAuthenticatedUser(request);
|
|
12385
|
+
if (denyUnlessAllowed(
|
|
12386
|
+
reply,
|
|
12387
|
+
canUseDataApi(user) && canAccessCollection(user, request.params.collectionId)
|
|
12388
|
+
)) {
|
|
12389
|
+
return;
|
|
12390
|
+
}
|
|
12391
|
+
await db.reorderDocuments(
|
|
12392
|
+
request.params.collectionId,
|
|
12393
|
+
request.body.folderId,
|
|
12394
|
+
request.body.orderedDocumentIds,
|
|
12395
|
+
user.id
|
|
12396
|
+
);
|
|
12397
|
+
return reply.code(204).send(null);
|
|
12398
|
+
} catch (error) {
|
|
12399
|
+
if (handleDbError(reply, error)) {
|
|
12400
|
+
return;
|
|
12401
|
+
}
|
|
12402
|
+
throw error;
|
|
12403
|
+
}
|
|
12404
|
+
}
|
|
12405
|
+
});
|
|
12406
|
+
routes.route({
|
|
12407
|
+
method: "PUT",
|
|
12408
|
+
url: "/documents/:id/move",
|
|
12409
|
+
schema: {
|
|
12410
|
+
params: idParamSchema,
|
|
12411
|
+
body: moveDocumentBodySchema,
|
|
12412
|
+
response: {
|
|
12413
|
+
204: emptyResponseSchema,
|
|
12414
|
+
404: errorResponseSchema
|
|
12415
|
+
}
|
|
12416
|
+
},
|
|
12417
|
+
/**
|
|
12418
|
+
* Moves a collection document to another folder or collection root index.
|
|
12419
|
+
*/
|
|
12420
|
+
handler: async (request, reply) => {
|
|
12421
|
+
try {
|
|
12422
|
+
const user = requireAuthenticatedUser(request);
|
|
12423
|
+
const existingDocument = await db.findDocumentById(request.params.id);
|
|
12424
|
+
if (!existingDocument) {
|
|
12425
|
+
return reply.code(404).send({ error: "Document not found" });
|
|
12426
|
+
}
|
|
12427
|
+
if (denyUnlessAllowed(
|
|
12428
|
+
reply,
|
|
12429
|
+
canUseDataApi(user) && canAccessCollection(user, existingDocument.collectionId)
|
|
12430
|
+
)) {
|
|
12431
|
+
return;
|
|
12432
|
+
}
|
|
12433
|
+
await db.moveDocument(
|
|
12434
|
+
request.params.id,
|
|
12435
|
+
request.body.folderId,
|
|
12436
|
+
request.body.index,
|
|
12437
|
+
user.id
|
|
12438
|
+
);
|
|
12439
|
+
return reply.code(204).send(null);
|
|
12440
|
+
} catch (error) {
|
|
12441
|
+
if (handleDbError(reply, error)) {
|
|
12442
|
+
return;
|
|
12443
|
+
}
|
|
12444
|
+
throw error;
|
|
12445
|
+
}
|
|
12446
|
+
}
|
|
12447
|
+
});
|
|
12448
|
+
}
|
|
12449
|
+
|
|
10843
12450
|
// src/server/routes/health.ts
|
|
10844
12451
|
import { z as z11 } from "zod/v4";
|
|
10845
12452
|
var healthResponseSchema = z11.object({
|
|
@@ -11286,7 +12893,8 @@ async function registerRequestRoutes(app, db) {
|
|
|
11286
12893
|
preRequestScript: request.body.preRequestScript,
|
|
11287
12894
|
postRequestScript: request.body.postRequestScript,
|
|
11288
12895
|
comment: request.body.comment,
|
|
11289
|
-
folderId: request.body.folderId ?? null
|
|
12896
|
+
folderId: request.body.folderId ?? null,
|
|
12897
|
+
marker: request.body.marker
|
|
11290
12898
|
},
|
|
11291
12899
|
user.id
|
|
11292
12900
|
);
|
|
@@ -11338,7 +12946,8 @@ async function registerRequestRoutes(app, db) {
|
|
|
11338
12946
|
preRequestScript: request.body.preRequestScript,
|
|
11339
12947
|
postRequestScript: request.body.postRequestScript,
|
|
11340
12948
|
comment: request.body.comment,
|
|
11341
|
-
folderId: request.body.folderId ?? null
|
|
12949
|
+
folderId: request.body.folderId ?? null,
|
|
12950
|
+
marker: request.body.marker
|
|
11342
12951
|
},
|
|
11343
12952
|
user.id
|
|
11344
12953
|
);
|
|
@@ -12426,6 +14035,7 @@ async function registerProtectedRoutes(app, options) {
|
|
|
12426
14035
|
await registerSnippetRoutes(app, options.db);
|
|
12427
14036
|
await registerFolderRoutes(app, options.db);
|
|
12428
14037
|
await registerRequestRoutes(app, options.db);
|
|
14038
|
+
await registerDocumentRoutes(app, options.db);
|
|
12429
14039
|
await registerRunResultRoutes(app, options.db);
|
|
12430
14040
|
await registerLlmRoutes(app, {
|
|
12431
14041
|
db: options.db,
|