@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/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
- * Create a new file record (after upload to storage)
1597
+ * Upload a file to storage and create metadata record.
1562
1598
  *
1563
- * @param data - File metadata
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 service = new FileService(adapter, "tenant-123");
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
- * // After uploading to S3
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
- return await this.adapter.files.create(data);
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
- return await this.adapter.files.update(fileId, data);
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
- * Check if user has access to a file
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(_attr) {
2727
- return import_zod4.z.string().uuid();
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(_attr) {
2730
- return import_zod4.z.string().uuid();
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.string().uuid({
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.string().uuid({
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("attribute", dbView.name, "modify");
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("attribute", dbView.name, "delete");
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,