@stndrds/schema 0.1.0-alpha.17 → 0.1.0-alpha.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 */
@@ -167,6 +171,7 @@ __export(index_exports, {
167
171
  TableTabConfig: () => TableTabConfig,
168
172
  UserProfileNotFoundError: () => UserProfileNotFoundError,
169
173
  UserProfileService: () => UserProfileService,
174
+ UserService: () => UserService,
170
175
  ValidationError: () => ValidationError,
171
176
  ViewBuilder: () => ViewBuilder,
172
177
  ViewService: () => ViewService,
@@ -469,6 +474,13 @@ function formatCheckbox(value) {
469
474
  function formatNumber(value, attribute) {
470
475
  if (typeof value !== "number") return String(value);
471
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
+ }
472
484
  return value.toLocaleString(void 0, {
473
485
  minimumFractionDigits: decimals,
474
486
  maximumFractionDigits: decimals
@@ -640,6 +652,7 @@ var SchemaErrorCode = {
640
652
  // Protected Resources
641
653
  PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
642
654
  PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
655
+ PROTECTED_VIEW: "SCHEMA_PROTECTED_VIEW",
643
656
  PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
644
657
  // Permissions
645
658
  FORBIDDEN: "SCHEMA_FORBIDDEN",
@@ -728,7 +741,7 @@ var ValidationError = class _ValidationError extends SchemaError {
728
741
  };
729
742
  var ProtectedResourceError = class extends SchemaError {
730
743
  constructor(resourceType, resourceName, operation) {
731
- 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;
732
745
  super(`Cannot ${operation} system ${resourceType} "${resourceName}"`, code, {
733
746
  resourceType,
734
747
  resourceName,
@@ -1324,6 +1337,13 @@ var FileAttributeBuilder = class extends BaseAttributeBuilder {
1324
1337
  this.attr.verification = config;
1325
1338
  return this;
1326
1339
  }
1340
+ /**
1341
+ * Allow multiple files to be uploaded
1342
+ */
1343
+ multiple() {
1344
+ this.attr.multiple = true;
1345
+ return this;
1346
+ }
1327
1347
  required() {
1328
1348
  this.setRequired(true);
1329
1349
  return this;
@@ -1344,6 +1364,13 @@ var UserAttributeBuilder = class extends BaseAttributeBuilder {
1344
1364
  this.attr.allowedRoles = roles;
1345
1365
  return this;
1346
1366
  }
1367
+ /**
1368
+ * Allow multiple users to be selected
1369
+ */
1370
+ multiple() {
1371
+ this.attr.multiple = true;
1372
+ return this;
1373
+ }
1347
1374
  required() {
1348
1375
  this.setRequired(true);
1349
1376
  return this;
@@ -2750,10 +2777,12 @@ var fileConfigSchema = baseConfigSchema.extend({
2750
2777
  maxFiles: import_zod4.z.number().int().min(1).optional(),
2751
2778
  maxSize: import_zod4.z.number().int().min(1).optional(),
2752
2779
  allowedTypes: import_zod4.z.array(import_zod4.z.string()).optional(),
2753
- verification: fileVerificationConfigSchema.optional()
2780
+ verification: fileVerificationConfigSchema.optional(),
2781
+ multiple: import_zod4.z.boolean().optional()
2754
2782
  });
2755
2783
  var userConfigSchema = baseConfigSchema.extend({
2756
- 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()
2757
2786
  });
2758
2787
  var relationConfigSchema = baseConfigSchema.extend({
2759
2788
  targets: import_zod4.z.array(relationTargetSchema).min(1),
@@ -2891,20 +2920,39 @@ function createLocationValidator(_attr) {
2891
2920
  function createTimestampValidator(_attr) {
2892
2921
  return import_zod4.z.number().int().positive();
2893
2922
  }
2894
- function createFileValidator(_attr) {
2895
- 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;
2896
2938
  }
2897
- function createUserValidator(_attr) {
2898
- 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;
2899
2947
  }
2900
2948
  function createSingleRelationValidator(attr) {
2901
- const uuidSchema = import_zod4.z.string().uuid({
2949
+ const uuidSchema = import_zod4.z.uuid({
2902
2950
  message: `${attr.label} must be a valid record ID`
2903
2951
  });
2904
2952
  return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
2905
2953
  }
2906
2954
  function createMultiRelationValidator(attr) {
2907
- const uuidSchema = import_zod4.z.string().uuid({
2955
+ const uuidSchema = import_zod4.z.uuid({
2908
2956
  message: `Each ${attr.label} item must be a valid record ID`
2909
2957
  });
2910
2958
  let arraySchema = import_zod4.z.array(uuidSchema);
@@ -4174,6 +4222,24 @@ var AuditService = class {
4174
4222
  };
4175
4223
  await this.log(entry);
4176
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
+ }
4177
4243
  /**
4178
4244
  * Log a system action (no actor)
4179
4245
  */
@@ -4323,21 +4389,105 @@ var AuditService = class {
4323
4389
 
4324
4390
  // src/runtime/services/file.service.ts
4325
4391
  var FileService = class {
4326
- constructor(adapter, tenantId) {
4392
+ constructor(adapter, tenantId, options) {
4327
4393
  this.adapter = adapter;
4328
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;
4329
4398
  }
4399
+ // ============================================================================
4400
+ // UPLOAD (requires StorageAdapter)
4401
+ // ============================================================================
4330
4402
  /**
4331
- * Create a new file record (after upload to storage)
4403
+ * Upload a file to storage and create metadata record.
4332
4404
  *
4333
- * @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
4334
4413
  * @returns Created file record
4414
+ * @throws Error if StorageAdapter is not configured
4335
4415
  *
4336
4416
  * @example
4337
4417
  * ```typescript
4338
- * 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).
4481
+ *
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
4339
4487
  *
4340
- * // After uploading to S3
4488
+ * @example
4489
+ * ```typescript
4490
+ * // After uploading to S3 with Multer
4341
4491
  * const file = await service.createFile({
4342
4492
  * tenantId: "tenant-123",
4343
4493
  * name: "contract-2025.pdf",
@@ -4359,8 +4509,26 @@ var FileService = class {
4359
4509
  `Tenant mismatch: service initialized with "${this.tenantId}" but data has "${data.tenantId}"`
4360
4510
  );
4361
4511
  }
4362
- 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;
4363
4528
  }
4529
+ // ============================================================================
4530
+ // READ
4531
+ // ============================================================================
4364
4532
  /**
4365
4533
  * Get file by ID
4366
4534
  */
@@ -4381,6 +4549,9 @@ var FileService = class {
4381
4549
  }
4382
4550
  return file2;
4383
4551
  }
4552
+ // ============================================================================
4553
+ // UPDATE
4554
+ // ============================================================================
4384
4555
  /**
4385
4556
  * Update file metadata
4386
4557
  *
@@ -4389,18 +4560,44 @@ var FileService = class {
4389
4560
  * @returns Updated file
4390
4561
  */
4391
4562
  async updateFile(fileId, data) {
4392
- await this.getFileOrThrow(fileId);
4393
- 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;
4394
4588
  }
4589
+ // ============================================================================
4590
+ // DELETE
4591
+ // ============================================================================
4395
4592
  /**
4396
- * Delete file (soft delete)
4593
+ * Delete file (soft delete by default)
4397
4594
  *
4398
4595
  * @param fileId - File UUID
4399
4596
  * @param options - Delete options
4400
4597
  */
4401
4598
  async deleteFile(fileId, options) {
4599
+ const file2 = await this.getFileOrThrow(fileId);
4402
4600
  if (options?.checkOwnership && options.userId) {
4403
- const file2 = await this.getFileOrThrow(fileId);
4404
4601
  if (file2.uploadedBy !== options.userId) {
4405
4602
  throw new Error("You can only delete files you uploaded");
4406
4603
  }
@@ -4410,7 +4607,84 @@ var FileService = class {
4410
4607
  } else {
4411
4608
  await this.adapter.files.delete(fileId);
4412
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
+ }
4413
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
+ }
4684
+ }
4685
+ // ============================================================================
4686
+ // LIST
4687
+ // ============================================================================
4414
4688
  /**
4415
4689
  * List files for the tenant
4416
4690
  */
@@ -4429,6 +4703,74 @@ var FileService = class {
4429
4703
  async listFilesByUploader(uploadedBy) {
4430
4704
  return await this.adapter.files.findByUploader(uploadedBy);
4431
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
+ }
4432
4774
  /**
4433
4775
  * Change file visibility
4434
4776
  *
@@ -4472,29 +4814,9 @@ var FileService = class {
4472
4814
  allowedUsers: newAllowed
4473
4815
  });
4474
4816
  }
4475
- /**
4476
- * Check if user has access to a file
4477
- *
4478
- * @param fileId - File UUID
4479
- * @param userId - User ID to check
4480
- * @returns true if user can access the file
4481
- */
4482
- async canAccess(fileId, userId) {
4483
- const file2 = await this.getFile(fileId);
4484
- if (!file2) {
4485
- return false;
4486
- }
4487
- if (file2.visibility === "public") {
4488
- return true;
4489
- }
4490
- if (file2.uploadedBy === userId) {
4491
- return true;
4492
- }
4493
- if (file2.visibility === "restricted") {
4494
- return file2.allowedUsers?.includes(userId) ?? false;
4495
- }
4496
- return false;
4497
- }
4817
+ // ============================================================================
4818
+ // ORGANIZATION
4819
+ // ============================================================================
4498
4820
  /**
4499
4821
  * Move file to different folder
4500
4822
  */
@@ -6330,6 +6652,123 @@ var RelationService = class {
6330
6652
  }
6331
6653
  };
6332
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
+ }
6769
+ }
6770
+ };
6771
+
6333
6772
  // src/runtime/services/record.service.ts
6334
6773
  var RecordService = class {
6335
6774
  constructor(adapter, tenantId, options) {
@@ -6337,6 +6776,7 @@ var RecordService = class {
6337
6776
  this.tenantId = tenantId;
6338
6777
  this.schemaService = new ObjectSchemaService(adapter, registry);
6339
6778
  this.relationService = new RelationService(adapter, registry);
6779
+ this.userService = new UserService(adapter, tenantId);
6340
6780
  this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
6341
6781
  this.permissionService = options?.permissionService;
6342
6782
  this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
@@ -6477,6 +6917,9 @@ var RecordService = class {
6477
6917
  if (!options?.skipRelationValidation) {
6478
6918
  await this.relationService.validateRelationsOrThrow(schema, data);
6479
6919
  }
6920
+ if (!options?.skipUserValidation) {
6921
+ await this.userService.validateUsersOrThrow(schema, data);
6922
+ }
6480
6923
  }
6481
6924
  const completionStatus = computeRecordStatus(schema, data);
6482
6925
  const label = await this.computeLabel(schema, data);
@@ -6588,6 +7031,9 @@ var RecordService = class {
6588
7031
  if (!options?.skipRelationValidation) {
6589
7032
  await this.relationService.validateRelationsOrThrow(schema, data);
6590
7033
  }
7034
+ if (!options?.skipUserValidation) {
7035
+ await this.userService.validateUsersOrThrow(schema, data);
7036
+ }
6591
7037
  }
6592
7038
  const completionStatus = computeRecordStatus(schema, mergedData);
6593
7039
  const label = await this.computeLabel(schema, mergedData);
@@ -7245,7 +7691,7 @@ var ViewService = class {
7245
7691
  throw new NotFoundError("View", viewId);
7246
7692
  }
7247
7693
  if (dbView.system) {
7248
- throw new ProtectedResourceError("attribute", dbView.name, "modify");
7694
+ throw new ProtectedResourceError("view", dbView.name, "modify");
7249
7695
  }
7250
7696
  const updated = await this.adapter.views.update(viewId, {
7251
7697
  label: input.label,
@@ -7268,7 +7714,7 @@ var ViewService = class {
7268
7714
  throw new NotFoundError("View", viewId);
7269
7715
  }
7270
7716
  if (dbView.system) {
7271
- throw new ProtectedResourceError("attribute", dbView.name, "delete");
7717
+ throw new ProtectedResourceError("view", dbView.name, "delete");
7272
7718
  }
7273
7719
  await this.adapter.views.delete(viewId);
7274
7720
  }
@@ -7717,6 +8163,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
7717
8163
  TableTabConfig,
7718
8164
  UserProfileNotFoundError,
7719
8165
  UserProfileService,
8166
+ UserService,
7720
8167
  ValidationError,
7721
8168
  ViewBuilder,
7722
8169
  ViewService,