@stndrds/schema 0.1.0-alpha.16 → 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.
Files changed (38) hide show
  1. package/dist/chunk-2PBQNUTS.mjs +7400 -0
  2. package/dist/chunk-4D23X5XN.mjs +7257 -0
  3. package/dist/chunk-4D476S25.mjs +7713 -0
  4. package/dist/chunk-6RPWHMTH.mjs +7101 -0
  5. package/dist/chunk-CMO3JEJL.mjs +7259 -0
  6. package/dist/chunk-CWPOGEK2.mjs +7707 -0
  7. package/dist/chunk-EWNSOD22.mjs +7707 -0
  8. package/dist/chunk-JWRGX3UW.mjs +7265 -0
  9. package/dist/chunk-KZSM5546.mjs +7706 -0
  10. package/dist/chunk-L7AQ7SZC.mjs +7432 -0
  11. package/dist/chunk-LZW2MI43.mjs +7404 -0
  12. package/dist/chunk-OUSLILLE.mjs +7259 -0
  13. package/dist/chunk-PMJZPJDP.mjs +7265 -0
  14. package/dist/chunk-V5QXU2OK.mjs +82 -0
  15. package/dist/chunk-XMGMXAHW.mjs +7706 -0
  16. package/dist/chunk-YEV46K3M.mjs +7259 -0
  17. package/dist/default-roles-AKWIWTLE.mjs +20 -0
  18. package/dist/index.d.mts +55 -5
  19. package/dist/index.d.ts +55 -5
  20. package/dist/index.js +890 -110
  21. package/dist/index.mjs +14 -2
  22. package/dist/runtime--rp-TG-h.d.mts +5181 -0
  23. package/dist/runtime--rp-TG-h.d.ts +5181 -0
  24. package/dist/runtime-DWhSpnn4.d.mts +5256 -0
  25. package/dist/runtime-DWhSpnn4.d.ts +5256 -0
  26. package/dist/runtime-Dw2exb5q.d.mts +5601 -0
  27. package/dist/runtime-Dw2exb5q.d.ts +5601 -0
  28. package/dist/runtime-DySuNV6Y.d.mts +5245 -0
  29. package/dist/runtime-DySuNV6Y.d.ts +5245 -0
  30. package/dist/runtime-DzdsFCyL.d.mts +5601 -0
  31. package/dist/runtime-DzdsFCyL.d.ts +5601 -0
  32. package/dist/runtime-uXhTgSIx.d.mts +5260 -0
  33. package/dist/runtime-uXhTgSIx.d.ts +5260 -0
  34. package/dist/runtime.d.mts +1 -1
  35. package/dist/runtime.d.ts +1 -1
  36. package/dist/runtime.js +974 -168
  37. package/dist/runtime.mjs +5 -1
  38. 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,9 +135,11 @@ __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,
142
+ enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
137
143
  extractAttributeNames: () => extractAttributeNames,
138
144
  getSyncPreview: () => getSyncPreview,
139
145
  getViewSyncPreview: () => getViewSyncPreview,
@@ -165,6 +171,175 @@ function generateId() {
165
171
  });
166
172
  }
167
173
 
174
+ // src/format.ts
175
+ var import_constants = require("@stndrds/constants");
176
+ var EMPTY_VALUE_PLACEHOLDER = "\u2014";
177
+ function formatText(value) {
178
+ return String(value);
179
+ }
180
+ function formatCheckbox(value) {
181
+ return value ? "Yes" : "No";
182
+ }
183
+ function formatNumber(value, attribute) {
184
+ if (typeof value !== "number") return String(value);
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
+ }
193
+ return value.toLocaleString(void 0, {
194
+ minimumFractionDigits: decimals,
195
+ maximumFractionDigits: decimals
196
+ });
197
+ }
198
+ function formatCurrency(value, _attribute) {
199
+ if (typeof value !== "object" || value === null) return String(value);
200
+ const currency = value;
201
+ if (!("value" in currency && "code" in currency)) return String(value);
202
+ const formattedValue = currency.value.toLocaleString(void 0, {
203
+ minimumFractionDigits: 2,
204
+ maximumFractionDigits: 2
205
+ });
206
+ return `${formattedValue} ${currency.code}`;
207
+ }
208
+ function formatDate(value) {
209
+ if (value instanceof Date) {
210
+ return value.toISOString().split("T")[0];
211
+ }
212
+ if (typeof value === "string") {
213
+ const date = new Date(value);
214
+ if (!Number.isNaN(date.getTime())) {
215
+ return date.toISOString().split("T")[0];
216
+ }
217
+ }
218
+ return String(value);
219
+ }
220
+ function formatTimestamp(value) {
221
+ if (typeof value === "number") {
222
+ return new Date(value).toISOString();
223
+ }
224
+ if (value instanceof Date) {
225
+ return value.toISOString();
226
+ }
227
+ return String(value);
228
+ }
229
+ function formatPhone(value) {
230
+ if (typeof value !== "object" || value === null) return String(value);
231
+ const phone = value;
232
+ if (!("phoneNumber" in phone)) return String(value);
233
+ if (phone.countryCode) {
234
+ const country = (0, import_constants.getCountryByIso3)(phone.countryCode);
235
+ const dial = country?.phoneCode ?? "";
236
+ return `${dial} ${phone.phoneNumber}`.trim();
237
+ }
238
+ return phone.phoneNumber;
239
+ }
240
+ function formatLocation(value, attribute) {
241
+ if (typeof value !== "object" || value === null) return String(value);
242
+ const loc = value;
243
+ const granularity = attribute.granularity ?? "full";
244
+ const parts = [];
245
+ switch (granularity) {
246
+ case "country":
247
+ if (loc.country) parts.push(loc.country);
248
+ break;
249
+ case "state":
250
+ if (loc.state) parts.push(loc.state);
251
+ if (loc.country) parts.push(loc.country);
252
+ break;
253
+ case "city":
254
+ if (loc.city) parts.push(loc.city);
255
+ if (loc.state) parts.push(loc.state);
256
+ if (loc.country) parts.push(loc.country);
257
+ break;
258
+ case "coordinates":
259
+ if (loc.latitude !== void 0 && loc.longitude !== void 0) {
260
+ parts.push(`${loc.latitude}, ${loc.longitude}`);
261
+ }
262
+ break;
263
+ case "address":
264
+ if (loc.address) parts.push(loc.address);
265
+ if (loc.city) parts.push(loc.city);
266
+ if (loc.state) parts.push(loc.state);
267
+ if (loc.country) parts.push(loc.country);
268
+ break;
269
+ default:
270
+ if (loc.address) parts.push(loc.address);
271
+ if (loc.city) parts.push(loc.city);
272
+ if (loc.state) parts.push(loc.state);
273
+ if (loc.postalCode) parts.push(loc.postalCode);
274
+ if (loc.country) parts.push(loc.country);
275
+ break;
276
+ }
277
+ return parts.join(", ") || EMPTY_VALUE_PLACEHOLDER;
278
+ }
279
+ function formatSelect(value, attribute) {
280
+ if (typeof value !== "string") return String(value);
281
+ const option = attribute.options?.find((o) => o.value === value);
282
+ return option?.label ?? String(value);
283
+ }
284
+ function formatMultiselect(value, attribute) {
285
+ if (!Array.isArray(value)) return String(value);
286
+ if (attribute.options) {
287
+ const labels = value.map((v) => attribute.options.find((o) => o.value === v)?.label).filter(Boolean);
288
+ return labels.join(", ");
289
+ }
290
+ return value.join(", ");
291
+ }
292
+ function formatRating(value, attribute) {
293
+ if (typeof value !== "number") return String(value);
294
+ const max = attribute.max ?? 5;
295
+ return `${value}/${max}`;
296
+ }
297
+ function formatAttributeValue(value, attribute) {
298
+ if (value === null || value === void 0 || value === "") {
299
+ return EMPTY_VALUE_PLACEHOLDER;
300
+ }
301
+ switch (attribute.type) {
302
+ case "text":
303
+ case "textarea":
304
+ return formatText(value);
305
+ case "checkbox":
306
+ return formatCheckbox(value);
307
+ case "number":
308
+ return formatNumber(value, attribute);
309
+ case "currency":
310
+ return formatCurrency(value, attribute);
311
+ case "date":
312
+ return formatDate(value);
313
+ case "timestamp":
314
+ return formatTimestamp(value);
315
+ case "phone":
316
+ return formatPhone(value);
317
+ case "location":
318
+ return formatLocation(value, attribute);
319
+ case "select":
320
+ case "status":
321
+ return formatSelect(value, attribute);
322
+ case "multiselect":
323
+ return formatMultiselect(value, attribute);
324
+ case "rating":
325
+ return formatRating(value, attribute);
326
+ // Unsupported types - return value as-is or placeholder
327
+ case "file":
328
+ case "user":
329
+ case "relation":
330
+ if (Array.isArray(value)) {
331
+ return value.join(", ");
332
+ }
333
+ return String(value);
334
+ default: {
335
+ if (Array.isArray(value)) {
336
+ return value.join(", ");
337
+ }
338
+ return String(value);
339
+ }
340
+ }
341
+ }
342
+
168
343
  // src/runtime/template.ts
169
344
  var pipes = {
170
345
  /** Convert to uppercase */
@@ -216,6 +391,24 @@ function extractAttributeNames(template) {
216
391
  }
217
392
  return names;
218
393
  }
394
+ function hasOptions(attr) {
395
+ return "options" in attr && Array.isArray(attr.options) && attr.options.length > 0;
396
+ }
397
+ function enrichValuesWithSelectLabels(values, attributes) {
398
+ const enriched = { ...values };
399
+ for (const attr of attributes) {
400
+ const value = values[attr.name];
401
+ if (value == null) continue;
402
+ const isSelectLike = attr.type === "select" || attr.type === "status" || attr.type === "multiselect";
403
+ if (!(isSelectLike && hasOptions(attr))) continue;
404
+ if (attr.type === "multiselect" && Array.isArray(value) && value.length === 0) continue;
405
+ const formatted = formatAttributeValue(value, attr);
406
+ if (formatted && formatted !== EMPTY_VALUE_PLACEHOLDER) {
407
+ enriched[attr.name] = formatted;
408
+ }
409
+ }
410
+ return enriched;
411
+ }
219
412
 
220
413
  // src/runtime/mock-adapter.ts
221
414
  function createMockObjectsRepository(stores) {
@@ -692,14 +885,31 @@ function createMockObjectRecordsRepository(stores) {
692
885
  (options.offset ?? 0) + options.limit
693
886
  );
694
887
  }
888
+ const attributesByObjectId = /* @__PURE__ */ new Map();
889
+ for (const attr of stores.attributes.values()) {
890
+ if (!attributesByObjectId.has(attr.objectId)) {
891
+ attributesByObjectId.set(attr.objectId, []);
892
+ }
893
+ attributesByObjectId.get(attr.objectId)?.push(attr);
894
+ }
695
895
  const results = matchingRecords.map((r) => {
696
896
  const obj = objectsMap.get(r.objectId);
697
897
  const labelExpression = obj?.labelExpression ?? "{{ name }}";
898
+ const dbAttrs = attributesByObjectId.get(r.objectId) ?? [];
899
+ const attrs = dbAttrs.map((a) => ({
900
+ ...a.config,
901
+ id: a.id,
902
+ name: a.name,
903
+ type: a.type,
904
+ label: a.config.label ?? a.name,
905
+ required: a.config.required ?? false
906
+ }));
907
+ const enrichedValues = enrichValuesWithSelectLabels(r.values, attrs);
698
908
  return {
699
909
  objectId: r.objectId,
700
910
  objectName: obj?.name ?? "unknown",
701
911
  objectLabel: obj?.label ?? "Unknown",
702
- label: renderLabelExpression(labelExpression, r.values),
912
+ label: renderLabelExpression(labelExpression, enrichedValues),
703
913
  recordId: r.id,
704
914
  values: r.values,
705
915
  completionStatus: r.completionStatus,
@@ -1116,6 +1326,10 @@ var AuditService = class {
1116
1326
  this.options = options;
1117
1327
  this.buffer = [];
1118
1328
  this.flushTimer = null;
1329
+ /** Prevents concurrent flush operations */
1330
+ this.isFlushing = false;
1331
+ /** Pending flush promise to allow waiting on concurrent flush */
1332
+ this.flushPromise = null;
1119
1333
  if (options?.async && options.flushIntervalMs) {
1120
1334
  this.startFlushTimer();
1121
1335
  }
@@ -1202,6 +1416,24 @@ var AuditService = class {
1202
1416
  };
1203
1417
  await this.log(entry);
1204
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
+ }
1205
1437
  /**
1206
1438
  * Log a system action (no actor)
1207
1439
  */
@@ -1268,14 +1500,23 @@ var AuditService = class {
1268
1500
  // ============================================================================
1269
1501
  /**
1270
1502
  * Flush buffered logs to the database
1503
+ * Protected against concurrent flush calls
1271
1504
  */
1272
1505
  async flush() {
1506
+ if (this.isFlushing && this.flushPromise) {
1507
+ return this.flushPromise;
1508
+ }
1273
1509
  if (this.buffer.length === 0 || !this.adapter.audit) {
1274
1510
  return;
1275
1511
  }
1512
+ this.isFlushing = true;
1276
1513
  const entries = [...this.buffer];
1277
1514
  this.buffer = [];
1278
- await this.adapter.audit.createMany(entries);
1515
+ this.flushPromise = this.adapter.audit.createMany(entries).finally(() => {
1516
+ this.isFlushing = false;
1517
+ this.flushPromise = null;
1518
+ });
1519
+ return this.flushPromise;
1279
1520
  }
1280
1521
  /**
1281
1522
  * Clean up resources (stop timer, flush remaining logs)
@@ -1297,6 +1538,12 @@ var AuditService = class {
1297
1538
  if (!this.adapter.audit) {
1298
1539
  return;
1299
1540
  }
1541
+ if (entry.actorId && !entry.actorEmail && entry.actorType === "user") {
1542
+ const profile = await this.adapter.userProfiles.findById(entry.actorId);
1543
+ if (profile) {
1544
+ entry.actorEmail = profile.email;
1545
+ }
1546
+ }
1300
1547
  if (this.options?.async) {
1301
1548
  this.buffer.push(entry);
1302
1549
  const batchSize = this.options.batchSize ?? 10;
@@ -1336,21 +1583,105 @@ var AuditService = class {
1336
1583
 
1337
1584
  // src/runtime/services/file.service.ts
1338
1585
  var FileService = class {
1339
- constructor(adapter, tenantId) {
1586
+ constructor(adapter, tenantId, options) {
1340
1587
  this.adapter = adapter;
1341
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;
1342
1592
  }
1593
+ // ============================================================================
1594
+ // UPLOAD (requires StorageAdapter)
1595
+ // ============================================================================
1343
1596
  /**
1344
- * Create a new file record (after upload to storage)
1597
+ * Upload a file to storage and create metadata record.
1345
1598
  *
1346
- * @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
1347
1607
  * @returns Created file record
1608
+ * @throws Error if StorageAdapter is not configured
1348
1609
  *
1349
1610
  * @example
1350
1611
  * ```typescript
1351
- * 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.
1678
+ *
1679
+ * @param data - File metadata
1680
+ * @returns Created file record
1352
1681
  *
1353
- * // After uploading to S3
1682
+ * @example
1683
+ * ```typescript
1684
+ * // After uploading to S3 with Multer
1354
1685
  * const file = await service.createFile({
1355
1686
  * tenantId: "tenant-123",
1356
1687
  * name: "contract-2025.pdf",
@@ -1372,8 +1703,26 @@ var FileService = class {
1372
1703
  `Tenant mismatch: service initialized with "${this.tenantId}" but data has "${data.tenantId}"`
1373
1704
  );
1374
1705
  }
1375
- 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;
1376
1722
  }
1723
+ // ============================================================================
1724
+ // READ
1725
+ // ============================================================================
1377
1726
  /**
1378
1727
  * Get file by ID
1379
1728
  */
@@ -1394,6 +1743,9 @@ var FileService = class {
1394
1743
  }
1395
1744
  return file;
1396
1745
  }
1746
+ // ============================================================================
1747
+ // UPDATE
1748
+ // ============================================================================
1397
1749
  /**
1398
1750
  * Update file metadata
1399
1751
  *
@@ -1402,18 +1754,44 @@ var FileService = class {
1402
1754
  * @returns Updated file
1403
1755
  */
1404
1756
  async updateFile(fileId, data) {
1405
- await this.getFileOrThrow(fileId);
1406
- 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;
1407
1782
  }
1783
+ // ============================================================================
1784
+ // DELETE
1785
+ // ============================================================================
1408
1786
  /**
1409
- * Delete file (soft delete)
1787
+ * Delete file (soft delete by default)
1410
1788
  *
1411
1789
  * @param fileId - File UUID
1412
1790
  * @param options - Delete options
1413
1791
  */
1414
1792
  async deleteFile(fileId, options) {
1793
+ const file = await this.getFileOrThrow(fileId);
1415
1794
  if (options?.checkOwnership && options.userId) {
1416
- const file = await this.getFileOrThrow(fileId);
1417
1795
  if (file.uploadedBy !== options.userId) {
1418
1796
  throw new Error("You can only delete files you uploaded");
1419
1797
  }
@@ -1423,7 +1801,84 @@ var FileService = class {
1423
1801
  } else {
1424
1802
  await this.adapter.files.delete(fileId);
1425
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
+ }
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
+ }
1426
1878
  }
1879
+ // ============================================================================
1880
+ // LIST
1881
+ // ============================================================================
1427
1882
  /**
1428
1883
  * List files for the tenant
1429
1884
  */
@@ -1442,6 +1897,74 @@ var FileService = class {
1442
1897
  async listFilesByUploader(uploadedBy) {
1443
1898
  return await this.adapter.files.findByUploader(uploadedBy);
1444
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
+ }
1445
1968
  /**
1446
1969
  * Change file visibility
1447
1970
  *
@@ -1485,29 +2008,9 @@ var FileService = class {
1485
2008
  allowedUsers: newAllowed
1486
2009
  });
1487
2010
  }
1488
- /**
1489
- * Check if user has access to a file
1490
- *
1491
- * @param fileId - File UUID
1492
- * @param userId - User ID to check
1493
- * @returns true if user can access the file
1494
- */
1495
- async canAccess(fileId, userId) {
1496
- const file = await this.getFile(fileId);
1497
- if (!file) {
1498
- return false;
1499
- }
1500
- if (file.visibility === "public") {
1501
- return true;
1502
- }
1503
- if (file.uploadedBy === userId) {
1504
- return true;
1505
- }
1506
- if (file.visibility === "restricted") {
1507
- return file.allowedUsers?.includes(userId) ?? false;
1508
- }
1509
- return false;
1510
- }
2011
+ // ============================================================================
2012
+ // ORGANIZATION
2013
+ // ============================================================================
1511
2014
  /**
1512
2015
  * Move file to different folder
1513
2016
  */
@@ -1534,6 +2037,115 @@ var FileService = class {
1534
2037
  }
1535
2038
  };
1536
2039
 
2040
+ // src/exceptions.ts
2041
+ var SchemaErrorCode = {
2042
+ // Generic
2043
+ UNKNOWN: "SCHEMA_UNKNOWN_ERROR",
2044
+ // Not Found
2045
+ OBJECT_NOT_FOUND: "SCHEMA_OBJECT_NOT_FOUND",
2046
+ ATTRIBUTE_NOT_FOUND: "SCHEMA_ATTRIBUTE_NOT_FOUND",
2047
+ RECORD_NOT_FOUND: "SCHEMA_RECORD_NOT_FOUND",
2048
+ USER_PROFILE_NOT_FOUND: "SCHEMA_USER_PROFILE_NOT_FOUND",
2049
+ FILE_NOT_FOUND: "SCHEMA_FILE_NOT_FOUND",
2050
+ ROLE_NOT_FOUND: "SCHEMA_ROLE_NOT_FOUND",
2051
+ // Validation
2052
+ VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED",
2053
+ INVALID_ATTRIBUTE_NAME: "SCHEMA_INVALID_ATTRIBUTE_NAME",
2054
+ INVALID_OBJECT_NAME: "SCHEMA_INVALID_OBJECT_NAME",
2055
+ // Protected Resources
2056
+ PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
2057
+ PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
2058
+ PROTECTED_VIEW: "SCHEMA_PROTECTED_VIEW",
2059
+ PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
2060
+ // Permissions
2061
+ FORBIDDEN: "SCHEMA_FORBIDDEN",
2062
+ // Sync
2063
+ SYNC_FAILED: "SCHEMA_SYNC_FAILED",
2064
+ NOT_SYSTEM_OBJECT: "SCHEMA_NOT_SYSTEM_OBJECT",
2065
+ // Duplicates
2066
+ DUPLICATE_OBJECT: "SCHEMA_DUPLICATE_OBJECT",
2067
+ DUPLICATE_ATTRIBUTE: "SCHEMA_DUPLICATE_ATTRIBUTE"
2068
+ };
2069
+ var SchemaError = class extends Error {
2070
+ constructor(message, code = SchemaErrorCode.UNKNOWN, details) {
2071
+ super(message);
2072
+ this.name = "SchemaError";
2073
+ this.code = code;
2074
+ this.details = details;
2075
+ Object.setPrototypeOf(this, new.target.prototype);
2076
+ }
2077
+ toJSON() {
2078
+ return {
2079
+ name: this.name,
2080
+ code: this.code,
2081
+ message: this.message,
2082
+ details: this.details
2083
+ };
2084
+ }
2085
+ };
2086
+ var NotFoundError = class extends SchemaError {
2087
+ constructor(resourceType, resourceId, code = SchemaErrorCode.RECORD_NOT_FOUND) {
2088
+ super(`${resourceType} with id "${resourceId}" not found`, code, {
2089
+ resourceType,
2090
+ resourceId
2091
+ });
2092
+ this.name = "NotFoundError";
2093
+ this.resourceType = resourceType;
2094
+ this.resourceId = resourceId;
2095
+ }
2096
+ };
2097
+ var RecordNotFoundError = class extends NotFoundError {
2098
+ constructor(recordId) {
2099
+ super("Record", recordId, SchemaErrorCode.RECORD_NOT_FOUND);
2100
+ this.name = "RecordNotFoundError";
2101
+ }
2102
+ };
2103
+ var ValidationError = class _ValidationError extends SchemaError {
2104
+ constructor(message, errors) {
2105
+ super(message, SchemaErrorCode.VALIDATION_FAILED, { errors });
2106
+ this.name = "ValidationError";
2107
+ this.errors = errors;
2108
+ }
2109
+ /**
2110
+ * Create a validation error from Zod-style errors
2111
+ */
2112
+ static fromZodErrors(errors) {
2113
+ const details = errors.map((err) => ({
2114
+ path: err.path.map(String),
2115
+ message: err.message
2116
+ }));
2117
+ const message = `Validation failed: ${details.map((d) => `${d.path.join(".")}: ${d.message}`).join(", ")}`;
2118
+ return new _ValidationError(message, details);
2119
+ }
2120
+ };
2121
+ var ProtectedResourceError = class extends SchemaError {
2122
+ constructor(resourceType, resourceName, operation) {
2123
+ const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : resourceType === "view" ? SchemaErrorCode.PROTECTED_VIEW : SchemaErrorCode.PROTECTED_ATTRIBUTE;
2124
+ super(`Cannot ${operation} system ${resourceType} "${resourceName}"`, code, {
2125
+ resourceType,
2126
+ resourceName,
2127
+ operation
2128
+ });
2129
+ this.name = "ProtectedResourceError";
2130
+ this.resourceType = resourceType;
2131
+ this.resourceName = resourceName;
2132
+ this.operation = operation;
2133
+ }
2134
+ };
2135
+ var ForbiddenError = class extends SchemaError {
2136
+ constructor(objectName, action, userId) {
2137
+ super(`No ${action} permission on object "${objectName}"`, SchemaErrorCode.FORBIDDEN, {
2138
+ objectName,
2139
+ action,
2140
+ userId
2141
+ });
2142
+ this.name = "ForbiddenError";
2143
+ this.objectName = objectName;
2144
+ this.action = action;
2145
+ this.userId = userId;
2146
+ }
2147
+ };
2148
+
1537
2149
  // src/runtime/services/flow.service.ts
1538
2150
  var FlowService = class {
1539
2151
  constructor(adapter, systemFlows = []) {
@@ -1594,16 +2206,26 @@ var FlowService = class {
1594
2206
  */
1595
2207
  async createFlow(input, tenantId) {
1596
2208
  if (!this.adapter.flows) {
1597
- throw new Error("Flows feature is not enabled. Database adapter does not support flows.");
2209
+ throw new SchemaError(
2210
+ "Flows feature is not enabled. Database adapter does not support flows.",
2211
+ SchemaErrorCode.UNKNOWN,
2212
+ { feature: "flows" }
2213
+ );
1598
2214
  }
1599
2215
  this.validateFlowName(input.name);
1600
2216
  const existing = await this.adapter.flows.findByName(tenantId, input.name);
1601
2217
  if (existing) {
1602
- throw new Error(`Flow "${input.name}" already exists`);
2218
+ throw new SchemaError(
2219
+ `Flow "${input.name}" already exists`,
2220
+ SchemaErrorCode.DUPLICATE_OBJECT,
2221
+ { flowName: input.name }
2222
+ );
1603
2223
  }
1604
2224
  if (this.systemFlows.has(input.name)) {
1605
- throw new Error(
1606
- `Cannot create flow "${input.name}": a system flow with this name already exists`
2225
+ throw new SchemaError(
2226
+ `Cannot create flow "${input.name}": a system flow with this name already exists`,
2227
+ SchemaErrorCode.DUPLICATE_OBJECT,
2228
+ { flowName: input.name, system: true }
1607
2229
  );
1608
2230
  }
1609
2231
  this.validateFlowStructure(input);
@@ -1628,14 +2250,16 @@ var FlowService = class {
1628
2250
  */
1629
2251
  async updateFlow(flowId, input) {
1630
2252
  if (!this.adapter.flows) {
1631
- throw new Error("Flows feature is not enabled.");
2253
+ throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
2254
+ feature: "flows"
2255
+ });
1632
2256
  }
1633
2257
  const dbFlow = await this.adapter.flows.findById(flowId);
1634
2258
  if (!dbFlow) {
1635
- throw new Error(`Flow with id "${flowId}" not found`);
2259
+ throw new NotFoundError("Flow", flowId);
1636
2260
  }
1637
2261
  if (dbFlow.system) {
1638
- throw new Error("Cannot modify system flows. System flows are protected.");
2262
+ throw new ProtectedResourceError("object", dbFlow.name, "modify");
1639
2263
  }
1640
2264
  if (input.slots || input.pages || input.relations) {
1641
2265
  this.validateFlowStructure({
@@ -1662,14 +2286,16 @@ var FlowService = class {
1662
2286
  */
1663
2287
  async publishFlow(flowId) {
1664
2288
  if (!this.adapter.flows) {
1665
- throw new Error("Flows feature is not enabled.");
2289
+ throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
2290
+ feature: "flows"
2291
+ });
1666
2292
  }
1667
2293
  const dbFlow = await this.adapter.flows.findById(flowId);
1668
2294
  if (!dbFlow) {
1669
- throw new Error(`Flow with id "${flowId}" not found`);
2295
+ throw new NotFoundError("Flow", flowId);
1670
2296
  }
1671
2297
  if (dbFlow.system) {
1672
- throw new Error("Cannot publish system flows. They are always published.");
2298
+ throw new ProtectedResourceError("object", dbFlow.name, "modify");
1673
2299
  }
1674
2300
  if (dbFlow.status === "published") {
1675
2301
  return this.convertDBFlowToDefinition(dbFlow);
@@ -1692,14 +2318,16 @@ var FlowService = class {
1692
2318
  */
1693
2319
  async archiveFlow(flowId) {
1694
2320
  if (!this.adapter.flows) {
1695
- throw new Error("Flows feature is not enabled.");
2321
+ throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
2322
+ feature: "flows"
2323
+ });
1696
2324
  }
1697
2325
  const dbFlow = await this.adapter.flows.findById(flowId);
1698
2326
  if (!dbFlow) {
1699
- throw new Error(`Flow with id "${flowId}" not found`);
2327
+ throw new NotFoundError("Flow", flowId);
1700
2328
  }
1701
2329
  if (dbFlow.system) {
1702
- throw new Error("Cannot archive system flows.");
2330
+ throw new ProtectedResourceError("object", dbFlow.name, "modify");
1703
2331
  }
1704
2332
  const updated = await this.adapter.flows.update(flowId, {
1705
2333
  status: "archived"
@@ -1711,14 +2339,16 @@ var FlowService = class {
1711
2339
  */
1712
2340
  async deleteFlow(flowId) {
1713
2341
  if (!this.adapter.flows) {
1714
- throw new Error("Flows feature is not enabled.");
2342
+ throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
2343
+ feature: "flows"
2344
+ });
1715
2345
  }
1716
2346
  const dbFlow = await this.adapter.flows.findById(flowId);
1717
2347
  if (!dbFlow) {
1718
- throw new Error(`Flow with id "${flowId}" not found`);
2348
+ throw new NotFoundError("Flow", flowId);
1719
2349
  }
1720
2350
  if (dbFlow.system) {
1721
- throw new Error("Cannot delete system flows. System flows are protected.");
2351
+ throw new ProtectedResourceError("object", dbFlow.name, "delete");
1722
2352
  }
1723
2353
  await this.adapter.flows.delete(flowId);
1724
2354
  }
@@ -1730,16 +2360,23 @@ var FlowService = class {
1730
2360
  */
1731
2361
  validateFlowName(name) {
1732
2362
  if (!name || name.length === 0) {
1733
- throw new Error("Flow name cannot be empty");
2363
+ throw new ValidationError("Flow name cannot be empty", [
2364
+ { path: ["name"], message: "Flow name cannot be empty" }
2365
+ ]);
1734
2366
  }
1735
2367
  if (name.length > 63) {
1736
- throw new Error("Flow name is too long (max 63 characters)");
2368
+ throw new ValidationError("Flow name is too long", [
2369
+ { path: ["name"], message: "Flow name is too long (max 63 characters)" }
2370
+ ]);
1737
2371
  }
1738
2372
  const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
1739
2373
  if (!kebabCaseRegex.test(name)) {
1740
- throw new Error(
1741
- "Invalid flow name format. Name must be in kebab-case (e.g., 'couple-creation', 'new-contact')"
1742
- );
2374
+ throw new ValidationError("Invalid flow name format", [
2375
+ {
2376
+ path: ["name"],
2377
+ message: "Flow name must be in kebab-case (e.g., 'couple-creation', 'new-contact')"
2378
+ }
2379
+ ]);
1743
2380
  }
1744
2381
  }
1745
2382
  /**
@@ -1747,33 +2384,59 @@ var FlowService = class {
1747
2384
  */
1748
2385
  validateFlowStructure(input) {
1749
2386
  if (!input.slots || input.slots.length === 0) {
1750
- throw new Error("Flow must have at least one slot");
2387
+ throw new ValidationError("Flow must have at least one slot", [
2388
+ { path: ["slots"], message: "Flow must have at least one slot" }
2389
+ ]);
1751
2390
  }
1752
2391
  if (!input.pages || input.pages.length === 0) {
1753
- throw new Error("Flow must have at least one page");
2392
+ throw new ValidationError("Flow must have at least one page", [
2393
+ { path: ["pages"], message: "Flow must have at least one page" }
2394
+ ]);
1754
2395
  }
1755
2396
  const slotIds = new Set(input.slots.map((s) => s.id));
1756
2397
  if (slotIds.size !== input.slots.length) {
1757
- throw new Error("Duplicate slot IDs detected");
2398
+ throw new ValidationError("Duplicate slot IDs detected", [
2399
+ { path: ["slots"], message: "Duplicate slot IDs detected" }
2400
+ ]);
1758
2401
  }
1759
2402
  for (const page of input.pages) {
1760
2403
  for (const row of page.rows) {
1761
2404
  for (const field of row.fields) {
1762
2405
  if (!slotIds.has(field.slotId)) {
1763
- throw new Error(`Field "${field.id}" references unknown slot "${field.slotId}"`);
2406
+ throw new ValidationError("Field references unknown slot", [
2407
+ {
2408
+ path: ["pages", page.id, "rows", row.id, "fields", field.id],
2409
+ message: `Field "${field.id}" references unknown slot "${field.slotId}"`
2410
+ }
2411
+ ]);
1764
2412
  }
1765
2413
  }
1766
2414
  }
1767
2415
  }
1768
2416
  for (const relation of input.relations) {
1769
2417
  if (!slotIds.has(relation.sourceSlotId)) {
1770
- throw new Error(`Relation references unknown source slot "${relation.sourceSlotId}"`);
2418
+ throw new ValidationError("Relation references unknown source slot", [
2419
+ {
2420
+ path: ["relations", relation.id, "sourceSlotId"],
2421
+ message: `Relation references unknown source slot "${relation.sourceSlotId}"`
2422
+ }
2423
+ ]);
1771
2424
  }
1772
2425
  if (!slotIds.has(relation.targetSlotId)) {
1773
- throw new Error(`Relation references unknown target slot "${relation.targetSlotId}"`);
2426
+ throw new ValidationError("Relation references unknown target slot", [
2427
+ {
2428
+ path: ["relations", relation.id, "targetSlotId"],
2429
+ message: `Relation references unknown target slot "${relation.targetSlotId}"`
2430
+ }
2431
+ ]);
1774
2432
  }
1775
2433
  if (relation.sourceSlotId === relation.targetSlotId) {
1776
- throw new Error(`Relation cannot link a slot to itself: "${relation.sourceSlotId}"`);
2434
+ throw new ValidationError("Relation cannot link a slot to itself", [
2435
+ {
2436
+ path: ["relations", relation.id],
2437
+ message: `Relation cannot link a slot to itself: "${relation.sourceSlotId}"`
2438
+ }
2439
+ ]);
1777
2440
  }
1778
2441
  }
1779
2442
  }
@@ -1917,8 +2580,8 @@ var GlobalSearchService = class {
1917
2580
  };
1918
2581
 
1919
2582
  // src/builders/attribute-validators.ts
1920
- var import_constants = require("@stndrds/constants");
1921
- var VALID_ICONS = new Set(import_constants.ICONS);
2583
+ var import_constants2 = require("@stndrds/constants");
2584
+ var VALID_ICONS = new Set(import_constants2.ICONS);
1922
2585
 
1923
2586
  // src/builders/flow-builder.ts
1924
2587
  var import_zod = require("zod");
@@ -2132,6 +2795,13 @@ var optionSchema = import_zod4.z.object({
2132
2795
  description: import_zod4.z.string().optional(),
2133
2796
  group: import_zod4.z.enum(["idle", "in_progress", "finished"]).optional()
2134
2797
  });
2798
+ var optionsArraySchema = import_zod4.z.array(optionSchema).min(1).refine(
2799
+ (options) => {
2800
+ const values = options.map((o) => o.value);
2801
+ return new Set(values).size === values.length;
2802
+ },
2803
+ { message: "Duplicate option values are not allowed" }
2804
+ );
2135
2805
  var relationTargetSchema = import_zod4.z.object({
2136
2806
  object: import_zod4.z.string().min(1),
2137
2807
  displayTemplate: import_zod4.z.string().optional(),
@@ -2192,7 +2862,7 @@ var currencyConfigSchema = baseConfigSchema.extend({
2192
2862
  allowedCurrencies: import_zod4.z.array(import_zod4.z.string().length(3)).optional()
2193
2863
  });
2194
2864
  var statusConfigSchema = baseConfigSchema.extend({
2195
- options: import_zod4.z.array(optionSchema).min(1)
2865
+ options: optionsArraySchema
2196
2866
  });
2197
2867
  var locationConfigSchema = baseConfigSchema.extend({
2198
2868
  granularity: import_zod4.z.enum(["full", "address", "city", "state", "country", "coordinates"]),
@@ -2206,19 +2876,21 @@ var timestampConfigSchema = baseConfigSchema.extend({
2206
2876
  autoUpdate: import_zod4.z.boolean().optional()
2207
2877
  });
2208
2878
  var selectConfigSchema = baseConfigSchema.extend({
2209
- options: import_zod4.z.array(optionSchema).min(1)
2879
+ options: optionsArraySchema
2210
2880
  });
2211
2881
  var multiselectConfigSchema = baseConfigSchema.extend({
2212
- options: import_zod4.z.array(optionSchema).min(1)
2882
+ options: optionsArraySchema
2213
2883
  });
2214
2884
  var fileConfigSchema = baseConfigSchema.extend({
2215
2885
  maxFiles: import_zod4.z.number().int().min(1).optional(),
2216
2886
  maxSize: import_zod4.z.number().int().min(1).optional(),
2217
2887
  allowedTypes: import_zod4.z.array(import_zod4.z.string()).optional(),
2218
- verification: fileVerificationConfigSchema.optional()
2888
+ verification: fileVerificationConfigSchema.optional(),
2889
+ multiple: import_zod4.z.boolean().optional()
2219
2890
  });
2220
2891
  var userConfigSchema = baseConfigSchema.extend({
2221
- 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()
2222
2894
  });
2223
2895
  var relationConfigSchema = baseConfigSchema.extend({
2224
2896
  targets: import_zod4.z.array(relationTargetSchema).min(1),
@@ -2340,20 +3012,39 @@ function createLocationValidator(_attr) {
2340
3012
  function createTimestampValidator(_attr) {
2341
3013
  return import_zod4.z.number().int().positive();
2342
3014
  }
2343
- function createFileValidator(_attr) {
2344
- 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;
2345
3030
  }
2346
- function createUserValidator(_attr) {
2347
- 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;
2348
3039
  }
2349
3040
  function createSingleRelationValidator(attr) {
2350
- const uuidSchema = import_zod4.z.string().uuid({
3041
+ const uuidSchema = import_zod4.z.uuid({
2351
3042
  message: `${attr.label} must be a valid record ID`
2352
3043
  });
2353
3044
  return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
2354
3045
  }
2355
3046
  function createMultiRelationValidator(attr) {
2356
- const uuidSchema = import_zod4.z.string().uuid({
3047
+ const uuidSchema = import_zod4.z.uuid({
2357
3048
  message: `Each ${attr.label} item must be a valid record ID`
2358
3049
  });
2359
3050
  let arraySchema = import_zod4.z.array(uuidSchema);
@@ -2871,14 +3562,15 @@ var ObjectSchemaService = class {
2871
3562
  });
2872
3563
  }
2873
3564
  }
3565
+ const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
3566
+ const attributes = dbAttributes.map((attr) => this.convertDBAttributeToAttribute(attr));
2874
3567
  if (updates.labelExpression !== void 0 && updates.labelExpression !== oldValues.labelExpression) {
2875
3568
  const newExpression = updates.labelExpression;
2876
- await this.adapter.objectRecords.batchRefreshLabels(
2877
- objectId,
2878
- (values) => renderLabelExpression(newExpression, values)
2879
- );
3569
+ await this.adapter.objectRecords.batchRefreshLabels(objectId, (values) => {
3570
+ const enrichedValues = enrichValuesWithSelectLabels(values, attributes);
3571
+ return renderLabelExpression(newExpression, enrichedValues);
3572
+ });
2880
3573
  }
2881
- const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
2882
3574
  return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
2883
3575
  }
2884
3576
  /**
@@ -3191,71 +3883,14 @@ var ObjectSchemaService = class {
3191
3883
  }
3192
3884
  };
3193
3885
 
3194
- // src/exceptions.ts
3195
- var SchemaErrorCode = {
3196
- // Generic
3197
- UNKNOWN: "SCHEMA_UNKNOWN_ERROR",
3198
- // Not Found
3199
- OBJECT_NOT_FOUND: "SCHEMA_OBJECT_NOT_FOUND",
3200
- ATTRIBUTE_NOT_FOUND: "SCHEMA_ATTRIBUTE_NOT_FOUND",
3201
- RECORD_NOT_FOUND: "SCHEMA_RECORD_NOT_FOUND",
3202
- USER_PROFILE_NOT_FOUND: "SCHEMA_USER_PROFILE_NOT_FOUND",
3203
- FILE_NOT_FOUND: "SCHEMA_FILE_NOT_FOUND",
3204
- ROLE_NOT_FOUND: "SCHEMA_ROLE_NOT_FOUND",
3205
- // Validation
3206
- VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED",
3207
- INVALID_ATTRIBUTE_NAME: "SCHEMA_INVALID_ATTRIBUTE_NAME",
3208
- INVALID_OBJECT_NAME: "SCHEMA_INVALID_OBJECT_NAME",
3209
- // Protected Resources
3210
- PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
3211
- PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
3212
- PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
3213
- // Permissions
3214
- FORBIDDEN: "SCHEMA_FORBIDDEN",
3215
- // Sync
3216
- SYNC_FAILED: "SCHEMA_SYNC_FAILED",
3217
- NOT_SYSTEM_OBJECT: "SCHEMA_NOT_SYSTEM_OBJECT",
3218
- // Duplicates
3219
- DUPLICATE_OBJECT: "SCHEMA_DUPLICATE_OBJECT",
3220
- DUPLICATE_ATTRIBUTE: "SCHEMA_DUPLICATE_ATTRIBUTE"
3221
- };
3222
- var SchemaError = class extends Error {
3223
- constructor(message, code = SchemaErrorCode.UNKNOWN, details) {
3224
- super(message);
3225
- this.name = "SchemaError";
3226
- this.code = code;
3227
- this.details = details;
3228
- Object.setPrototypeOf(this, new.target.prototype);
3229
- }
3230
- toJSON() {
3231
- return {
3232
- name: this.name,
3233
- code: this.code,
3234
- message: this.message,
3235
- details: this.details
3236
- };
3237
- }
3238
- };
3239
- var ForbiddenError = class extends SchemaError {
3240
- constructor(objectName, action, userId) {
3241
- super(`No ${action} permission on object "${objectName}"`, SchemaErrorCode.FORBIDDEN, {
3242
- objectName,
3243
- action,
3244
- userId
3245
- });
3246
- this.name = "ForbiddenError";
3247
- this.objectName = objectName;
3248
- this.action = action;
3249
- this.userId = userId;
3250
- }
3251
- };
3252
-
3253
3886
  // src/runtime/services/permission.service.ts
3254
3887
  var PermissionService = class {
3255
3888
  constructor(adapter, tenantId, options) {
3256
3889
  this.adapter = adapter;
3257
3890
  this.tenantId = tenantId;
3258
3891
  this.cache = /* @__PURE__ */ new Map();
3892
+ /** Track pending permission fetches to prevent duplicate concurrent requests */
3893
+ this.pendingFetches = /* @__PURE__ */ new Map();
3259
3894
  if (!adapter.permissions) {
3260
3895
  throw new Error(
3261
3896
  "PermissionService requires a DatabaseAdapter with permissions repository. Make sure your adapter implements the permissions property."
@@ -3404,6 +4039,23 @@ var PermissionService = class {
3404
4039
  if (cached && cached.expiresAt > Date.now()) {
3405
4040
  return cached.permissions;
3406
4041
  }
4042
+ const pendingFetch = this.pendingFetches.get(cacheKey);
4043
+ if (pendingFetch) {
4044
+ return pendingFetch;
4045
+ }
4046
+ const fetchPromise = this.fetchAndCachePermissions(userProfileId, cacheKey);
4047
+ this.pendingFetches.set(cacheKey, fetchPromise);
4048
+ try {
4049
+ return await fetchPromise;
4050
+ } finally {
4051
+ this.pendingFetches.delete(cacheKey);
4052
+ }
4053
+ }
4054
+ /**
4055
+ * Fetch permissions from database and cache the result
4056
+ * @internal
4057
+ */
4058
+ async fetchAndCachePermissions(userProfileId, cacheKey) {
3407
4059
  const permissions = await this.permissionsRepo.getEffectivePermissions(
3408
4060
  userProfileId,
3409
4061
  this.tenantId
@@ -3932,8 +4584,11 @@ var RelationService = class {
3932
4584
  async validateRelationsOrThrow(schema, data) {
3933
4585
  const result = await this.validateRelations(schema, data);
3934
4586
  if (!result.valid) {
3935
- const messages = result.errors.map((e) => `${e.attribute}: ${e.message}`).join("; ");
3936
- throw new Error(`Relation validation failed: ${messages}`);
4587
+ const errors = result.errors.map((e) => ({
4588
+ path: [e.attribute],
4589
+ message: e.message
4590
+ }));
4591
+ throw new ValidationError("Relation validation failed", errors);
3937
4592
  }
3938
4593
  }
3939
4594
  // ============================================================================
@@ -3981,7 +4636,8 @@ var RelationService = class {
3981
4636
  totalCount += result.total;
3982
4637
  for (const record of result.records) {
3983
4638
  const template = target.displayTemplate || objectSchema.labelExpression;
3984
- const label = renderLabelExpression(template, record.values);
4639
+ const enrichedValues = enrichValuesWithSelectLabels(record.values, objectSchema.attributes);
4640
+ const label = renderLabelExpression(template, enrichedValues);
3985
4641
  allOptions.push({
3986
4642
  id: record.id,
3987
4643
  objectId: objectSchema.id,
@@ -4045,7 +4701,8 @@ var RelationService = class {
4045
4701
  }
4046
4702
  }
4047
4703
  for (const record of objectRecords) {
4048
- const label = renderLabelExpression(template, record.values);
4704
+ const enrichedValues = enrichValuesWithSelectLabels(record.values, objectSchema.attributes);
4705
+ const label = renderLabelExpression(template, enrichedValues);
4049
4706
  resolved.push({
4050
4707
  id: record.id,
4051
4708
  objectId: record.objectId,
@@ -4066,10 +4723,131 @@ var RelationService = class {
4066
4723
  if (!attribute || attribute.type !== "relation") {
4067
4724
  return null;
4068
4725
  }
4069
- return {
4726
+ const merged = {
4070
4727
  ...attribute.config,
4071
4728
  ...attribute
4072
4729
  };
4730
+ if (!("targets" in merged && Array.isArray(merged.targets) && "cardinality" in merged)) {
4731
+ return null;
4732
+ }
4733
+ return merged;
4734
+ }
4735
+ };
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
+ }
4073
4851
  }
4074
4852
  };
4075
4853
 
@@ -4080,9 +4858,10 @@ var RecordService = class {
4080
4858
  this.tenantId = tenantId;
4081
4859
  this.schemaService = new ObjectSchemaService(adapter, registry);
4082
4860
  this.relationService = new RelationService(adapter, registry);
4861
+ this.userService = new UserService(adapter, tenantId);
4083
4862
  this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
4084
4863
  this.permissionService = options?.permissionService;
4085
- this.auditService = options?.auditService;
4864
+ this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
4086
4865
  this.userId = options?.userId;
4087
4866
  this.userEmail = options?.userEmail;
4088
4867
  }
@@ -4149,21 +4928,23 @@ var RecordService = class {
4149
4928
  /**
4150
4929
  * Compute display label from schema expression
4151
4930
  * Automatically resolves relation attribute values to their labels
4931
+ * and select/multiselect values to their option labels
4152
4932
  * @internal
4153
4933
  */
4154
4934
  async computeLabel(schema, values) {
4155
4935
  const attrNames = extractAttributeNames(schema.labelExpression);
4936
+ let enrichedValues = enrichValuesWithSelectLabels(values, schema.attributes);
4156
4937
  const relationAttrs = schema.attributes.filter(
4157
4938
  (attr) => attr.type === "relation" && attrNames.includes(attr.name)
4158
4939
  );
4159
4940
  if (relationAttrs.length === 0) {
4160
- return renderLabelExpression(schema.labelExpression, values);
4941
+ return renderLabelExpression(schema.labelExpression, enrichedValues);
4161
4942
  }
4162
4943
  const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
4163
4944
  if (resolvedMap.size === 0) {
4164
- return renderLabelExpression(schema.labelExpression, values);
4945
+ return renderLabelExpression(schema.labelExpression, enrichedValues);
4165
4946
  }
4166
- const enrichedValues = { ...values };
4947
+ enrichedValues = { ...enrichedValues };
4167
4948
  for (const attr of relationAttrs) {
4168
4949
  const val = values[attr.name];
4169
4950
  const ids = this.extractRelationIds(val);
@@ -4218,6 +4999,9 @@ var RecordService = class {
4218
4999
  if (!options?.skipRelationValidation) {
4219
5000
  await this.relationService.validateRelationsOrThrow(schema, data);
4220
5001
  }
5002
+ if (!options?.skipUserValidation) {
5003
+ await this.userService.validateUsersOrThrow(schema, data);
5004
+ }
4221
5005
  }
4222
5006
  const completionStatus = computeRecordStatus(schema, data);
4223
5007
  const label = await this.computeLabel(schema, data);
@@ -4276,7 +5060,7 @@ var RecordService = class {
4276
5060
  async getRecordOrThrow(recordId) {
4277
5061
  const record = await this.getRecord(recordId);
4278
5062
  if (!record) {
4279
- throw new Error(`Record with id "${recordId}" not found`);
5063
+ throw new RecordNotFoundError(recordId);
4280
5064
  }
4281
5065
  return record;
4282
5066
  }
@@ -4329,6 +5113,9 @@ var RecordService = class {
4329
5113
  if (!options?.skipRelationValidation) {
4330
5114
  await this.relationService.validateRelationsOrThrow(schema, data);
4331
5115
  }
5116
+ if (!options?.skipUserValidation) {
5117
+ await this.userService.validateUsersOrThrow(schema, data);
5118
+ }
4332
5119
  }
4333
5120
  const completionStatus = computeRecordStatus(schema, mergedData);
4334
5121
  const label = await this.computeLabel(schema, mergedData);
@@ -4454,7 +5241,7 @@ var RecordService = class {
4454
5241
  await this.checkPermission(schema.name, "delete");
4455
5242
  if (options?.checkSystem) {
4456
5243
  if (schema.system) {
4457
- throw new Error(`Cannot delete record of system object "${schema.label}"`);
5244
+ throw new ProtectedResourceError("object", schema.name, "delete");
4458
5245
  }
4459
5246
  }
4460
5247
  if (!options?.skipReferenceCheck) {
@@ -4503,7 +5290,11 @@ var RecordService = class {
4503
5290
  async restoreRecord(recordId, options) {
4504
5291
  const record = await this.getRecordOrThrow(recordId);
4505
5292
  if (!record.deletedAt) {
4506
- throw new Error(`Record "${recordId}" is not deleted`);
5293
+ throw new SchemaError(
5294
+ `Record "${recordId}" is not deleted`,
5295
+ SchemaErrorCode.VALIDATION_FAILED,
5296
+ { recordId, reason: "not_deleted" }
5297
+ );
4507
5298
  }
4508
5299
  const schema = await this.schemaService.getObjectSchema(record.objectId);
4509
5300
  await this.checkPermission(schema.name, "update");
@@ -4941,11 +5732,17 @@ var ViewService = class {
4941
5732
  this.validateViewName(input.name);
4942
5733
  const existing = await this.adapter.views.findByName(tenantId, input.objectName, input.name);
4943
5734
  if (existing) {
4944
- throw new Error(`View "${input.name}" already exists for object "${input.objectName}"`);
5735
+ throw new SchemaError(
5736
+ `View "${input.name}" already exists for object "${input.objectName}"`,
5737
+ SchemaErrorCode.DUPLICATE_ATTRIBUTE,
5738
+ { viewName: input.name, objectName: input.objectName }
5739
+ );
4945
5740
  }
4946
5741
  if (this.nativeViews.has(input.objectName, input.name)) {
4947
- throw new Error(
4948
- `Cannot create view "${input.name}": a system view with this name already exists`
5742
+ throw new SchemaError(
5743
+ `Cannot create view "${input.name}": a system view with this name already exists`,
5744
+ SchemaErrorCode.DUPLICATE_ATTRIBUTE,
5745
+ { viewName: input.name, objectName: input.objectName, system: true }
4949
5746
  );
4950
5747
  }
4951
5748
  const dbView = await this.adapter.views.create({
@@ -4973,10 +5770,10 @@ var ViewService = class {
4973
5770
  async updateView(viewId, input) {
4974
5771
  const dbView = await this.adapter.views.findById(viewId);
4975
5772
  if (!dbView) {
4976
- throw new Error(`View with id "${viewId}" not found`);
5773
+ throw new NotFoundError("View", viewId);
4977
5774
  }
4978
5775
  if (dbView.system) {
4979
- throw new Error("Cannot modify system views. System views are protected.");
5776
+ throw new ProtectedResourceError("view", dbView.name, "modify");
4980
5777
  }
4981
5778
  const updated = await this.adapter.views.update(viewId, {
4982
5779
  label: input.label,
@@ -4996,10 +5793,10 @@ var ViewService = class {
4996
5793
  async deleteView(viewId) {
4997
5794
  const dbView = await this.adapter.views.findById(viewId);
4998
5795
  if (!dbView) {
4999
- throw new Error(`View with id "${viewId}" not found`);
5796
+ throw new NotFoundError("View", viewId);
5000
5797
  }
5001
5798
  if (dbView.system) {
5002
- throw new Error("Cannot delete system views. System views are protected.");
5799
+ throw new ProtectedResourceError("view", dbView.name, "delete");
5003
5800
  }
5004
5801
  await this.adapter.views.delete(viewId);
5005
5802
  }
@@ -5013,7 +5810,7 @@ var ViewService = class {
5013
5810
  async setDefaultView(viewId, tenantId) {
5014
5811
  const dbView = await this.adapter.views.findById(viewId);
5015
5812
  if (!dbView) {
5016
- throw new Error(`View with id "${viewId}" not found`);
5813
+ throw new NotFoundError("View", viewId);
5017
5814
  }
5018
5815
  const currentViews = await this.adapter.views.findByObjectName(tenantId, dbView.objectName);
5019
5816
  for (const v of currentViews) {
@@ -5032,16 +5829,23 @@ var ViewService = class {
5032
5829
  */
5033
5830
  validateViewName(name) {
5034
5831
  if (!name || name.length === 0) {
5035
- throw new Error("View name cannot be empty");
5832
+ throw new ValidationError("View name cannot be empty", [
5833
+ { path: ["name"], message: "View name cannot be empty" }
5834
+ ]);
5036
5835
  }
5037
5836
  if (name.length > 63) {
5038
- throw new Error("View name is too long (max 63 characters)");
5837
+ throw new ValidationError("View name is too long", [
5838
+ { path: ["name"], message: "View name is too long (max 63 characters)" }
5839
+ ]);
5039
5840
  }
5040
5841
  const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
5041
5842
  if (!kebabCaseRegex.test(name)) {
5042
- throw new Error(
5043
- "Invalid view name format. Name must be in kebab-case (e.g., 'detail', 'list-view')"
5044
- );
5843
+ throw new ValidationError("Invalid view name format", [
5844
+ {
5845
+ path: ["name"],
5846
+ message: "View name must be in kebab-case (e.g., 'detail', 'list-view')"
5847
+ }
5848
+ ]);
5045
5849
  }
5046
5850
  }
5047
5851
  /**
@@ -5418,9 +6222,11 @@ var NoopGeocodingAdapter = class {
5418
6222
  RecordService,
5419
6223
  RelationService,
5420
6224
  UserProfileService,
6225
+ UserService,
5421
6226
  ViewService,
5422
6227
  buildAuditChanges,
5423
6228
  createMockAdapter,
6229
+ enrichValuesWithSelectLabels,
5424
6230
  extractAttributeNames,
5425
6231
  getSyncPreview,
5426
6232
  getViewSyncPreview,