@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/index.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 */
@@ -129,6 +133,7 @@ __export(index_exports, {
129
133
  DEFAULT_ROLE_LABELS: () => DEFAULT_ROLE_LABELS,
130
134
  DEFAULT_ROLE_PERMISSIONS: () => DEFAULT_ROLE_PERMISSIONS,
131
135
  DuplicateError: () => DuplicateError,
136
+ EMPTY_VALUE_PLACEHOLDER: () => EMPTY_VALUE_PLACEHOLDER,
132
137
  FileNotFoundError: () => FileNotFoundError,
133
138
  FileService: () => FileService,
134
139
  FlowBuilder: () => FlowBuilder,
@@ -166,6 +171,7 @@ __export(index_exports, {
166
171
  TableTabConfig: () => TableTabConfig,
167
172
  UserProfileNotFoundError: () => UserProfileNotFoundError,
168
173
  UserProfileService: () => UserProfileService,
174
+ UserService: () => UserService,
169
175
  ValidationError: () => ValidationError,
170
176
  ViewBuilder: () => ViewBuilder,
171
177
  ViewService: () => ViewService,
@@ -199,10 +205,12 @@ __export(index_exports, {
199
205
  currencyConfigSchema: () => currencyConfigSchema,
200
206
  date: () => date,
201
207
  dateConfigSchema: () => dateConfigSchema,
208
+ enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
202
209
  extractAttributeNames: () => extractAttributeNames,
203
210
  file: () => file,
204
211
  fileConfigSchema: () => fileConfigSchema,
205
212
  flow: () => flow,
213
+ formatAttributeValue: () => formatAttributeValue,
206
214
  generateId: () => generateId,
207
215
  generatePrefixedId: () => generatePrefixedId,
208
216
  getAttributeConfigSchema: () => getAttributeConfigSchema,
@@ -210,6 +218,7 @@ __export(index_exports, {
210
218
  getSyncPreview: () => getSyncPreview,
211
219
  getViewSyncPreview: () => getViewSyncPreview,
212
220
  group: () => group,
221
+ isActivityTab: () => isActivityTab,
213
222
  isAdvancedFilterState: () => isAdvancedFilterState,
214
223
  isCustomTab: () => isCustomTab,
215
224
  isDefaultRole: () => isDefaultRole,
@@ -434,6 +443,9 @@ function isTableTab(tab) {
434
443
  function isCustomTab(tab) {
435
444
  return tab.type === "custom";
436
445
  }
446
+ function isActivityTab(tab) {
447
+ return tab.type === "activity";
448
+ }
437
449
 
438
450
  // src/utils.ts
439
451
  function generateId() {
@@ -450,6 +462,175 @@ function generatePrefixedId(prefix) {
450
462
  return `${prefix}_${generateId()}`;
451
463
  }
452
464
 
465
+ // src/format.ts
466
+ var import_constants = require("@stndrds/constants");
467
+ var EMPTY_VALUE_PLACEHOLDER = "\u2014";
468
+ function formatText(value) {
469
+ return String(value);
470
+ }
471
+ function formatCheckbox(value) {
472
+ return value ? "Yes" : "No";
473
+ }
474
+ function formatNumber(value, attribute) {
475
+ if (typeof value !== "number") return String(value);
476
+ const decimals = attribute.decimals;
477
+ if (attribute.unit === "percentage") {
478
+ return value.toLocaleString(void 0, {
479
+ style: "percent",
480
+ minimumFractionDigits: decimals,
481
+ maximumFractionDigits: decimals
482
+ });
483
+ }
484
+ return value.toLocaleString(void 0, {
485
+ minimumFractionDigits: decimals,
486
+ maximumFractionDigits: decimals
487
+ });
488
+ }
489
+ function formatCurrency(value, _attribute) {
490
+ if (typeof value !== "object" || value === null) return String(value);
491
+ const currency2 = value;
492
+ if (!("value" in currency2 && "code" in currency2)) return String(value);
493
+ const formattedValue = currency2.value.toLocaleString(void 0, {
494
+ minimumFractionDigits: 2,
495
+ maximumFractionDigits: 2
496
+ });
497
+ return `${formattedValue} ${currency2.code}`;
498
+ }
499
+ function formatDate(value) {
500
+ if (value instanceof Date) {
501
+ return value.toISOString().split("T")[0];
502
+ }
503
+ if (typeof value === "string") {
504
+ const date2 = new Date(value);
505
+ if (!Number.isNaN(date2.getTime())) {
506
+ return date2.toISOString().split("T")[0];
507
+ }
508
+ }
509
+ return String(value);
510
+ }
511
+ function formatTimestamp(value) {
512
+ if (typeof value === "number") {
513
+ return new Date(value).toISOString();
514
+ }
515
+ if (value instanceof Date) {
516
+ return value.toISOString();
517
+ }
518
+ return String(value);
519
+ }
520
+ function formatPhone(value) {
521
+ if (typeof value !== "object" || value === null) return String(value);
522
+ const phone2 = value;
523
+ if (!("phoneNumber" in phone2)) return String(value);
524
+ if (phone2.countryCode) {
525
+ const country = (0, import_constants.getCountryByIso3)(phone2.countryCode);
526
+ const dial = country?.phoneCode ?? "";
527
+ return `${dial} ${phone2.phoneNumber}`.trim();
528
+ }
529
+ return phone2.phoneNumber;
530
+ }
531
+ function formatLocation(value, attribute) {
532
+ if (typeof value !== "object" || value === null) return String(value);
533
+ const loc = value;
534
+ const granularity = attribute.granularity ?? "full";
535
+ const parts = [];
536
+ switch (granularity) {
537
+ case "country":
538
+ if (loc.country) parts.push(loc.country);
539
+ break;
540
+ case "state":
541
+ if (loc.state) parts.push(loc.state);
542
+ if (loc.country) parts.push(loc.country);
543
+ break;
544
+ case "city":
545
+ if (loc.city) parts.push(loc.city);
546
+ if (loc.state) parts.push(loc.state);
547
+ if (loc.country) parts.push(loc.country);
548
+ break;
549
+ case "coordinates":
550
+ if (loc.latitude !== void 0 && loc.longitude !== void 0) {
551
+ parts.push(`${loc.latitude}, ${loc.longitude}`);
552
+ }
553
+ break;
554
+ case "address":
555
+ if (loc.address) parts.push(loc.address);
556
+ if (loc.city) parts.push(loc.city);
557
+ if (loc.state) parts.push(loc.state);
558
+ if (loc.country) parts.push(loc.country);
559
+ break;
560
+ default:
561
+ if (loc.address) parts.push(loc.address);
562
+ if (loc.city) parts.push(loc.city);
563
+ if (loc.state) parts.push(loc.state);
564
+ if (loc.postalCode) parts.push(loc.postalCode);
565
+ if (loc.country) parts.push(loc.country);
566
+ break;
567
+ }
568
+ return parts.join(", ") || EMPTY_VALUE_PLACEHOLDER;
569
+ }
570
+ function formatSelect(value, attribute) {
571
+ if (typeof value !== "string") return String(value);
572
+ const option = attribute.options?.find((o) => o.value === value);
573
+ return option?.label ?? String(value);
574
+ }
575
+ function formatMultiselect(value, attribute) {
576
+ if (!Array.isArray(value)) return String(value);
577
+ if (attribute.options) {
578
+ const labels = value.map((v) => attribute.options.find((o) => o.value === v)?.label).filter(Boolean);
579
+ return labels.join(", ");
580
+ }
581
+ return value.join(", ");
582
+ }
583
+ function formatRating(value, attribute) {
584
+ if (typeof value !== "number") return String(value);
585
+ const max = attribute.max ?? 5;
586
+ return `${value}/${max}`;
587
+ }
588
+ function formatAttributeValue(value, attribute) {
589
+ if (value === null || value === void 0 || value === "") {
590
+ return EMPTY_VALUE_PLACEHOLDER;
591
+ }
592
+ switch (attribute.type) {
593
+ case "text":
594
+ case "textarea":
595
+ return formatText(value);
596
+ case "checkbox":
597
+ return formatCheckbox(value);
598
+ case "number":
599
+ return formatNumber(value, attribute);
600
+ case "currency":
601
+ return formatCurrency(value, attribute);
602
+ case "date":
603
+ return formatDate(value);
604
+ case "timestamp":
605
+ return formatTimestamp(value);
606
+ case "phone":
607
+ return formatPhone(value);
608
+ case "location":
609
+ return formatLocation(value, attribute);
610
+ case "select":
611
+ case "status":
612
+ return formatSelect(value, attribute);
613
+ case "multiselect":
614
+ return formatMultiselect(value, attribute);
615
+ case "rating":
616
+ return formatRating(value, attribute);
617
+ // Unsupported types - return value as-is or placeholder
618
+ case "file":
619
+ case "user":
620
+ case "relation":
621
+ if (Array.isArray(value)) {
622
+ return value.join(", ");
623
+ }
624
+ return String(value);
625
+ default: {
626
+ if (Array.isArray(value)) {
627
+ return value.join(", ");
628
+ }
629
+ return String(value);
630
+ }
631
+ }
632
+ }
633
+
453
634
  // src/constants/index.ts
454
635
  init_default_roles();
455
636
 
@@ -471,6 +652,7 @@ var SchemaErrorCode = {
471
652
  // Protected Resources
472
653
  PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
473
654
  PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
655
+ PROTECTED_VIEW: "SCHEMA_PROTECTED_VIEW",
474
656
  PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
475
657
  // Permissions
476
658
  FORBIDDEN: "SCHEMA_FORBIDDEN",
@@ -559,7 +741,7 @@ var ValidationError = class _ValidationError extends SchemaError {
559
741
  };
560
742
  var ProtectedResourceError = class extends SchemaError {
561
743
  constructor(resourceType, resourceName, operation) {
562
- const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : SchemaErrorCode.PROTECTED_ATTRIBUTE;
744
+ const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : resourceType === "view" ? SchemaErrorCode.PROTECTED_VIEW : SchemaErrorCode.PROTECTED_ATTRIBUTE;
563
745
  super(`Cannot ${operation} system ${resourceType} "${resourceName}"`, code, {
564
746
  resourceType,
565
747
  resourceName,
@@ -653,8 +835,8 @@ function isForbiddenError(error) {
653
835
  }
654
836
 
655
837
  // src/builders/attribute-validators.ts
656
- var import_constants = require("@stndrds/constants");
657
- var VALID_ICONS = new Set(import_constants.ICONS);
838
+ var import_constants2 = require("@stndrds/constants");
839
+ var VALID_ICONS = new Set(import_constants2.ICONS);
658
840
  var ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
659
841
  var MAX_NAME_LENGTH = 63;
660
842
  var MAX_LABEL_LENGTH = 128;
@@ -1155,6 +1337,13 @@ var FileAttributeBuilder = class extends BaseAttributeBuilder {
1155
1337
  this.attr.verification = config;
1156
1338
  return this;
1157
1339
  }
1340
+ /**
1341
+ * Allow multiple files to be uploaded
1342
+ */
1343
+ multiple() {
1344
+ this.attr.multiple = true;
1345
+ return this;
1346
+ }
1158
1347
  required() {
1159
1348
  this.setRequired(true);
1160
1349
  return this;
@@ -1175,6 +1364,13 @@ var UserAttributeBuilder = class extends BaseAttributeBuilder {
1175
1364
  this.attr.allowedRoles = roles;
1176
1365
  return this;
1177
1366
  }
1367
+ /**
1368
+ * Allow multiple users to be selected
1369
+ */
1370
+ multiple() {
1371
+ this.attr.multiple = true;
1372
+ return this;
1373
+ }
1178
1374
  required() {
1179
1375
  this.setRequired(true);
1180
1376
  return this;
@@ -2491,6 +2687,13 @@ var optionSchema = import_zod4.z.object({
2491
2687
  description: import_zod4.z.string().optional(),
2492
2688
  group: import_zod4.z.enum(["idle", "in_progress", "finished"]).optional()
2493
2689
  });
2690
+ var optionsArraySchema = import_zod4.z.array(optionSchema).min(1).refine(
2691
+ (options) => {
2692
+ const values = options.map((o) => o.value);
2693
+ return new Set(values).size === values.length;
2694
+ },
2695
+ { message: "Duplicate option values are not allowed" }
2696
+ );
2494
2697
  var relationTargetSchema = import_zod4.z.object({
2495
2698
  object: import_zod4.z.string().min(1),
2496
2699
  displayTemplate: import_zod4.z.string().optional(),
@@ -2551,7 +2754,7 @@ var currencyConfigSchema = baseConfigSchema.extend({
2551
2754
  allowedCurrencies: import_zod4.z.array(import_zod4.z.string().length(3)).optional()
2552
2755
  });
2553
2756
  var statusConfigSchema = baseConfigSchema.extend({
2554
- options: import_zod4.z.array(optionSchema).min(1)
2757
+ options: optionsArraySchema
2555
2758
  });
2556
2759
  var locationConfigSchema = baseConfigSchema.extend({
2557
2760
  granularity: import_zod4.z.enum(["full", "address", "city", "state", "country", "coordinates"]),
@@ -2565,19 +2768,21 @@ var timestampConfigSchema = baseConfigSchema.extend({
2565
2768
  autoUpdate: import_zod4.z.boolean().optional()
2566
2769
  });
2567
2770
  var selectConfigSchema = baseConfigSchema.extend({
2568
- options: import_zod4.z.array(optionSchema).min(1)
2771
+ options: optionsArraySchema
2569
2772
  });
2570
2773
  var multiselectConfigSchema = baseConfigSchema.extend({
2571
- options: import_zod4.z.array(optionSchema).min(1)
2774
+ options: optionsArraySchema
2572
2775
  });
2573
2776
  var fileConfigSchema = baseConfigSchema.extend({
2574
2777
  maxFiles: import_zod4.z.number().int().min(1).optional(),
2575
2778
  maxSize: import_zod4.z.number().int().min(1).optional(),
2576
2779
  allowedTypes: import_zod4.z.array(import_zod4.z.string()).optional(),
2577
- verification: fileVerificationConfigSchema.optional()
2780
+ verification: fileVerificationConfigSchema.optional(),
2781
+ multiple: import_zod4.z.boolean().optional()
2578
2782
  });
2579
2783
  var userConfigSchema = baseConfigSchema.extend({
2580
- allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional()
2784
+ allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional(),
2785
+ multiple: import_zod4.z.boolean().optional()
2581
2786
  });
2582
2787
  var relationConfigSchema = baseConfigSchema.extend({
2583
2788
  targets: import_zod4.z.array(relationTargetSchema).min(1),
@@ -2715,20 +2920,39 @@ function createLocationValidator(_attr) {
2715
2920
  function createTimestampValidator(_attr) {
2716
2921
  return import_zod4.z.number().int().positive();
2717
2922
  }
2718
- function createFileValidator(_attr) {
2719
- return import_zod4.z.string().uuid();
2923
+ function createFileValidator(attr) {
2924
+ const uuidSchema = import_zod4.z.uuid({
2925
+ message: `${attr.label} must be a valid file ID`
2926
+ });
2927
+ if (attr.multiple) {
2928
+ let arraySchema = import_zod4.z.array(uuidSchema);
2929
+ if (attr.maxFiles) {
2930
+ arraySchema = arraySchema.max(
2931
+ attr.maxFiles,
2932
+ `${attr.label} cannot have more than ${attr.maxFiles} file${attr.maxFiles > 1 ? "s" : ""}`
2933
+ );
2934
+ }
2935
+ return arraySchema;
2936
+ }
2937
+ return uuidSchema;
2720
2938
  }
2721
- function createUserValidator(_attr) {
2722
- return import_zod4.z.string().uuid();
2939
+ function createUserValidator(attr) {
2940
+ const uuidSchema = import_zod4.z.uuid({
2941
+ message: `${attr.label} must be a valid user ID`
2942
+ });
2943
+ if (attr.multiple) {
2944
+ return import_zod4.z.array(uuidSchema);
2945
+ }
2946
+ return uuidSchema;
2723
2947
  }
2724
2948
  function createSingleRelationValidator(attr) {
2725
- const uuidSchema = import_zod4.z.string().uuid({
2949
+ const uuidSchema = import_zod4.z.uuid({
2726
2950
  message: `${attr.label} must be a valid record ID`
2727
2951
  });
2728
2952
  return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
2729
2953
  }
2730
2954
  function createMultiRelationValidator(attr) {
2731
- const uuidSchema = import_zod4.z.string().uuid({
2955
+ const uuidSchema = import_zod4.z.uuid({
2732
2956
  message: `Each ${attr.label} item must be a valid record ID`
2733
2957
  });
2734
2958
  let arraySchema = import_zod4.z.array(uuidSchema);
@@ -2973,6 +3197,24 @@ function extractAttributeNames(template) {
2973
3197
  }
2974
3198
  return names;
2975
3199
  }
3200
+ function hasOptions(attr) {
3201
+ return "options" in attr && Array.isArray(attr.options) && attr.options.length > 0;
3202
+ }
3203
+ function enrichValuesWithSelectLabels(values, attributes) {
3204
+ const enriched = { ...values };
3205
+ for (const attr of attributes) {
3206
+ const value = values[attr.name];
3207
+ if (value == null) continue;
3208
+ const isSelectLike = attr.type === "select" || attr.type === "status" || attr.type === "multiselect";
3209
+ if (!(isSelectLike && hasOptions(attr))) continue;
3210
+ if (attr.type === "multiselect" && Array.isArray(value) && value.length === 0) continue;
3211
+ const formatted = formatAttributeValue(value, attr);
3212
+ if (formatted && formatted !== EMPTY_VALUE_PLACEHOLDER) {
3213
+ enriched[attr.name] = formatted;
3214
+ }
3215
+ }
3216
+ return enriched;
3217
+ }
2976
3218
 
2977
3219
  // src/runtime/mock-adapter.ts
2978
3220
  function createMockObjectsRepository(stores) {
@@ -3449,14 +3691,31 @@ function createMockObjectRecordsRepository(stores) {
3449
3691
  (options.offset ?? 0) + options.limit
3450
3692
  );
3451
3693
  }
3694
+ const attributesByObjectId = /* @__PURE__ */ new Map();
3695
+ for (const attr of stores.attributes.values()) {
3696
+ if (!attributesByObjectId.has(attr.objectId)) {
3697
+ attributesByObjectId.set(attr.objectId, []);
3698
+ }
3699
+ attributesByObjectId.get(attr.objectId)?.push(attr);
3700
+ }
3452
3701
  const results = matchingRecords.map((r) => {
3453
3702
  const obj = objectsMap.get(r.objectId);
3454
3703
  const labelExpression = obj?.labelExpression ?? "{{ name }}";
3704
+ const dbAttrs = attributesByObjectId.get(r.objectId) ?? [];
3705
+ const attrs = dbAttrs.map((a) => ({
3706
+ ...a.config,
3707
+ id: a.id,
3708
+ name: a.name,
3709
+ type: a.type,
3710
+ label: a.config.label ?? a.name,
3711
+ required: a.config.required ?? false
3712
+ }));
3713
+ const enrichedValues = enrichValuesWithSelectLabels(r.values, attrs);
3455
3714
  return {
3456
3715
  objectId: r.objectId,
3457
3716
  objectName: obj?.name ?? "unknown",
3458
3717
  objectLabel: obj?.label ?? "Unknown",
3459
- label: renderLabelExpression(labelExpression, r.values),
3718
+ label: renderLabelExpression(labelExpression, enrichedValues),
3460
3719
  recordId: r.id,
3461
3720
  values: r.values,
3462
3721
  completionStatus: r.completionStatus,
@@ -3873,6 +4132,10 @@ var AuditService = class {
3873
4132
  this.options = options;
3874
4133
  this.buffer = [];
3875
4134
  this.flushTimer = null;
4135
+ /** Prevents concurrent flush operations */
4136
+ this.isFlushing = false;
4137
+ /** Pending flush promise to allow waiting on concurrent flush */
4138
+ this.flushPromise = null;
3876
4139
  if (options?.async && options.flushIntervalMs) {
3877
4140
  this.startFlushTimer();
3878
4141
  }
@@ -3959,6 +4222,24 @@ var AuditService = class {
3959
4222
  };
3960
4223
  await this.log(entry);
3961
4224
  }
4225
+ /**
4226
+ * Log a file action (upload, update, delete)
4227
+ */
4228
+ async logFileAction(params) {
4229
+ const entry = {
4230
+ tenantId: this.tenantId,
4231
+ actorId: params.actorId,
4232
+ actorEmail: params.actorEmail,
4233
+ actorType: "user",
4234
+ action: params.action,
4235
+ resourceType: "file",
4236
+ resourceId: params.fileId,
4237
+ resourceLabel: params.fileName,
4238
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
4239
+ metadata: params.metadata
4240
+ };
4241
+ await this.log(entry);
4242
+ }
3962
4243
  /**
3963
4244
  * Log a system action (no actor)
3964
4245
  */
@@ -4025,14 +4306,23 @@ var AuditService = class {
4025
4306
  // ============================================================================
4026
4307
  /**
4027
4308
  * Flush buffered logs to the database
4309
+ * Protected against concurrent flush calls
4028
4310
  */
4029
4311
  async flush() {
4312
+ if (this.isFlushing && this.flushPromise) {
4313
+ return this.flushPromise;
4314
+ }
4030
4315
  if (this.buffer.length === 0 || !this.adapter.audit) {
4031
4316
  return;
4032
4317
  }
4318
+ this.isFlushing = true;
4033
4319
  const entries = [...this.buffer];
4034
4320
  this.buffer = [];
4035
- await this.adapter.audit.createMany(entries);
4321
+ this.flushPromise = this.adapter.audit.createMany(entries).finally(() => {
4322
+ this.isFlushing = false;
4323
+ this.flushPromise = null;
4324
+ });
4325
+ return this.flushPromise;
4036
4326
  }
4037
4327
  /**
4038
4328
  * Clean up resources (stop timer, flush remaining logs)
@@ -4054,6 +4344,12 @@ var AuditService = class {
4054
4344
  if (!this.adapter.audit) {
4055
4345
  return;
4056
4346
  }
4347
+ if (entry.actorId && !entry.actorEmail && entry.actorType === "user") {
4348
+ const profile = await this.adapter.userProfiles.findById(entry.actorId);
4349
+ if (profile) {
4350
+ entry.actorEmail = profile.email;
4351
+ }
4352
+ }
4057
4353
  if (this.options?.async) {
4058
4354
  this.buffer.push(entry);
4059
4355
  const batchSize = this.options.batchSize ?? 10;
@@ -4093,21 +4389,105 @@ var AuditService = class {
4093
4389
 
4094
4390
  // src/runtime/services/file.service.ts
4095
4391
  var FileService = class {
4096
- constructor(adapter, tenantId) {
4392
+ constructor(adapter, tenantId, options) {
4097
4393
  this.adapter = adapter;
4098
4394
  this.tenantId = tenantId;
4395
+ this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
4396
+ this.userId = options?.userId;
4397
+ this.userEmail = options?.userEmail;
4099
4398
  }
4399
+ // ============================================================================
4400
+ // UPLOAD (requires StorageAdapter)
4401
+ // ============================================================================
4100
4402
  /**
4101
- * Create a new file record (after upload to storage)
4403
+ * Upload a file to storage and create metadata record.
4102
4404
  *
4103
- * @param data - File metadata
4405
+ * This method orchestrates:
4406
+ * 1. Upload to storage (via StorageAdapter)
4407
+ * 2. Create file metadata in database
4408
+ * 3. Audit log the operation
4409
+ *
4410
+ * Requires `adapter.storage` to be configured.
4411
+ *
4412
+ * @param input - File content and metadata
4104
4413
  * @returns Created file record
4414
+ * @throws Error if StorageAdapter is not configured
4105
4415
  *
4106
4416
  * @example
4107
4417
  * ```typescript
4108
- * const service = new FileService(adapter, "tenant-123");
4418
+ * const file = await service.uploadFile({
4419
+ * content: fileBuffer,
4420
+ * fileName: "document.pdf",
4421
+ * mimeType: "application/pdf",
4422
+ * size: 12345,
4423
+ * uploadedBy: "user-123",
4424
+ * visibility: "private",
4425
+ * folderPath: "/documents",
4426
+ * tags: ["contract", "2025"],
4427
+ * });
4428
+ * ```
4429
+ */
4430
+ async uploadFile(input) {
4431
+ if (!this.adapter.storage) {
4432
+ throw new Error(
4433
+ "StorageAdapter is not configured. Provide adapter.storage to use uploadFile()."
4434
+ );
4435
+ }
4436
+ const uploadResult = await this.adapter.storage.upload({
4437
+ content: input.content,
4438
+ fileName: input.fileName,
4439
+ mimeType: input.mimeType,
4440
+ size: input.size,
4441
+ tenantId: this.tenantId,
4442
+ folderPath: input.folderPath
4443
+ });
4444
+ const file2 = await this.adapter.files.create({
4445
+ tenantId: this.tenantId,
4446
+ name: input.fileName,
4447
+ originalName: input.fileName,
4448
+ mimeType: input.mimeType,
4449
+ size: input.size,
4450
+ storageProvider: uploadResult.storageProvider,
4451
+ storagePath: uploadResult.storagePath,
4452
+ storageBucket: uploadResult.storageBucket,
4453
+ url: uploadResult.url,
4454
+ uploadedBy: input.uploadedBy,
4455
+ folderPath: input.folderPath,
4456
+ tags: input.tags,
4457
+ visibility: input.visibility ?? "private",
4458
+ allowedUsers: input.allowedUsers
4459
+ });
4460
+ if (this.auditService && this.userId) {
4461
+ await this.auditService.logFileAction({
4462
+ action: "file.uploaded",
4463
+ actorId: this.userId,
4464
+ actorEmail: this.userEmail,
4465
+ fileId: file2.id,
4466
+ fileName: file2.name,
4467
+ metadata: {
4468
+ mimeType: file2.mimeType,
4469
+ size: file2.size,
4470
+ visibility: file2.visibility
4471
+ }
4472
+ });
4473
+ }
4474
+ return file2;
4475
+ }
4476
+ // ============================================================================
4477
+ // CREATE (metadata only, for external storage)
4478
+ // ============================================================================
4479
+ /**
4480
+ * Create a new file record (after upload to storage).
4109
4481
  *
4110
- * // After uploading to S3
4482
+ * Use this method when handling storage externally (e.g., with Multer + S3).
4483
+ * For integrated upload, use `uploadFile()` instead.
4484
+ *
4485
+ * @param data - File metadata
4486
+ * @returns Created file record
4487
+ *
4488
+ * @example
4489
+ * ```typescript
4490
+ * // After uploading to S3 with Multer
4111
4491
  * const file = await service.createFile({
4112
4492
  * tenantId: "tenant-123",
4113
4493
  * name: "contract-2025.pdf",
@@ -4129,8 +4509,26 @@ var FileService = class {
4129
4509
  `Tenant mismatch: service initialized with "${this.tenantId}" but data has "${data.tenantId}"`
4130
4510
  );
4131
4511
  }
4132
- return await this.adapter.files.create(data);
4512
+ const file2 = await this.adapter.files.create(data);
4513
+ if (this.auditService && this.userId) {
4514
+ await this.auditService.logFileAction({
4515
+ action: "file.uploaded",
4516
+ actorId: this.userId,
4517
+ actorEmail: this.userEmail,
4518
+ fileId: file2.id,
4519
+ fileName: file2.name,
4520
+ metadata: {
4521
+ mimeType: file2.mimeType,
4522
+ size: file2.size,
4523
+ visibility: file2.visibility
4524
+ }
4525
+ });
4526
+ }
4527
+ return file2;
4133
4528
  }
4529
+ // ============================================================================
4530
+ // READ
4531
+ // ============================================================================
4134
4532
  /**
4135
4533
  * Get file by ID
4136
4534
  */
@@ -4151,6 +4549,9 @@ var FileService = class {
4151
4549
  }
4152
4550
  return file2;
4153
4551
  }
4552
+ // ============================================================================
4553
+ // UPDATE
4554
+ // ============================================================================
4154
4555
  /**
4155
4556
  * Update file metadata
4156
4557
  *
@@ -4159,18 +4560,44 @@ var FileService = class {
4159
4560
  * @returns Updated file
4160
4561
  */
4161
4562
  async updateFile(fileId, data) {
4162
- await this.getFileOrThrow(fileId);
4163
- return await this.adapter.files.update(fileId, data);
4563
+ const existingFile = await this.getFileOrThrow(fileId);
4564
+ const updatedFile = await this.adapter.files.update(fileId, data);
4565
+ if (this.auditService && this.userId) {
4566
+ const changes = [];
4567
+ for (const key of Object.keys(data)) {
4568
+ if (data[key] !== void 0 && data[key] !== existingFile[key]) {
4569
+ changes.push({
4570
+ field: key,
4571
+ oldValue: existingFile[key],
4572
+ newValue: data[key]
4573
+ });
4574
+ }
4575
+ }
4576
+ if (changes.length > 0) {
4577
+ await this.auditService.logFileAction({
4578
+ action: "file.updated",
4579
+ actorId: this.userId,
4580
+ actorEmail: this.userEmail,
4581
+ fileId,
4582
+ fileName: updatedFile.name,
4583
+ changes
4584
+ });
4585
+ }
4586
+ }
4587
+ return updatedFile;
4164
4588
  }
4589
+ // ============================================================================
4590
+ // DELETE
4591
+ // ============================================================================
4165
4592
  /**
4166
- * Delete file (soft delete)
4593
+ * Delete file (soft delete by default)
4167
4594
  *
4168
4595
  * @param fileId - File UUID
4169
4596
  * @param options - Delete options
4170
4597
  */
4171
4598
  async deleteFile(fileId, options) {
4599
+ const file2 = await this.getFileOrThrow(fileId);
4172
4600
  if (options?.checkOwnership && options.userId) {
4173
- const file2 = await this.getFileOrThrow(fileId);
4174
4601
  if (file2.uploadedBy !== options.userId) {
4175
4602
  throw new Error("You can only delete files you uploaded");
4176
4603
  }
@@ -4180,7 +4607,84 @@ var FileService = class {
4180
4607
  } else {
4181
4608
  await this.adapter.files.delete(fileId);
4182
4609
  }
4610
+ if (this.auditService && this.userId) {
4611
+ await this.auditService.logFileAction({
4612
+ action: "file.deleted",
4613
+ actorId: this.userId,
4614
+ actorEmail: this.userEmail,
4615
+ fileId,
4616
+ fileName: file2.name
4617
+ });
4618
+ }
4619
+ }
4620
+ /**
4621
+ * Delete file from both storage and database.
4622
+ *
4623
+ * Requires `adapter.storage` to be configured.
4624
+ *
4625
+ * @param fileId - File UUID
4626
+ * @param options - Delete options
4627
+ * @throws Error if StorageAdapter is not configured
4628
+ */
4629
+ async deleteFileWithStorage(fileId, options) {
4630
+ if (!this.adapter.storage) {
4631
+ throw new Error(
4632
+ "StorageAdapter is not configured. Provide adapter.storage to use deleteFileWithStorage()."
4633
+ );
4634
+ }
4635
+ const file2 = await this.getFileOrThrow(fileId);
4636
+ await this.adapter.storage.delete(file2.storagePath);
4637
+ if (options?.hard) {
4638
+ await this.adapter.files.hardDelete(fileId);
4639
+ } else {
4640
+ await this.adapter.files.delete(fileId);
4641
+ }
4642
+ if (this.auditService && this.userId) {
4643
+ await this.auditService.logFileAction({
4644
+ action: "file.deleted",
4645
+ actorId: this.userId,
4646
+ actorEmail: this.userEmail,
4647
+ fileId,
4648
+ fileName: file2.name,
4649
+ metadata: { deletedFromStorage: true }
4650
+ });
4651
+ }
4652
+ }
4653
+ /**
4654
+ * Delete multiple files
4655
+ *
4656
+ * @param fileIds - Array of file UUIDs
4657
+ * @param options - Delete options
4658
+ */
4659
+ async bulkDelete(fileIds, options) {
4660
+ for (const fileId of fileIds) {
4661
+ const file2 = await this.getFile(fileId);
4662
+ if (!file2) {
4663
+ continue;
4664
+ }
4665
+ if (options?.deleteFromStorage && this.adapter.storage) {
4666
+ await this.adapter.storage.delete(file2.storagePath);
4667
+ }
4668
+ if (options?.hard) {
4669
+ await this.adapter.files.hardDelete(fileId);
4670
+ } else {
4671
+ await this.adapter.files.delete(fileId);
4672
+ }
4673
+ if (this.auditService && this.userId) {
4674
+ await this.auditService.logFileAction({
4675
+ action: "file.deleted",
4676
+ actorId: this.userId,
4677
+ actorEmail: this.userEmail,
4678
+ fileId,
4679
+ fileName: file2.name,
4680
+ metadata: { deletedFromStorage: options?.deleteFromStorage ?? false }
4681
+ });
4682
+ }
4683
+ }
4183
4684
  }
4685
+ // ============================================================================
4686
+ // LIST
4687
+ // ============================================================================
4184
4688
  /**
4185
4689
  * List files for the tenant
4186
4690
  */
@@ -4199,6 +4703,74 @@ var FileService = class {
4199
4703
  async listFilesByUploader(uploadedBy) {
4200
4704
  return await this.adapter.files.findByUploader(uploadedBy);
4201
4705
  }
4706
+ // ============================================================================
4707
+ // SIGNED URL
4708
+ // ============================================================================
4709
+ /**
4710
+ * Get a signed URL for private file access.
4711
+ *
4712
+ * Checks access permissions before generating URL.
4713
+ * Requires `adapter.storage` to be configured.
4714
+ *
4715
+ * @param fileId - File UUID
4716
+ * @param userId - User requesting access
4717
+ * @param options - Signed URL options
4718
+ * @returns Signed URL
4719
+ * @throws Error if user doesn't have access or StorageAdapter is not configured
4720
+ *
4721
+ * @example
4722
+ * ```typescript
4723
+ * const url = await service.getSignedUrl("file-123", "user-456", {
4724
+ * expiresIn: 3600, // 1 hour
4725
+ * });
4726
+ * ```
4727
+ */
4728
+ async getSignedUrl(fileId, userId, options) {
4729
+ const file2 = await this.getFileOrThrow(fileId);
4730
+ const hasAccess = await this.checkAccess(fileId, userId);
4731
+ if (!hasAccess) {
4732
+ throw new Error("Access denied to this file");
4733
+ }
4734
+ if (file2.visibility === "public") {
4735
+ return file2.url;
4736
+ }
4737
+ if (!this.adapter.storage) {
4738
+ return file2.url;
4739
+ }
4740
+ return await this.adapter.storage.getSignedUrl(file2.storagePath, options);
4741
+ }
4742
+ // ============================================================================
4743
+ // ACCESS CONTROL
4744
+ // ============================================================================
4745
+ /**
4746
+ * Check if user has access to a file
4747
+ *
4748
+ * @param fileId - File UUID
4749
+ * @param userId - User ID to check
4750
+ * @returns true if user can access the file
4751
+ */
4752
+ async checkAccess(fileId, userId) {
4753
+ const file2 = await this.getFile(fileId);
4754
+ if (!file2) {
4755
+ return false;
4756
+ }
4757
+ if (file2.visibility === "public") {
4758
+ return true;
4759
+ }
4760
+ if (file2.uploadedBy === userId) {
4761
+ return true;
4762
+ }
4763
+ if (file2.visibility === "restricted") {
4764
+ return file2.allowedUsers?.includes(userId) ?? false;
4765
+ }
4766
+ return false;
4767
+ }
4768
+ /**
4769
+ * @deprecated Use checkAccess() instead
4770
+ */
4771
+ canAccess(fileId, userId) {
4772
+ return this.checkAccess(fileId, userId);
4773
+ }
4202
4774
  /**
4203
4775
  * Change file visibility
4204
4776
  *
@@ -4242,29 +4814,9 @@ var FileService = class {
4242
4814
  allowedUsers: newAllowed
4243
4815
  });
4244
4816
  }
4245
- /**
4246
- * Check if user has access to a file
4247
- *
4248
- * @param fileId - File UUID
4249
- * @param userId - User ID to check
4250
- * @returns true if user can access the file
4251
- */
4252
- async canAccess(fileId, userId) {
4253
- const file2 = await this.getFile(fileId);
4254
- if (!file2) {
4255
- return false;
4256
- }
4257
- if (file2.visibility === "public") {
4258
- return true;
4259
- }
4260
- if (file2.uploadedBy === userId) {
4261
- return true;
4262
- }
4263
- if (file2.visibility === "restricted") {
4264
- return file2.allowedUsers?.includes(userId) ?? false;
4265
- }
4266
- return false;
4267
- }
4817
+ // ============================================================================
4818
+ // ORGANIZATION
4819
+ // ============================================================================
4268
4820
  /**
4269
4821
  * Move file to different folder
4270
4822
  */
@@ -4351,16 +4903,26 @@ var FlowService = class {
4351
4903
  */
4352
4904
  async createFlow(input, tenantId) {
4353
4905
  if (!this.adapter.flows) {
4354
- throw new Error("Flows feature is not enabled. Database adapter does not support flows.");
4906
+ throw new SchemaError(
4907
+ "Flows feature is not enabled. Database adapter does not support flows.",
4908
+ SchemaErrorCode.UNKNOWN,
4909
+ { feature: "flows" }
4910
+ );
4355
4911
  }
4356
4912
  this.validateFlowName(input.name);
4357
4913
  const existing = await this.adapter.flows.findByName(tenantId, input.name);
4358
4914
  if (existing) {
4359
- throw new Error(`Flow "${input.name}" already exists`);
4915
+ throw new SchemaError(
4916
+ `Flow "${input.name}" already exists`,
4917
+ SchemaErrorCode.DUPLICATE_OBJECT,
4918
+ { flowName: input.name }
4919
+ );
4360
4920
  }
4361
4921
  if (this.systemFlows.has(input.name)) {
4362
- throw new Error(
4363
- `Cannot create flow "${input.name}": a system flow with this name already exists`
4922
+ throw new SchemaError(
4923
+ `Cannot create flow "${input.name}": a system flow with this name already exists`,
4924
+ SchemaErrorCode.DUPLICATE_OBJECT,
4925
+ { flowName: input.name, system: true }
4364
4926
  );
4365
4927
  }
4366
4928
  this.validateFlowStructure(input);
@@ -4385,14 +4947,16 @@ var FlowService = class {
4385
4947
  */
4386
4948
  async updateFlow(flowId, input) {
4387
4949
  if (!this.adapter.flows) {
4388
- throw new Error("Flows feature is not enabled.");
4950
+ throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
4951
+ feature: "flows"
4952
+ });
4389
4953
  }
4390
4954
  const dbFlow = await this.adapter.flows.findById(flowId);
4391
4955
  if (!dbFlow) {
4392
- throw new Error(`Flow with id "${flowId}" not found`);
4956
+ throw new NotFoundError("Flow", flowId);
4393
4957
  }
4394
4958
  if (dbFlow.system) {
4395
- throw new Error("Cannot modify system flows. System flows are protected.");
4959
+ throw new ProtectedResourceError("object", dbFlow.name, "modify");
4396
4960
  }
4397
4961
  if (input.slots || input.pages || input.relations) {
4398
4962
  this.validateFlowStructure({
@@ -4419,14 +4983,16 @@ var FlowService = class {
4419
4983
  */
4420
4984
  async publishFlow(flowId) {
4421
4985
  if (!this.adapter.flows) {
4422
- throw new Error("Flows feature is not enabled.");
4986
+ throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
4987
+ feature: "flows"
4988
+ });
4423
4989
  }
4424
4990
  const dbFlow = await this.adapter.flows.findById(flowId);
4425
4991
  if (!dbFlow) {
4426
- throw new Error(`Flow with id "${flowId}" not found`);
4992
+ throw new NotFoundError("Flow", flowId);
4427
4993
  }
4428
4994
  if (dbFlow.system) {
4429
- throw new Error("Cannot publish system flows. They are always published.");
4995
+ throw new ProtectedResourceError("object", dbFlow.name, "modify");
4430
4996
  }
4431
4997
  if (dbFlow.status === "published") {
4432
4998
  return this.convertDBFlowToDefinition(dbFlow);
@@ -4449,14 +5015,16 @@ var FlowService = class {
4449
5015
  */
4450
5016
  async archiveFlow(flowId) {
4451
5017
  if (!this.adapter.flows) {
4452
- throw new Error("Flows feature is not enabled.");
5018
+ throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
5019
+ feature: "flows"
5020
+ });
4453
5021
  }
4454
5022
  const dbFlow = await this.adapter.flows.findById(flowId);
4455
5023
  if (!dbFlow) {
4456
- throw new Error(`Flow with id "${flowId}" not found`);
5024
+ throw new NotFoundError("Flow", flowId);
4457
5025
  }
4458
5026
  if (dbFlow.system) {
4459
- throw new Error("Cannot archive system flows.");
5027
+ throw new ProtectedResourceError("object", dbFlow.name, "modify");
4460
5028
  }
4461
5029
  const updated = await this.adapter.flows.update(flowId, {
4462
5030
  status: "archived"
@@ -4468,14 +5036,16 @@ var FlowService = class {
4468
5036
  */
4469
5037
  async deleteFlow(flowId) {
4470
5038
  if (!this.adapter.flows) {
4471
- throw new Error("Flows feature is not enabled.");
5039
+ throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
5040
+ feature: "flows"
5041
+ });
4472
5042
  }
4473
5043
  const dbFlow = await this.adapter.flows.findById(flowId);
4474
5044
  if (!dbFlow) {
4475
- throw new Error(`Flow with id "${flowId}" not found`);
5045
+ throw new NotFoundError("Flow", flowId);
4476
5046
  }
4477
5047
  if (dbFlow.system) {
4478
- throw new Error("Cannot delete system flows. System flows are protected.");
5048
+ throw new ProtectedResourceError("object", dbFlow.name, "delete");
4479
5049
  }
4480
5050
  await this.adapter.flows.delete(flowId);
4481
5051
  }
@@ -4487,16 +5057,23 @@ var FlowService = class {
4487
5057
  */
4488
5058
  validateFlowName(name) {
4489
5059
  if (!name || name.length === 0) {
4490
- throw new Error("Flow name cannot be empty");
5060
+ throw new ValidationError("Flow name cannot be empty", [
5061
+ { path: ["name"], message: "Flow name cannot be empty" }
5062
+ ]);
4491
5063
  }
4492
5064
  if (name.length > 63) {
4493
- throw new Error("Flow name is too long (max 63 characters)");
5065
+ throw new ValidationError("Flow name is too long", [
5066
+ { path: ["name"], message: "Flow name is too long (max 63 characters)" }
5067
+ ]);
4494
5068
  }
4495
5069
  const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
4496
5070
  if (!kebabCaseRegex.test(name)) {
4497
- throw new Error(
4498
- "Invalid flow name format. Name must be in kebab-case (e.g., 'couple-creation', 'new-contact')"
4499
- );
5071
+ throw new ValidationError("Invalid flow name format", [
5072
+ {
5073
+ path: ["name"],
5074
+ message: "Flow name must be in kebab-case (e.g., 'couple-creation', 'new-contact')"
5075
+ }
5076
+ ]);
4500
5077
  }
4501
5078
  }
4502
5079
  /**
@@ -4504,33 +5081,59 @@ var FlowService = class {
4504
5081
  */
4505
5082
  validateFlowStructure(input) {
4506
5083
  if (!input.slots || input.slots.length === 0) {
4507
- throw new Error("Flow must have at least one slot");
5084
+ throw new ValidationError("Flow must have at least one slot", [
5085
+ { path: ["slots"], message: "Flow must have at least one slot" }
5086
+ ]);
4508
5087
  }
4509
5088
  if (!input.pages || input.pages.length === 0) {
4510
- throw new Error("Flow must have at least one page");
5089
+ throw new ValidationError("Flow must have at least one page", [
5090
+ { path: ["pages"], message: "Flow must have at least one page" }
5091
+ ]);
4511
5092
  }
4512
5093
  const slotIds = new Set(input.slots.map((s) => s.id));
4513
5094
  if (slotIds.size !== input.slots.length) {
4514
- throw new Error("Duplicate slot IDs detected");
5095
+ throw new ValidationError("Duplicate slot IDs detected", [
5096
+ { path: ["slots"], message: "Duplicate slot IDs detected" }
5097
+ ]);
4515
5098
  }
4516
5099
  for (const page of input.pages) {
4517
5100
  for (const row of page.rows) {
4518
5101
  for (const field of row.fields) {
4519
5102
  if (!slotIds.has(field.slotId)) {
4520
- throw new Error(`Field "${field.id}" references unknown slot "${field.slotId}"`);
5103
+ throw new ValidationError("Field references unknown slot", [
5104
+ {
5105
+ path: ["pages", page.id, "rows", row.id, "fields", field.id],
5106
+ message: `Field "${field.id}" references unknown slot "${field.slotId}"`
5107
+ }
5108
+ ]);
4521
5109
  }
4522
5110
  }
4523
5111
  }
4524
5112
  }
4525
5113
  for (const relation2 of input.relations) {
4526
5114
  if (!slotIds.has(relation2.sourceSlotId)) {
4527
- throw new Error(`Relation references unknown source slot "${relation2.sourceSlotId}"`);
5115
+ throw new ValidationError("Relation references unknown source slot", [
5116
+ {
5117
+ path: ["relations", relation2.id, "sourceSlotId"],
5118
+ message: `Relation references unknown source slot "${relation2.sourceSlotId}"`
5119
+ }
5120
+ ]);
4528
5121
  }
4529
5122
  if (!slotIds.has(relation2.targetSlotId)) {
4530
- throw new Error(`Relation references unknown target slot "${relation2.targetSlotId}"`);
5123
+ throw new ValidationError("Relation references unknown target slot", [
5124
+ {
5125
+ path: ["relations", relation2.id, "targetSlotId"],
5126
+ message: `Relation references unknown target slot "${relation2.targetSlotId}"`
5127
+ }
5128
+ ]);
4531
5129
  }
4532
5130
  if (relation2.sourceSlotId === relation2.targetSlotId) {
4533
- throw new Error(`Relation cannot link a slot to itself: "${relation2.sourceSlotId}"`);
5131
+ throw new ValidationError("Relation cannot link a slot to itself", [
5132
+ {
5133
+ path: ["relations", relation2.id],
5134
+ message: `Relation cannot link a slot to itself: "${relation2.sourceSlotId}"`
5135
+ }
5136
+ ]);
4534
5137
  }
4535
5138
  }
4536
5139
  }
@@ -5023,14 +5626,15 @@ var ObjectSchemaService = class {
5023
5626
  });
5024
5627
  }
5025
5628
  }
5629
+ const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
5630
+ const attributes = dbAttributes.map((attr) => this.convertDBAttributeToAttribute(attr));
5026
5631
  if (updates.labelExpression !== void 0 && updates.labelExpression !== oldValues.labelExpression) {
5027
5632
  const newExpression = updates.labelExpression;
5028
- await this.adapter.objectRecords.batchRefreshLabels(
5029
- objectId,
5030
- (values) => renderLabelExpression(newExpression, values)
5031
- );
5633
+ await this.adapter.objectRecords.batchRefreshLabels(objectId, (values) => {
5634
+ const enrichedValues = enrichValuesWithSelectLabels(values, attributes);
5635
+ return renderLabelExpression(newExpression, enrichedValues);
5636
+ });
5032
5637
  }
5033
- const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
5034
5638
  return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
5035
5639
  }
5036
5640
  /**
@@ -5349,6 +5953,8 @@ var PermissionService = class {
5349
5953
  this.adapter = adapter;
5350
5954
  this.tenantId = tenantId;
5351
5955
  this.cache = /* @__PURE__ */ new Map();
5956
+ /** Track pending permission fetches to prevent duplicate concurrent requests */
5957
+ this.pendingFetches = /* @__PURE__ */ new Map();
5352
5958
  if (!adapter.permissions) {
5353
5959
  throw new Error(
5354
5960
  "PermissionService requires a DatabaseAdapter with permissions repository. Make sure your adapter implements the permissions property."
@@ -5497,6 +6103,23 @@ var PermissionService = class {
5497
6103
  if (cached && cached.expiresAt > Date.now()) {
5498
6104
  return cached.permissions;
5499
6105
  }
6106
+ const pendingFetch = this.pendingFetches.get(cacheKey);
6107
+ if (pendingFetch) {
6108
+ return pendingFetch;
6109
+ }
6110
+ const fetchPromise = this.fetchAndCachePermissions(userProfileId, cacheKey);
6111
+ this.pendingFetches.set(cacheKey, fetchPromise);
6112
+ try {
6113
+ return await fetchPromise;
6114
+ } finally {
6115
+ this.pendingFetches.delete(cacheKey);
6116
+ }
6117
+ }
6118
+ /**
6119
+ * Fetch permissions from database and cache the result
6120
+ * @internal
6121
+ */
6122
+ async fetchAndCachePermissions(userProfileId, cacheKey) {
5500
6123
  const permissions = await this.permissionsRepo.getEffectivePermissions(
5501
6124
  userProfileId,
5502
6125
  this.tenantId
@@ -5879,8 +6502,11 @@ var RelationService = class {
5879
6502
  async validateRelationsOrThrow(schema, data) {
5880
6503
  const result = await this.validateRelations(schema, data);
5881
6504
  if (!result.valid) {
5882
- const messages = result.errors.map((e) => `${e.attribute}: ${e.message}`).join("; ");
5883
- throw new Error(`Relation validation failed: ${messages}`);
6505
+ const errors = result.errors.map((e) => ({
6506
+ path: [e.attribute],
6507
+ message: e.message
6508
+ }));
6509
+ throw new ValidationError("Relation validation failed", errors);
5884
6510
  }
5885
6511
  }
5886
6512
  // ============================================================================
@@ -5928,7 +6554,8 @@ var RelationService = class {
5928
6554
  totalCount += result.total;
5929
6555
  for (const record of result.records) {
5930
6556
  const template = target.displayTemplate || objectSchema.labelExpression;
5931
- const label = renderLabelExpression(template, record.values);
6557
+ const enrichedValues = enrichValuesWithSelectLabels(record.values, objectSchema.attributes);
6558
+ const label = renderLabelExpression(template, enrichedValues);
5932
6559
  allOptions.push({
5933
6560
  id: record.id,
5934
6561
  objectId: objectSchema.id,
@@ -5992,7 +6619,8 @@ var RelationService = class {
5992
6619
  }
5993
6620
  }
5994
6621
  for (const record of objectRecords) {
5995
- const label = renderLabelExpression(template, record.values);
6622
+ const enrichedValues = enrichValuesWithSelectLabels(record.values, objectSchema.attributes);
6623
+ const label = renderLabelExpression(template, enrichedValues);
5996
6624
  resolved.push({
5997
6625
  id: record.id,
5998
6626
  objectId: record.objectId,
@@ -6013,10 +6641,131 @@ var RelationService = class {
6013
6641
  if (!attribute || attribute.type !== "relation") {
6014
6642
  return null;
6015
6643
  }
6016
- return {
6644
+ const merged = {
6017
6645
  ...attribute.config,
6018
6646
  ...attribute
6019
6647
  };
6648
+ if (!("targets" in merged && Array.isArray(merged.targets) && "cardinality" in merged)) {
6649
+ return null;
6650
+ }
6651
+ return merged;
6652
+ }
6653
+ };
6654
+
6655
+ // src/runtime/services/user.service.ts
6656
+ var UserService = class {
6657
+ constructor(adapter, tenantId) {
6658
+ this.adapter = adapter;
6659
+ this.tenantId = tenantId;
6660
+ }
6661
+ /**
6662
+ * Validate all user attributes in the data
6663
+ *
6664
+ * @param schema - Object schema containing attribute definitions
6665
+ * @param data - Record data to validate
6666
+ * @returns Validation result with errors if any
6667
+ *
6668
+ * @example
6669
+ * ```typescript
6670
+ * const result = await userService.validateUsers(schema, {
6671
+ * assignee: "user-123",
6672
+ * watchers: ["user-456", "user-789"]
6673
+ * });
6674
+ *
6675
+ * if (!result.valid) {
6676
+ * console.log(result.errors);
6677
+ * // [{ attribute: "assignee", message: "User not found", invalidIds: ["user-123"] }]
6678
+ * }
6679
+ * ```
6680
+ */
6681
+ async validateUsers(schema, data) {
6682
+ const errors = [];
6683
+ const userAttrs = schema.attributes.filter(
6684
+ (attr) => attr.type === "user"
6685
+ );
6686
+ for (const attr of userAttrs) {
6687
+ const value = data[attr.name];
6688
+ if (value === void 0 || value === null) {
6689
+ continue;
6690
+ }
6691
+ const attrErrors = await this.validateUserAttribute(attr, value);
6692
+ errors.push(...attrErrors);
6693
+ }
6694
+ return {
6695
+ valid: errors.length === 0,
6696
+ errors
6697
+ };
6698
+ }
6699
+ /**
6700
+ * Validate a single user attribute value
6701
+ */
6702
+ async validateUserAttribute(attr, value) {
6703
+ const errors = [];
6704
+ const ids = this.extractIds(attr, value);
6705
+ if (ids.length === 0) {
6706
+ return errors;
6707
+ }
6708
+ const invalidIds = [];
6709
+ const roleErrors = [];
6710
+ for (const id of ids) {
6711
+ const user2 = await this.adapter.userProfiles.findById(id);
6712
+ if (!user2) {
6713
+ invalidIds.push(id);
6714
+ continue;
6715
+ }
6716
+ if (user2.tenantId !== this.tenantId) {
6717
+ invalidIds.push(id);
6718
+ continue;
6719
+ }
6720
+ if (attr.allowedRoles && attr.allowedRoles.length > 0) {
6721
+ if (!attr.allowedRoles.includes(user2.role)) {
6722
+ roleErrors.push(id);
6723
+ }
6724
+ }
6725
+ }
6726
+ if (invalidIds.length > 0) {
6727
+ errors.push({
6728
+ attribute: attr.name,
6729
+ message: `Invalid or non-existent users for ${attr.label}`,
6730
+ invalidIds
6731
+ });
6732
+ }
6733
+ if (roleErrors.length > 0) {
6734
+ errors.push({
6735
+ attribute: attr.name,
6736
+ message: `Users do not have required role for ${attr.label}. Allowed roles: ${attr.allowedRoles?.join(", ")}`,
6737
+ invalidIds: roleErrors
6738
+ });
6739
+ }
6740
+ return errors;
6741
+ }
6742
+ /**
6743
+ * Extract IDs from user value based on multiple flag
6744
+ */
6745
+ extractIds(attr, value) {
6746
+ if (attr.multiple) {
6747
+ if (!Array.isArray(value)) {
6748
+ return [];
6749
+ }
6750
+ return value.filter((v) => typeof v === "string" && v !== "");
6751
+ }
6752
+ if (typeof value === "string" && value !== "") {
6753
+ return [value];
6754
+ }
6755
+ return [];
6756
+ }
6757
+ /**
6758
+ * Validate users and throw if invalid
6759
+ */
6760
+ async validateUsersOrThrow(schema, data) {
6761
+ const result = await this.validateUsers(schema, data);
6762
+ if (!result.valid) {
6763
+ const errors = result.errors.map((e) => ({
6764
+ path: [e.attribute],
6765
+ message: e.message
6766
+ }));
6767
+ throw new ValidationError("User validation failed", errors);
6768
+ }
6020
6769
  }
6021
6770
  };
6022
6771
 
@@ -6027,9 +6776,10 @@ var RecordService = class {
6027
6776
  this.tenantId = tenantId;
6028
6777
  this.schemaService = new ObjectSchemaService(adapter, registry);
6029
6778
  this.relationService = new RelationService(adapter, registry);
6779
+ this.userService = new UserService(adapter, tenantId);
6030
6780
  this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
6031
6781
  this.permissionService = options?.permissionService;
6032
- this.auditService = options?.auditService;
6782
+ this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
6033
6783
  this.userId = options?.userId;
6034
6784
  this.userEmail = options?.userEmail;
6035
6785
  }
@@ -6096,21 +6846,23 @@ var RecordService = class {
6096
6846
  /**
6097
6847
  * Compute display label from schema expression
6098
6848
  * Automatically resolves relation attribute values to their labels
6849
+ * and select/multiselect values to their option labels
6099
6850
  * @internal
6100
6851
  */
6101
6852
  async computeLabel(schema, values) {
6102
6853
  const attrNames = extractAttributeNames(schema.labelExpression);
6854
+ let enrichedValues = enrichValuesWithSelectLabels(values, schema.attributes);
6103
6855
  const relationAttrs = schema.attributes.filter(
6104
6856
  (attr) => attr.type === "relation" && attrNames.includes(attr.name)
6105
6857
  );
6106
6858
  if (relationAttrs.length === 0) {
6107
- return renderLabelExpression(schema.labelExpression, values);
6859
+ return renderLabelExpression(schema.labelExpression, enrichedValues);
6108
6860
  }
6109
6861
  const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
6110
6862
  if (resolvedMap.size === 0) {
6111
- return renderLabelExpression(schema.labelExpression, values);
6863
+ return renderLabelExpression(schema.labelExpression, enrichedValues);
6112
6864
  }
6113
- const enrichedValues = { ...values };
6865
+ enrichedValues = { ...enrichedValues };
6114
6866
  for (const attr of relationAttrs) {
6115
6867
  const val = values[attr.name];
6116
6868
  const ids = this.extractRelationIds(val);
@@ -6165,6 +6917,9 @@ var RecordService = class {
6165
6917
  if (!options?.skipRelationValidation) {
6166
6918
  await this.relationService.validateRelationsOrThrow(schema, data);
6167
6919
  }
6920
+ if (!options?.skipUserValidation) {
6921
+ await this.userService.validateUsersOrThrow(schema, data);
6922
+ }
6168
6923
  }
6169
6924
  const completionStatus = computeRecordStatus(schema, data);
6170
6925
  const label = await this.computeLabel(schema, data);
@@ -6223,7 +6978,7 @@ var RecordService = class {
6223
6978
  async getRecordOrThrow(recordId) {
6224
6979
  const record = await this.getRecord(recordId);
6225
6980
  if (!record) {
6226
- throw new Error(`Record with id "${recordId}" not found`);
6981
+ throw new RecordNotFoundError(recordId);
6227
6982
  }
6228
6983
  return record;
6229
6984
  }
@@ -6276,6 +7031,9 @@ var RecordService = class {
6276
7031
  if (!options?.skipRelationValidation) {
6277
7032
  await this.relationService.validateRelationsOrThrow(schema, data);
6278
7033
  }
7034
+ if (!options?.skipUserValidation) {
7035
+ await this.userService.validateUsersOrThrow(schema, data);
7036
+ }
6279
7037
  }
6280
7038
  const completionStatus = computeRecordStatus(schema, mergedData);
6281
7039
  const label = await this.computeLabel(schema, mergedData);
@@ -6401,7 +7159,7 @@ var RecordService = class {
6401
7159
  await this.checkPermission(schema.name, "delete");
6402
7160
  if (options?.checkSystem) {
6403
7161
  if (schema.system) {
6404
- throw new Error(`Cannot delete record of system object "${schema.label}"`);
7162
+ throw new ProtectedResourceError("object", schema.name, "delete");
6405
7163
  }
6406
7164
  }
6407
7165
  if (!options?.skipReferenceCheck) {
@@ -6450,7 +7208,11 @@ var RecordService = class {
6450
7208
  async restoreRecord(recordId, options) {
6451
7209
  const record = await this.getRecordOrThrow(recordId);
6452
7210
  if (!record.deletedAt) {
6453
- throw new Error(`Record "${recordId}" is not deleted`);
7211
+ throw new SchemaError(
7212
+ `Record "${recordId}" is not deleted`,
7213
+ SchemaErrorCode.VALIDATION_FAILED,
7214
+ { recordId, reason: "not_deleted" }
7215
+ );
6454
7216
  }
6455
7217
  const schema = await this.schemaService.getObjectSchema(record.objectId);
6456
7218
  await this.checkPermission(schema.name, "update");
@@ -6888,11 +7650,17 @@ var ViewService = class {
6888
7650
  this.validateViewName(input.name);
6889
7651
  const existing = await this.adapter.views.findByName(tenantId, input.objectName, input.name);
6890
7652
  if (existing) {
6891
- throw new Error(`View "${input.name}" already exists for object "${input.objectName}"`);
7653
+ throw new SchemaError(
7654
+ `View "${input.name}" already exists for object "${input.objectName}"`,
7655
+ SchemaErrorCode.DUPLICATE_ATTRIBUTE,
7656
+ { viewName: input.name, objectName: input.objectName }
7657
+ );
6892
7658
  }
6893
7659
  if (this.nativeViews.has(input.objectName, input.name)) {
6894
- throw new Error(
6895
- `Cannot create view "${input.name}": a system view with this name already exists`
7660
+ throw new SchemaError(
7661
+ `Cannot create view "${input.name}": a system view with this name already exists`,
7662
+ SchemaErrorCode.DUPLICATE_ATTRIBUTE,
7663
+ { viewName: input.name, objectName: input.objectName, system: true }
6896
7664
  );
6897
7665
  }
6898
7666
  const dbView = await this.adapter.views.create({
@@ -6920,10 +7688,10 @@ var ViewService = class {
6920
7688
  async updateView(viewId, input) {
6921
7689
  const dbView = await this.adapter.views.findById(viewId);
6922
7690
  if (!dbView) {
6923
- throw new Error(`View with id "${viewId}" not found`);
7691
+ throw new NotFoundError("View", viewId);
6924
7692
  }
6925
7693
  if (dbView.system) {
6926
- throw new Error("Cannot modify system views. System views are protected.");
7694
+ throw new ProtectedResourceError("view", dbView.name, "modify");
6927
7695
  }
6928
7696
  const updated = await this.adapter.views.update(viewId, {
6929
7697
  label: input.label,
@@ -6943,10 +7711,10 @@ var ViewService = class {
6943
7711
  async deleteView(viewId) {
6944
7712
  const dbView = await this.adapter.views.findById(viewId);
6945
7713
  if (!dbView) {
6946
- throw new Error(`View with id "${viewId}" not found`);
7714
+ throw new NotFoundError("View", viewId);
6947
7715
  }
6948
7716
  if (dbView.system) {
6949
- throw new Error("Cannot delete system views. System views are protected.");
7717
+ throw new ProtectedResourceError("view", dbView.name, "delete");
6950
7718
  }
6951
7719
  await this.adapter.views.delete(viewId);
6952
7720
  }
@@ -6960,7 +7728,7 @@ var ViewService = class {
6960
7728
  async setDefaultView(viewId, tenantId) {
6961
7729
  const dbView = await this.adapter.views.findById(viewId);
6962
7730
  if (!dbView) {
6963
- throw new Error(`View with id "${viewId}" not found`);
7731
+ throw new NotFoundError("View", viewId);
6964
7732
  }
6965
7733
  const currentViews = await this.adapter.views.findByObjectName(tenantId, dbView.objectName);
6966
7734
  for (const v of currentViews) {
@@ -6979,16 +7747,23 @@ var ViewService = class {
6979
7747
  */
6980
7748
  validateViewName(name) {
6981
7749
  if (!name || name.length === 0) {
6982
- throw new Error("View name cannot be empty");
7750
+ throw new ValidationError("View name cannot be empty", [
7751
+ { path: ["name"], message: "View name cannot be empty" }
7752
+ ]);
6983
7753
  }
6984
7754
  if (name.length > 63) {
6985
- throw new Error("View name is too long (max 63 characters)");
7755
+ throw new ValidationError("View name is too long", [
7756
+ { path: ["name"], message: "View name is too long (max 63 characters)" }
7757
+ ]);
6986
7758
  }
6987
7759
  const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
6988
7760
  if (!kebabCaseRegex.test(name)) {
6989
- throw new Error(
6990
- "Invalid view name format. Name must be in kebab-case (e.g., 'detail', 'list-view')"
6991
- );
7761
+ throw new ValidationError("Invalid view name format", [
7762
+ {
7763
+ path: ["name"],
7764
+ message: "View name must be in kebab-case (e.g., 'detail', 'list-view')"
7765
+ }
7766
+ ]);
6992
7767
  }
6993
7768
  }
6994
7769
  /**
@@ -7350,6 +8125,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
7350
8125
  DEFAULT_ROLE_LABELS,
7351
8126
  DEFAULT_ROLE_PERMISSIONS,
7352
8127
  DuplicateError,
8128
+ EMPTY_VALUE_PLACEHOLDER,
7353
8129
  FileNotFoundError,
7354
8130
  FileService,
7355
8131
  FlowBuilder,
@@ -7387,6 +8163,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
7387
8163
  TableTabConfig,
7388
8164
  UserProfileNotFoundError,
7389
8165
  UserProfileService,
8166
+ UserService,
7390
8167
  ValidationError,
7391
8168
  ViewBuilder,
7392
8169
  ViewService,
@@ -7420,10 +8197,12 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
7420
8197
  currencyConfigSchema,
7421
8198
  date,
7422
8199
  dateConfigSchema,
8200
+ enrichValuesWithSelectLabels,
7423
8201
  extractAttributeNames,
7424
8202
  file,
7425
8203
  fileConfigSchema,
7426
8204
  flow,
8205
+ formatAttributeValue,
7427
8206
  generateId,
7428
8207
  generatePrefixedId,
7429
8208
  getAttributeConfigSchema,
@@ -7431,6 +8210,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
7431
8210
  getSyncPreview,
7432
8211
  getViewSyncPreview,
7433
8212
  group,
8213
+ isActivityTab,
7434
8214
  isAdvancedFilterState,
7435
8215
  isCustomTab,
7436
8216
  isDefaultRole,