@stndrds/schema 0.1.0-alpha.17 → 0.1.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2PBQNUTS.mjs +7400 -0
- package/dist/chunk-4D476S25.mjs +7713 -0
- package/dist/chunk-CWPOGEK2.mjs +7707 -0
- package/dist/chunk-EWNSOD22.mjs +7707 -0
- package/dist/chunk-KZSM5546.mjs +7706 -0
- package/dist/chunk-L7AQ7SZC.mjs +7432 -0
- package/dist/chunk-LZW2MI43.mjs +7404 -0
- package/dist/chunk-V5QXU2OK.mjs +82 -0
- package/dist/chunk-XMGMXAHW.mjs +7706 -0
- package/dist/default-roles-AKWIWTLE.mjs +20 -0
- package/dist/index.d.mts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +494 -47
- package/dist/index.mjs +4 -2
- package/dist/runtime-DWhSpnn4.d.mts +5256 -0
- package/dist/runtime-DWhSpnn4.d.ts +5256 -0
- package/dist/runtime-Dw2exb5q.d.mts +5601 -0
- package/dist/runtime-Dw2exb5q.d.ts +5601 -0
- package/dist/runtime-DySuNV6Y.d.mts +5245 -0
- package/dist/runtime-DySuNV6Y.d.ts +5245 -0
- package/dist/runtime-DzdsFCyL.d.mts +5601 -0
- package/dist/runtime-DzdsFCyL.d.ts +5601 -0
- package/dist/runtime-uXhTgSIx.d.mts +5260 -0
- package/dist/runtime-uXhTgSIx.d.ts +5260 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +480 -47
- package/dist/runtime.mjs +3 -1
- package/package.json +3 -3
package/dist/runtime.js
CHANGED
|
@@ -59,21 +59,25 @@ var init_default_roles = __esm({
|
|
|
59
59
|
/** Tenant settings and configuration */
|
|
60
60
|
SETTINGS: "settings",
|
|
61
61
|
/** Audit logs and activity trail */
|
|
62
|
-
AUDIT: "audit"
|
|
62
|
+
AUDIT: "audit",
|
|
63
|
+
/** File uploads and storage management */
|
|
64
|
+
FILES: "files"
|
|
63
65
|
};
|
|
64
66
|
ALL_SYSTEM_RESOURCES = [
|
|
65
67
|
SYSTEM_RESOURCES.USERS,
|
|
66
68
|
SYSTEM_RESOURCES.SCHEMA,
|
|
67
69
|
SYSTEM_RESOURCES.ROLES,
|
|
68
70
|
SYSTEM_RESOURCES.SETTINGS,
|
|
69
|
-
SYSTEM_RESOURCES.AUDIT
|
|
71
|
+
SYSTEM_RESOURCES.AUDIT,
|
|
72
|
+
SYSTEM_RESOURCES.FILES
|
|
70
73
|
];
|
|
71
74
|
SYSTEM_RESOURCE_LABELS = {
|
|
72
75
|
users: "Users",
|
|
73
76
|
schema: "Schema",
|
|
74
77
|
roles: "Roles & Permissions",
|
|
75
78
|
settings: "Settings",
|
|
76
|
-
audit: "Audit Logs"
|
|
79
|
+
audit: "Audit Logs",
|
|
80
|
+
files: "Files"
|
|
77
81
|
};
|
|
78
82
|
DEFAULT_ROLES = {
|
|
79
83
|
/** Full platform access - can manage everything */
|
|
@@ -131,6 +135,7 @@ __export(runtime_exports, {
|
|
|
131
135
|
RecordService: () => RecordService,
|
|
132
136
|
RelationService: () => RelationService,
|
|
133
137
|
UserProfileService: () => UserProfileService,
|
|
138
|
+
UserService: () => UserService,
|
|
134
139
|
ViewService: () => ViewService,
|
|
135
140
|
buildAuditChanges: () => buildAuditChanges,
|
|
136
141
|
createMockAdapter: () => createMockAdapter,
|
|
@@ -178,6 +183,13 @@ function formatCheckbox(value) {
|
|
|
178
183
|
function formatNumber(value, attribute) {
|
|
179
184
|
if (typeof value !== "number") return String(value);
|
|
180
185
|
const decimals = attribute.decimals;
|
|
186
|
+
if (attribute.unit === "percentage") {
|
|
187
|
+
return value.toLocaleString(void 0, {
|
|
188
|
+
style: "percent",
|
|
189
|
+
minimumFractionDigits: decimals,
|
|
190
|
+
maximumFractionDigits: decimals
|
|
191
|
+
});
|
|
192
|
+
}
|
|
181
193
|
return value.toLocaleString(void 0, {
|
|
182
194
|
minimumFractionDigits: decimals,
|
|
183
195
|
maximumFractionDigits: decimals
|
|
@@ -1404,6 +1416,24 @@ var AuditService = class {
|
|
|
1404
1416
|
};
|
|
1405
1417
|
await this.log(entry);
|
|
1406
1418
|
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Log a file action (upload, update, delete)
|
|
1421
|
+
*/
|
|
1422
|
+
async logFileAction(params) {
|
|
1423
|
+
const entry = {
|
|
1424
|
+
tenantId: this.tenantId,
|
|
1425
|
+
actorId: params.actorId,
|
|
1426
|
+
actorEmail: params.actorEmail,
|
|
1427
|
+
actorType: "user",
|
|
1428
|
+
action: params.action,
|
|
1429
|
+
resourceType: "file",
|
|
1430
|
+
resourceId: params.fileId,
|
|
1431
|
+
resourceLabel: params.fileName,
|
|
1432
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
1433
|
+
metadata: params.metadata
|
|
1434
|
+
};
|
|
1435
|
+
await this.log(entry);
|
|
1436
|
+
}
|
|
1407
1437
|
/**
|
|
1408
1438
|
* Log a system action (no actor)
|
|
1409
1439
|
*/
|
|
@@ -1553,21 +1583,105 @@ var AuditService = class {
|
|
|
1553
1583
|
|
|
1554
1584
|
// src/runtime/services/file.service.ts
|
|
1555
1585
|
var FileService = class {
|
|
1556
|
-
constructor(adapter, tenantId) {
|
|
1586
|
+
constructor(adapter, tenantId, options) {
|
|
1557
1587
|
this.adapter = adapter;
|
|
1558
1588
|
this.tenantId = tenantId;
|
|
1589
|
+
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
1590
|
+
this.userId = options?.userId;
|
|
1591
|
+
this.userEmail = options?.userEmail;
|
|
1559
1592
|
}
|
|
1593
|
+
// ============================================================================
|
|
1594
|
+
// UPLOAD (requires StorageAdapter)
|
|
1595
|
+
// ============================================================================
|
|
1560
1596
|
/**
|
|
1561
|
-
*
|
|
1597
|
+
* Upload a file to storage and create metadata record.
|
|
1562
1598
|
*
|
|
1563
|
-
*
|
|
1599
|
+
* This method orchestrates:
|
|
1600
|
+
* 1. Upload to storage (via StorageAdapter)
|
|
1601
|
+
* 2. Create file metadata in database
|
|
1602
|
+
* 3. Audit log the operation
|
|
1603
|
+
*
|
|
1604
|
+
* Requires `adapter.storage` to be configured.
|
|
1605
|
+
*
|
|
1606
|
+
* @param input - File content and metadata
|
|
1564
1607
|
* @returns Created file record
|
|
1608
|
+
* @throws Error if StorageAdapter is not configured
|
|
1565
1609
|
*
|
|
1566
1610
|
* @example
|
|
1567
1611
|
* ```typescript
|
|
1568
|
-
* const
|
|
1612
|
+
* const file = await service.uploadFile({
|
|
1613
|
+
* content: fileBuffer,
|
|
1614
|
+
* fileName: "document.pdf",
|
|
1615
|
+
* mimeType: "application/pdf",
|
|
1616
|
+
* size: 12345,
|
|
1617
|
+
* uploadedBy: "user-123",
|
|
1618
|
+
* visibility: "private",
|
|
1619
|
+
* folderPath: "/documents",
|
|
1620
|
+
* tags: ["contract", "2025"],
|
|
1621
|
+
* });
|
|
1622
|
+
* ```
|
|
1623
|
+
*/
|
|
1624
|
+
async uploadFile(input) {
|
|
1625
|
+
if (!this.adapter.storage) {
|
|
1626
|
+
throw new Error(
|
|
1627
|
+
"StorageAdapter is not configured. Provide adapter.storage to use uploadFile()."
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
const uploadResult = await this.adapter.storage.upload({
|
|
1631
|
+
content: input.content,
|
|
1632
|
+
fileName: input.fileName,
|
|
1633
|
+
mimeType: input.mimeType,
|
|
1634
|
+
size: input.size,
|
|
1635
|
+
tenantId: this.tenantId,
|
|
1636
|
+
folderPath: input.folderPath
|
|
1637
|
+
});
|
|
1638
|
+
const file = await this.adapter.files.create({
|
|
1639
|
+
tenantId: this.tenantId,
|
|
1640
|
+
name: input.fileName,
|
|
1641
|
+
originalName: input.fileName,
|
|
1642
|
+
mimeType: input.mimeType,
|
|
1643
|
+
size: input.size,
|
|
1644
|
+
storageProvider: uploadResult.storageProvider,
|
|
1645
|
+
storagePath: uploadResult.storagePath,
|
|
1646
|
+
storageBucket: uploadResult.storageBucket,
|
|
1647
|
+
url: uploadResult.url,
|
|
1648
|
+
uploadedBy: input.uploadedBy,
|
|
1649
|
+
folderPath: input.folderPath,
|
|
1650
|
+
tags: input.tags,
|
|
1651
|
+
visibility: input.visibility ?? "private",
|
|
1652
|
+
allowedUsers: input.allowedUsers
|
|
1653
|
+
});
|
|
1654
|
+
if (this.auditService && this.userId) {
|
|
1655
|
+
await this.auditService.logFileAction({
|
|
1656
|
+
action: "file.uploaded",
|
|
1657
|
+
actorId: this.userId,
|
|
1658
|
+
actorEmail: this.userEmail,
|
|
1659
|
+
fileId: file.id,
|
|
1660
|
+
fileName: file.name,
|
|
1661
|
+
metadata: {
|
|
1662
|
+
mimeType: file.mimeType,
|
|
1663
|
+
size: file.size,
|
|
1664
|
+
visibility: file.visibility
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
return file;
|
|
1669
|
+
}
|
|
1670
|
+
// ============================================================================
|
|
1671
|
+
// CREATE (metadata only, for external storage)
|
|
1672
|
+
// ============================================================================
|
|
1673
|
+
/**
|
|
1674
|
+
* Create a new file record (after upload to storage).
|
|
1675
|
+
*
|
|
1676
|
+
* Use this method when handling storage externally (e.g., with Multer + S3).
|
|
1677
|
+
* For integrated upload, use `uploadFile()` instead.
|
|
1569
1678
|
*
|
|
1570
|
-
*
|
|
1679
|
+
* @param data - File metadata
|
|
1680
|
+
* @returns Created file record
|
|
1681
|
+
*
|
|
1682
|
+
* @example
|
|
1683
|
+
* ```typescript
|
|
1684
|
+
* // After uploading to S3 with Multer
|
|
1571
1685
|
* const file = await service.createFile({
|
|
1572
1686
|
* tenantId: "tenant-123",
|
|
1573
1687
|
* name: "contract-2025.pdf",
|
|
@@ -1589,8 +1703,26 @@ var FileService = class {
|
|
|
1589
1703
|
`Tenant mismatch: service initialized with "${this.tenantId}" but data has "${data.tenantId}"`
|
|
1590
1704
|
);
|
|
1591
1705
|
}
|
|
1592
|
-
|
|
1706
|
+
const file = await this.adapter.files.create(data);
|
|
1707
|
+
if (this.auditService && this.userId) {
|
|
1708
|
+
await this.auditService.logFileAction({
|
|
1709
|
+
action: "file.uploaded",
|
|
1710
|
+
actorId: this.userId,
|
|
1711
|
+
actorEmail: this.userEmail,
|
|
1712
|
+
fileId: file.id,
|
|
1713
|
+
fileName: file.name,
|
|
1714
|
+
metadata: {
|
|
1715
|
+
mimeType: file.mimeType,
|
|
1716
|
+
size: file.size,
|
|
1717
|
+
visibility: file.visibility
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
return file;
|
|
1593
1722
|
}
|
|
1723
|
+
// ============================================================================
|
|
1724
|
+
// READ
|
|
1725
|
+
// ============================================================================
|
|
1594
1726
|
/**
|
|
1595
1727
|
* Get file by ID
|
|
1596
1728
|
*/
|
|
@@ -1611,6 +1743,9 @@ var FileService = class {
|
|
|
1611
1743
|
}
|
|
1612
1744
|
return file;
|
|
1613
1745
|
}
|
|
1746
|
+
// ============================================================================
|
|
1747
|
+
// UPDATE
|
|
1748
|
+
// ============================================================================
|
|
1614
1749
|
/**
|
|
1615
1750
|
* Update file metadata
|
|
1616
1751
|
*
|
|
@@ -1619,18 +1754,44 @@ var FileService = class {
|
|
|
1619
1754
|
* @returns Updated file
|
|
1620
1755
|
*/
|
|
1621
1756
|
async updateFile(fileId, data) {
|
|
1622
|
-
await this.getFileOrThrow(fileId);
|
|
1623
|
-
|
|
1757
|
+
const existingFile = await this.getFileOrThrow(fileId);
|
|
1758
|
+
const updatedFile = await this.adapter.files.update(fileId, data);
|
|
1759
|
+
if (this.auditService && this.userId) {
|
|
1760
|
+
const changes = [];
|
|
1761
|
+
for (const key of Object.keys(data)) {
|
|
1762
|
+
if (data[key] !== void 0 && data[key] !== existingFile[key]) {
|
|
1763
|
+
changes.push({
|
|
1764
|
+
field: key,
|
|
1765
|
+
oldValue: existingFile[key],
|
|
1766
|
+
newValue: data[key]
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (changes.length > 0) {
|
|
1771
|
+
await this.auditService.logFileAction({
|
|
1772
|
+
action: "file.updated",
|
|
1773
|
+
actorId: this.userId,
|
|
1774
|
+
actorEmail: this.userEmail,
|
|
1775
|
+
fileId,
|
|
1776
|
+
fileName: updatedFile.name,
|
|
1777
|
+
changes
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return updatedFile;
|
|
1624
1782
|
}
|
|
1783
|
+
// ============================================================================
|
|
1784
|
+
// DELETE
|
|
1785
|
+
// ============================================================================
|
|
1625
1786
|
/**
|
|
1626
|
-
* Delete file (soft delete)
|
|
1787
|
+
* Delete file (soft delete by default)
|
|
1627
1788
|
*
|
|
1628
1789
|
* @param fileId - File UUID
|
|
1629
1790
|
* @param options - Delete options
|
|
1630
1791
|
*/
|
|
1631
1792
|
async deleteFile(fileId, options) {
|
|
1793
|
+
const file = await this.getFileOrThrow(fileId);
|
|
1632
1794
|
if (options?.checkOwnership && options.userId) {
|
|
1633
|
-
const file = await this.getFileOrThrow(fileId);
|
|
1634
1795
|
if (file.uploadedBy !== options.userId) {
|
|
1635
1796
|
throw new Error("You can only delete files you uploaded");
|
|
1636
1797
|
}
|
|
@@ -1640,7 +1801,84 @@ var FileService = class {
|
|
|
1640
1801
|
} else {
|
|
1641
1802
|
await this.adapter.files.delete(fileId);
|
|
1642
1803
|
}
|
|
1804
|
+
if (this.auditService && this.userId) {
|
|
1805
|
+
await this.auditService.logFileAction({
|
|
1806
|
+
action: "file.deleted",
|
|
1807
|
+
actorId: this.userId,
|
|
1808
|
+
actorEmail: this.userEmail,
|
|
1809
|
+
fileId,
|
|
1810
|
+
fileName: file.name
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Delete file from both storage and database.
|
|
1816
|
+
*
|
|
1817
|
+
* Requires `adapter.storage` to be configured.
|
|
1818
|
+
*
|
|
1819
|
+
* @param fileId - File UUID
|
|
1820
|
+
* @param options - Delete options
|
|
1821
|
+
* @throws Error if StorageAdapter is not configured
|
|
1822
|
+
*/
|
|
1823
|
+
async deleteFileWithStorage(fileId, options) {
|
|
1824
|
+
if (!this.adapter.storage) {
|
|
1825
|
+
throw new Error(
|
|
1826
|
+
"StorageAdapter is not configured. Provide adapter.storage to use deleteFileWithStorage()."
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
const file = await this.getFileOrThrow(fileId);
|
|
1830
|
+
await this.adapter.storage.delete(file.storagePath);
|
|
1831
|
+
if (options?.hard) {
|
|
1832
|
+
await this.adapter.files.hardDelete(fileId);
|
|
1833
|
+
} else {
|
|
1834
|
+
await this.adapter.files.delete(fileId);
|
|
1835
|
+
}
|
|
1836
|
+
if (this.auditService && this.userId) {
|
|
1837
|
+
await this.auditService.logFileAction({
|
|
1838
|
+
action: "file.deleted",
|
|
1839
|
+
actorId: this.userId,
|
|
1840
|
+
actorEmail: this.userEmail,
|
|
1841
|
+
fileId,
|
|
1842
|
+
fileName: file.name,
|
|
1843
|
+
metadata: { deletedFromStorage: true }
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1643
1846
|
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Delete multiple files
|
|
1849
|
+
*
|
|
1850
|
+
* @param fileIds - Array of file UUIDs
|
|
1851
|
+
* @param options - Delete options
|
|
1852
|
+
*/
|
|
1853
|
+
async bulkDelete(fileIds, options) {
|
|
1854
|
+
for (const fileId of fileIds) {
|
|
1855
|
+
const file = await this.getFile(fileId);
|
|
1856
|
+
if (!file) {
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
if (options?.deleteFromStorage && this.adapter.storage) {
|
|
1860
|
+
await this.adapter.storage.delete(file.storagePath);
|
|
1861
|
+
}
|
|
1862
|
+
if (options?.hard) {
|
|
1863
|
+
await this.adapter.files.hardDelete(fileId);
|
|
1864
|
+
} else {
|
|
1865
|
+
await this.adapter.files.delete(fileId);
|
|
1866
|
+
}
|
|
1867
|
+
if (this.auditService && this.userId) {
|
|
1868
|
+
await this.auditService.logFileAction({
|
|
1869
|
+
action: "file.deleted",
|
|
1870
|
+
actorId: this.userId,
|
|
1871
|
+
actorEmail: this.userEmail,
|
|
1872
|
+
fileId,
|
|
1873
|
+
fileName: file.name,
|
|
1874
|
+
metadata: { deletedFromStorage: options?.deleteFromStorage ?? false }
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
// ============================================================================
|
|
1880
|
+
// LIST
|
|
1881
|
+
// ============================================================================
|
|
1644
1882
|
/**
|
|
1645
1883
|
* List files for the tenant
|
|
1646
1884
|
*/
|
|
@@ -1659,6 +1897,74 @@ var FileService = class {
|
|
|
1659
1897
|
async listFilesByUploader(uploadedBy) {
|
|
1660
1898
|
return await this.adapter.files.findByUploader(uploadedBy);
|
|
1661
1899
|
}
|
|
1900
|
+
// ============================================================================
|
|
1901
|
+
// SIGNED URL
|
|
1902
|
+
// ============================================================================
|
|
1903
|
+
/**
|
|
1904
|
+
* Get a signed URL for private file access.
|
|
1905
|
+
*
|
|
1906
|
+
* Checks access permissions before generating URL.
|
|
1907
|
+
* Requires `adapter.storage` to be configured.
|
|
1908
|
+
*
|
|
1909
|
+
* @param fileId - File UUID
|
|
1910
|
+
* @param userId - User requesting access
|
|
1911
|
+
* @param options - Signed URL options
|
|
1912
|
+
* @returns Signed URL
|
|
1913
|
+
* @throws Error if user doesn't have access or StorageAdapter is not configured
|
|
1914
|
+
*
|
|
1915
|
+
* @example
|
|
1916
|
+
* ```typescript
|
|
1917
|
+
* const url = await service.getSignedUrl("file-123", "user-456", {
|
|
1918
|
+
* expiresIn: 3600, // 1 hour
|
|
1919
|
+
* });
|
|
1920
|
+
* ```
|
|
1921
|
+
*/
|
|
1922
|
+
async getSignedUrl(fileId, userId, options) {
|
|
1923
|
+
const file = await this.getFileOrThrow(fileId);
|
|
1924
|
+
const hasAccess = await this.checkAccess(fileId, userId);
|
|
1925
|
+
if (!hasAccess) {
|
|
1926
|
+
throw new Error("Access denied to this file");
|
|
1927
|
+
}
|
|
1928
|
+
if (file.visibility === "public") {
|
|
1929
|
+
return file.url;
|
|
1930
|
+
}
|
|
1931
|
+
if (!this.adapter.storage) {
|
|
1932
|
+
return file.url;
|
|
1933
|
+
}
|
|
1934
|
+
return await this.adapter.storage.getSignedUrl(file.storagePath, options);
|
|
1935
|
+
}
|
|
1936
|
+
// ============================================================================
|
|
1937
|
+
// ACCESS CONTROL
|
|
1938
|
+
// ============================================================================
|
|
1939
|
+
/**
|
|
1940
|
+
* Check if user has access to a file
|
|
1941
|
+
*
|
|
1942
|
+
* @param fileId - File UUID
|
|
1943
|
+
* @param userId - User ID to check
|
|
1944
|
+
* @returns true if user can access the file
|
|
1945
|
+
*/
|
|
1946
|
+
async checkAccess(fileId, userId) {
|
|
1947
|
+
const file = await this.getFile(fileId);
|
|
1948
|
+
if (!file) {
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
if (file.visibility === "public") {
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1954
|
+
if (file.uploadedBy === userId) {
|
|
1955
|
+
return true;
|
|
1956
|
+
}
|
|
1957
|
+
if (file.visibility === "restricted") {
|
|
1958
|
+
return file.allowedUsers?.includes(userId) ?? false;
|
|
1959
|
+
}
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* @deprecated Use checkAccess() instead
|
|
1964
|
+
*/
|
|
1965
|
+
canAccess(fileId, userId) {
|
|
1966
|
+
return this.checkAccess(fileId, userId);
|
|
1967
|
+
}
|
|
1662
1968
|
/**
|
|
1663
1969
|
* Change file visibility
|
|
1664
1970
|
*
|
|
@@ -1702,29 +2008,9 @@ var FileService = class {
|
|
|
1702
2008
|
allowedUsers: newAllowed
|
|
1703
2009
|
});
|
|
1704
2010
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
* @param fileId - File UUID
|
|
1709
|
-
* @param userId - User ID to check
|
|
1710
|
-
* @returns true if user can access the file
|
|
1711
|
-
*/
|
|
1712
|
-
async canAccess(fileId, userId) {
|
|
1713
|
-
const file = await this.getFile(fileId);
|
|
1714
|
-
if (!file) {
|
|
1715
|
-
return false;
|
|
1716
|
-
}
|
|
1717
|
-
if (file.visibility === "public") {
|
|
1718
|
-
return true;
|
|
1719
|
-
}
|
|
1720
|
-
if (file.uploadedBy === userId) {
|
|
1721
|
-
return true;
|
|
1722
|
-
}
|
|
1723
|
-
if (file.visibility === "restricted") {
|
|
1724
|
-
return file.allowedUsers?.includes(userId) ?? false;
|
|
1725
|
-
}
|
|
1726
|
-
return false;
|
|
1727
|
-
}
|
|
2011
|
+
// ============================================================================
|
|
2012
|
+
// ORGANIZATION
|
|
2013
|
+
// ============================================================================
|
|
1728
2014
|
/**
|
|
1729
2015
|
* Move file to different folder
|
|
1730
2016
|
*/
|
|
@@ -1769,6 +2055,7 @@ var SchemaErrorCode = {
|
|
|
1769
2055
|
// Protected Resources
|
|
1770
2056
|
PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
|
|
1771
2057
|
PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
|
|
2058
|
+
PROTECTED_VIEW: "SCHEMA_PROTECTED_VIEW",
|
|
1772
2059
|
PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
|
|
1773
2060
|
// Permissions
|
|
1774
2061
|
FORBIDDEN: "SCHEMA_FORBIDDEN",
|
|
@@ -1833,7 +2120,7 @@ var ValidationError = class _ValidationError extends SchemaError {
|
|
|
1833
2120
|
};
|
|
1834
2121
|
var ProtectedResourceError = class extends SchemaError {
|
|
1835
2122
|
constructor(resourceType, resourceName, operation) {
|
|
1836
|
-
const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : SchemaErrorCode.PROTECTED_ATTRIBUTE;
|
|
2123
|
+
const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : resourceType === "view" ? SchemaErrorCode.PROTECTED_VIEW : SchemaErrorCode.PROTECTED_ATTRIBUTE;
|
|
1837
2124
|
super(`Cannot ${operation} system ${resourceType} "${resourceName}"`, code, {
|
|
1838
2125
|
resourceType,
|
|
1839
2126
|
resourceName,
|
|
@@ -2598,10 +2885,12 @@ var fileConfigSchema = baseConfigSchema.extend({
|
|
|
2598
2885
|
maxFiles: import_zod4.z.number().int().min(1).optional(),
|
|
2599
2886
|
maxSize: import_zod4.z.number().int().min(1).optional(),
|
|
2600
2887
|
allowedTypes: import_zod4.z.array(import_zod4.z.string()).optional(),
|
|
2601
|
-
verification: fileVerificationConfigSchema.optional()
|
|
2888
|
+
verification: fileVerificationConfigSchema.optional(),
|
|
2889
|
+
multiple: import_zod4.z.boolean().optional()
|
|
2602
2890
|
});
|
|
2603
2891
|
var userConfigSchema = baseConfigSchema.extend({
|
|
2604
|
-
allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional()
|
|
2892
|
+
allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional(),
|
|
2893
|
+
multiple: import_zod4.z.boolean().optional()
|
|
2605
2894
|
});
|
|
2606
2895
|
var relationConfigSchema = baseConfigSchema.extend({
|
|
2607
2896
|
targets: import_zod4.z.array(relationTargetSchema).min(1),
|
|
@@ -2723,20 +3012,39 @@ function createLocationValidator(_attr) {
|
|
|
2723
3012
|
function createTimestampValidator(_attr) {
|
|
2724
3013
|
return import_zod4.z.number().int().positive();
|
|
2725
3014
|
}
|
|
2726
|
-
function createFileValidator(
|
|
2727
|
-
|
|
3015
|
+
function createFileValidator(attr) {
|
|
3016
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
3017
|
+
message: `${attr.label} must be a valid file ID`
|
|
3018
|
+
});
|
|
3019
|
+
if (attr.multiple) {
|
|
3020
|
+
let arraySchema = import_zod4.z.array(uuidSchema);
|
|
3021
|
+
if (attr.maxFiles) {
|
|
3022
|
+
arraySchema = arraySchema.max(
|
|
3023
|
+
attr.maxFiles,
|
|
3024
|
+
`${attr.label} cannot have more than ${attr.maxFiles} file${attr.maxFiles > 1 ? "s" : ""}`
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
return arraySchema;
|
|
3028
|
+
}
|
|
3029
|
+
return uuidSchema;
|
|
2728
3030
|
}
|
|
2729
|
-
function createUserValidator(
|
|
2730
|
-
|
|
3031
|
+
function createUserValidator(attr) {
|
|
3032
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
3033
|
+
message: `${attr.label} must be a valid user ID`
|
|
3034
|
+
});
|
|
3035
|
+
if (attr.multiple) {
|
|
3036
|
+
return import_zod4.z.array(uuidSchema);
|
|
3037
|
+
}
|
|
3038
|
+
return uuidSchema;
|
|
2731
3039
|
}
|
|
2732
3040
|
function createSingleRelationValidator(attr) {
|
|
2733
|
-
const uuidSchema = import_zod4.z.
|
|
3041
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2734
3042
|
message: `${attr.label} must be a valid record ID`
|
|
2735
3043
|
});
|
|
2736
3044
|
return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
|
|
2737
3045
|
}
|
|
2738
3046
|
function createMultiRelationValidator(attr) {
|
|
2739
|
-
const uuidSchema = import_zod4.z.
|
|
3047
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2740
3048
|
message: `Each ${attr.label} item must be a valid record ID`
|
|
2741
3049
|
});
|
|
2742
3050
|
let arraySchema = import_zod4.z.array(uuidSchema);
|
|
@@ -4426,6 +4734,123 @@ var RelationService = class {
|
|
|
4426
4734
|
}
|
|
4427
4735
|
};
|
|
4428
4736
|
|
|
4737
|
+
// src/runtime/services/user.service.ts
|
|
4738
|
+
var UserService = class {
|
|
4739
|
+
constructor(adapter, tenantId) {
|
|
4740
|
+
this.adapter = adapter;
|
|
4741
|
+
this.tenantId = tenantId;
|
|
4742
|
+
}
|
|
4743
|
+
/**
|
|
4744
|
+
* Validate all user attributes in the data
|
|
4745
|
+
*
|
|
4746
|
+
* @param schema - Object schema containing attribute definitions
|
|
4747
|
+
* @param data - Record data to validate
|
|
4748
|
+
* @returns Validation result with errors if any
|
|
4749
|
+
*
|
|
4750
|
+
* @example
|
|
4751
|
+
* ```typescript
|
|
4752
|
+
* const result = await userService.validateUsers(schema, {
|
|
4753
|
+
* assignee: "user-123",
|
|
4754
|
+
* watchers: ["user-456", "user-789"]
|
|
4755
|
+
* });
|
|
4756
|
+
*
|
|
4757
|
+
* if (!result.valid) {
|
|
4758
|
+
* console.log(result.errors);
|
|
4759
|
+
* // [{ attribute: "assignee", message: "User not found", invalidIds: ["user-123"] }]
|
|
4760
|
+
* }
|
|
4761
|
+
* ```
|
|
4762
|
+
*/
|
|
4763
|
+
async validateUsers(schema, data) {
|
|
4764
|
+
const errors = [];
|
|
4765
|
+
const userAttrs = schema.attributes.filter(
|
|
4766
|
+
(attr) => attr.type === "user"
|
|
4767
|
+
);
|
|
4768
|
+
for (const attr of userAttrs) {
|
|
4769
|
+
const value = data[attr.name];
|
|
4770
|
+
if (value === void 0 || value === null) {
|
|
4771
|
+
continue;
|
|
4772
|
+
}
|
|
4773
|
+
const attrErrors = await this.validateUserAttribute(attr, value);
|
|
4774
|
+
errors.push(...attrErrors);
|
|
4775
|
+
}
|
|
4776
|
+
return {
|
|
4777
|
+
valid: errors.length === 0,
|
|
4778
|
+
errors
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
/**
|
|
4782
|
+
* Validate a single user attribute value
|
|
4783
|
+
*/
|
|
4784
|
+
async validateUserAttribute(attr, value) {
|
|
4785
|
+
const errors = [];
|
|
4786
|
+
const ids = this.extractIds(attr, value);
|
|
4787
|
+
if (ids.length === 0) {
|
|
4788
|
+
return errors;
|
|
4789
|
+
}
|
|
4790
|
+
const invalidIds = [];
|
|
4791
|
+
const roleErrors = [];
|
|
4792
|
+
for (const id of ids) {
|
|
4793
|
+
const user = await this.adapter.userProfiles.findById(id);
|
|
4794
|
+
if (!user) {
|
|
4795
|
+
invalidIds.push(id);
|
|
4796
|
+
continue;
|
|
4797
|
+
}
|
|
4798
|
+
if (user.tenantId !== this.tenantId) {
|
|
4799
|
+
invalidIds.push(id);
|
|
4800
|
+
continue;
|
|
4801
|
+
}
|
|
4802
|
+
if (attr.allowedRoles && attr.allowedRoles.length > 0) {
|
|
4803
|
+
if (!attr.allowedRoles.includes(user.role)) {
|
|
4804
|
+
roleErrors.push(id);
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
if (invalidIds.length > 0) {
|
|
4809
|
+
errors.push({
|
|
4810
|
+
attribute: attr.name,
|
|
4811
|
+
message: `Invalid or non-existent users for ${attr.label}`,
|
|
4812
|
+
invalidIds
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
if (roleErrors.length > 0) {
|
|
4816
|
+
errors.push({
|
|
4817
|
+
attribute: attr.name,
|
|
4818
|
+
message: `Users do not have required role for ${attr.label}. Allowed roles: ${attr.allowedRoles?.join(", ")}`,
|
|
4819
|
+
invalidIds: roleErrors
|
|
4820
|
+
});
|
|
4821
|
+
}
|
|
4822
|
+
return errors;
|
|
4823
|
+
}
|
|
4824
|
+
/**
|
|
4825
|
+
* Extract IDs from user value based on multiple flag
|
|
4826
|
+
*/
|
|
4827
|
+
extractIds(attr, value) {
|
|
4828
|
+
if (attr.multiple) {
|
|
4829
|
+
if (!Array.isArray(value)) {
|
|
4830
|
+
return [];
|
|
4831
|
+
}
|
|
4832
|
+
return value.filter((v) => typeof v === "string" && v !== "");
|
|
4833
|
+
}
|
|
4834
|
+
if (typeof value === "string" && value !== "") {
|
|
4835
|
+
return [value];
|
|
4836
|
+
}
|
|
4837
|
+
return [];
|
|
4838
|
+
}
|
|
4839
|
+
/**
|
|
4840
|
+
* Validate users and throw if invalid
|
|
4841
|
+
*/
|
|
4842
|
+
async validateUsersOrThrow(schema, data) {
|
|
4843
|
+
const result = await this.validateUsers(schema, data);
|
|
4844
|
+
if (!result.valid) {
|
|
4845
|
+
const errors = result.errors.map((e) => ({
|
|
4846
|
+
path: [e.attribute],
|
|
4847
|
+
message: e.message
|
|
4848
|
+
}));
|
|
4849
|
+
throw new ValidationError("User validation failed", errors);
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
};
|
|
4853
|
+
|
|
4429
4854
|
// src/runtime/services/record.service.ts
|
|
4430
4855
|
var RecordService = class {
|
|
4431
4856
|
constructor(adapter, tenantId, options) {
|
|
@@ -4433,6 +4858,7 @@ var RecordService = class {
|
|
|
4433
4858
|
this.tenantId = tenantId;
|
|
4434
4859
|
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
4435
4860
|
this.relationService = new RelationService(adapter, registry);
|
|
4861
|
+
this.userService = new UserService(adapter, tenantId);
|
|
4436
4862
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
4437
4863
|
this.permissionService = options?.permissionService;
|
|
4438
4864
|
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
@@ -4573,6 +4999,9 @@ var RecordService = class {
|
|
|
4573
4999
|
if (!options?.skipRelationValidation) {
|
|
4574
5000
|
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
4575
5001
|
}
|
|
5002
|
+
if (!options?.skipUserValidation) {
|
|
5003
|
+
await this.userService.validateUsersOrThrow(schema, data);
|
|
5004
|
+
}
|
|
4576
5005
|
}
|
|
4577
5006
|
const completionStatus = computeRecordStatus(schema, data);
|
|
4578
5007
|
const label = await this.computeLabel(schema, data);
|
|
@@ -4684,6 +5113,9 @@ var RecordService = class {
|
|
|
4684
5113
|
if (!options?.skipRelationValidation) {
|
|
4685
5114
|
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
4686
5115
|
}
|
|
5116
|
+
if (!options?.skipUserValidation) {
|
|
5117
|
+
await this.userService.validateUsersOrThrow(schema, data);
|
|
5118
|
+
}
|
|
4687
5119
|
}
|
|
4688
5120
|
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
4689
5121
|
const label = await this.computeLabel(schema, mergedData);
|
|
@@ -5341,7 +5773,7 @@ var ViewService = class {
|
|
|
5341
5773
|
throw new NotFoundError("View", viewId);
|
|
5342
5774
|
}
|
|
5343
5775
|
if (dbView.system) {
|
|
5344
|
-
throw new ProtectedResourceError("
|
|
5776
|
+
throw new ProtectedResourceError("view", dbView.name, "modify");
|
|
5345
5777
|
}
|
|
5346
5778
|
const updated = await this.adapter.views.update(viewId, {
|
|
5347
5779
|
label: input.label,
|
|
@@ -5364,7 +5796,7 @@ var ViewService = class {
|
|
|
5364
5796
|
throw new NotFoundError("View", viewId);
|
|
5365
5797
|
}
|
|
5366
5798
|
if (dbView.system) {
|
|
5367
|
-
throw new ProtectedResourceError("
|
|
5799
|
+
throw new ProtectedResourceError("view", dbView.name, "delete");
|
|
5368
5800
|
}
|
|
5369
5801
|
await this.adapter.views.delete(viewId);
|
|
5370
5802
|
}
|
|
@@ -5790,6 +6222,7 @@ var NoopGeocodingAdapter = class {
|
|
|
5790
6222
|
RecordService,
|
|
5791
6223
|
RelationService,
|
|
5792
6224
|
UserProfileService,
|
|
6225
|
+
UserService,
|
|
5793
6226
|
ViewService,
|
|
5794
6227
|
buildAuditChanges,
|
|
5795
6228
|
createMockAdapter,
|