@stndrds/schema 0.1.0-alpha.14 → 0.1.0-alpha.16

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 (72) hide show
  1. package/dist/chunk-2ASXJITY.mjs +5360 -0
  2. package/dist/chunk-5AWYVQKR.mjs +5310 -0
  3. package/dist/chunk-65HUQZHK.mjs +5374 -0
  4. package/dist/chunk-73P4F2OU.mjs +5809 -0
  5. package/dist/chunk-BHKKVBPP.mjs +5527 -0
  6. package/dist/chunk-BTVPMGYW.mjs +6327 -0
  7. package/dist/chunk-BUWVNOSK.mjs +5457 -0
  8. package/dist/chunk-C7HZ55GJ.mjs +6882 -0
  9. package/dist/chunk-CCEHW4E7.mjs +6071 -0
  10. package/dist/chunk-CNR3BZLU.mjs +5163 -0
  11. package/dist/chunk-DLMVX6DN.mjs +6074 -0
  12. package/dist/chunk-DUUORFHI.mjs +5379 -0
  13. package/dist/chunk-DWTD7YPJ.mjs +74 -0
  14. package/dist/chunk-FAR5ENGR.mjs +78 -0
  15. package/dist/chunk-ITWOVZOW.mjs +5809 -0
  16. package/dist/chunk-JDUTFH3J.mjs +6940 -0
  17. package/dist/chunk-KJLKY5X7.mjs +6314 -0
  18. package/dist/chunk-KTJXMGBR.mjs +5397 -0
  19. package/dist/chunk-NOVHQ7B2.mjs +6327 -0
  20. package/dist/chunk-PMFQ4ZL4.mjs +6230 -0
  21. package/dist/chunk-QAGN53HE.mjs +6327 -0
  22. package/dist/chunk-QC47SMCZ.mjs +5467 -0
  23. package/dist/chunk-UUOAVXAI.mjs +6327 -0
  24. package/dist/chunk-VYZ3BOSB.mjs +6946 -0
  25. package/dist/chunk-XAX75NSP.mjs +6321 -0
  26. package/dist/chunk-Z6LIJ5PO.mjs +5362 -0
  27. package/dist/chunk-ZG3PT3RL.mjs +6944 -0
  28. package/dist/default-roles-G4TOWUVM.mjs +20 -0
  29. package/dist/default-roles-QEZPRSUL.mjs +20 -0
  30. package/dist/index.d.mts +419 -16
  31. package/dist/index.d.ts +419 -16
  32. package/dist/index.js +2580 -406
  33. package/dist/index.mjs +80 -163
  34. package/dist/runtime-B1_syHVj.d.mts +4787 -0
  35. package/dist/runtime-B1_syHVj.d.ts +4787 -0
  36. package/dist/runtime-B7N-oycb.d.mts +4854 -0
  37. package/dist/runtime-B7N-oycb.d.ts +4854 -0
  38. package/dist/runtime-BDoAIuTe.d.mts +4872 -0
  39. package/dist/runtime-BDoAIuTe.d.ts +4872 -0
  40. package/dist/runtime-BPYJWZfC.d.mts +5130 -0
  41. package/dist/runtime-BPYJWZfC.d.ts +5130 -0
  42. package/dist/runtime-BVRmzg3d.d.mts +4718 -0
  43. package/dist/runtime-BVRmzg3d.d.ts +4718 -0
  44. package/dist/runtime-BrXwIidf.d.mts +4794 -0
  45. package/dist/runtime-BrXwIidf.d.ts +4794 -0
  46. package/dist/runtime-CC_HZhVN.d.mts +5224 -0
  47. package/dist/runtime-CC_HZhVN.d.ts +5224 -0
  48. package/dist/runtime-CEx57sBZ.d.mts +4791 -0
  49. package/dist/runtime-CEx57sBZ.d.ts +4791 -0
  50. package/dist/runtime-COGvuXIq.d.mts +4790 -0
  51. package/dist/runtime-COGvuXIq.d.ts +4790 -0
  52. package/dist/runtime-CSGWjztI.d.mts +5166 -0
  53. package/dist/runtime-CSGWjztI.d.ts +5166 -0
  54. package/dist/runtime-DRc_OhnM.d.mts +5142 -0
  55. package/dist/runtime-DRc_OhnM.d.ts +5142 -0
  56. package/dist/runtime-DUxWyRcq.d.mts +5399 -0
  57. package/dist/runtime-DUxWyRcq.d.ts +5399 -0
  58. package/dist/runtime-D_S3m9jf.d.mts +5142 -0
  59. package/dist/runtime-D_S3m9jf.d.ts +5142 -0
  60. package/dist/runtime-Db3rRxbJ.d.mts +4821 -0
  61. package/dist/runtime-Db3rRxbJ.d.ts +4821 -0
  62. package/dist/runtime-DlCiIIfr.d.mts +5130 -0
  63. package/dist/runtime-DlCiIIfr.d.ts +5130 -0
  64. package/dist/runtime-LO17NDk1.d.mts +5292 -0
  65. package/dist/runtime-LO17NDk1.d.ts +5292 -0
  66. package/dist/runtime-WqYxjhjJ.d.mts +5125 -0
  67. package/dist/runtime-WqYxjhjJ.d.ts +5125 -0
  68. package/dist/runtime.d.mts +1 -1
  69. package/dist/runtime.d.ts +1 -1
  70. package/dist/runtime.js +2178 -370
  71. package/dist/runtime.mjs +9 -1
  72. package/package.json +3 -3
package/dist/runtime.js CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,20 +30,109 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/constants/default-roles.ts
34
+ var default_roles_exports = {};
35
+ __export(default_roles_exports, {
36
+ ALL_SYSTEM_RESOURCES: () => ALL_SYSTEM_RESOURCES,
37
+ DEFAULT_ROLES: () => DEFAULT_ROLES,
38
+ DEFAULT_ROLE_DESCRIPTIONS: () => DEFAULT_ROLE_DESCRIPTIONS,
39
+ DEFAULT_ROLE_LABELS: () => DEFAULT_ROLE_LABELS,
40
+ DEFAULT_ROLE_PERMISSIONS: () => DEFAULT_ROLE_PERMISSIONS,
41
+ SYSTEM_RESOURCES: () => SYSTEM_RESOURCES,
42
+ SYSTEM_RESOURCE_LABELS: () => SYSTEM_RESOURCE_LABELS,
43
+ isDefaultRole: () => isDefaultRole
44
+ });
45
+ function isDefaultRole(roleName) {
46
+ return Object.values(DEFAULT_ROLES).includes(roleName);
47
+ }
48
+ var SYSTEM_RESOURCES, ALL_SYSTEM_RESOURCES, SYSTEM_RESOURCE_LABELS, DEFAULT_ROLES, DEFAULT_ROLE_LABELS, DEFAULT_ROLE_DESCRIPTIONS, ALL_ACTIONS, MEMBER_ACTIONS, READ_ONLY, DEFAULT_ROLE_PERMISSIONS;
49
+ var init_default_roles = __esm({
50
+ "src/constants/default-roles.ts"() {
51
+ "use strict";
52
+ SYSTEM_RESOURCES = {
53
+ /** User profiles and invitations management */
54
+ USERS: "users",
55
+ /** Object definitions and attributes management */
56
+ SCHEMA: "schema",
57
+ /** Roles and permissions management */
58
+ ROLES: "roles",
59
+ /** Tenant settings and configuration */
60
+ SETTINGS: "settings",
61
+ /** Audit logs and activity trail */
62
+ AUDIT: "audit"
63
+ };
64
+ ALL_SYSTEM_RESOURCES = [
65
+ SYSTEM_RESOURCES.USERS,
66
+ SYSTEM_RESOURCES.SCHEMA,
67
+ SYSTEM_RESOURCES.ROLES,
68
+ SYSTEM_RESOURCES.SETTINGS,
69
+ SYSTEM_RESOURCES.AUDIT
70
+ ];
71
+ SYSTEM_RESOURCE_LABELS = {
72
+ users: "Users",
73
+ schema: "Schema",
74
+ roles: "Roles & Permissions",
75
+ settings: "Settings",
76
+ audit: "Audit Logs"
77
+ };
78
+ DEFAULT_ROLES = {
79
+ /** Full platform access - can manage everything */
80
+ ADMIN: "admin",
81
+ /** Standard user - can work with business data but not platform settings */
82
+ MEMBER: "member",
83
+ /** Read-only access - can view but not modify */
84
+ GUEST: "guest"
85
+ };
86
+ DEFAULT_ROLE_LABELS = {
87
+ admin: "Administrator",
88
+ member: "Member",
89
+ guest: "Guest"
90
+ };
91
+ DEFAULT_ROLE_DESCRIPTIONS = {
92
+ admin: "Full access to all platform features and data",
93
+ member: "Can view, create and edit business data",
94
+ guest: "Read-only access to business data"
95
+ };
96
+ ALL_ACTIONS = ["read", "create", "update", "delete"];
97
+ MEMBER_ACTIONS = ["read", "create", "update"];
98
+ READ_ONLY = ["read"];
99
+ DEFAULT_ROLE_PERMISSIONS = {
100
+ admin: {
101
+ system: [{ target: "*", actions: ALL_ACTIONS }],
102
+ object: [{ target: "*", actions: ALL_ACTIONS }]
103
+ },
104
+ member: {
105
+ system: [],
106
+ // No access to system resources
107
+ object: [{ target: "*", actions: MEMBER_ACTIONS }]
108
+ },
109
+ guest: {
110
+ system: [],
111
+ // No access to system resources
112
+ object: [{ target: "*", actions: READ_ONLY }]
113
+ }
114
+ };
115
+ }
116
+ });
117
+
30
118
  // src/runtime.ts
31
119
  var runtime_exports = {};
32
120
  __export(runtime_exports, {
121
+ AuditService: () => AuditService,
33
122
  DEFAULT_LABEL_FALLBACK: () => DEFAULT_LABEL_FALLBACK,
34
123
  FileService: () => FileService,
124
+ FlowService: () => FlowService,
35
125
  GeocodingService: () => GeocodingService,
36
126
  GlobalSearchService: () => GlobalSearchService,
37
127
  NoopGeocodingAdapter: () => NoopGeocodingAdapter,
38
128
  NoopHookRegistry: () => NoopHookRegistry,
39
129
  ObjectSchemaService: () => ObjectSchemaService,
130
+ PermissionService: () => PermissionService,
40
131
  RecordService: () => RecordService,
41
132
  RelationService: () => RelationService,
42
133
  UserProfileService: () => UserProfileService,
43
134
  ViewService: () => ViewService,
135
+ buildAuditChanges: () => buildAuditChanges,
44
136
  createMockAdapter: () => createMockAdapter,
45
137
  extractAttributeNames: () => extractAttributeNames,
46
138
  getSyncPreview: () => getSyncPreview,
@@ -375,6 +467,12 @@ function createMockUserProfilesRepository(stores) {
375
467
  }
376
468
  return Promise.resolve(results);
377
469
  },
470
+ countByRole(tenantId, role) {
471
+ const count = Array.from(stores.userProfiles.values()).filter(
472
+ (profile) => profile.tenantId === tenantId && profile.role === role
473
+ ).length;
474
+ return Promise.resolve(count);
475
+ },
378
476
  updateLastLogin(id) {
379
477
  const profile = stores.userProfiles.get(id);
380
478
  if (profile) {
@@ -468,12 +566,36 @@ function createMockFilesRepository(stores) {
468
566
  }
469
567
  function createMockObjectRecordsRepository(stores) {
470
568
  return {
569
+ hardDelete(id) {
570
+ stores.objectRecords.delete(id);
571
+ return Promise.resolve();
572
+ },
573
+ restore(id) {
574
+ const existing = stores.objectRecords.get(id);
575
+ if (!existing) {
576
+ return Promise.reject(new Error(`ObjectRecord ${id} not found`));
577
+ }
578
+ existing.deletedAt = null;
579
+ stores.objectRecords.set(id, existing);
580
+ return Promise.resolve(existing);
581
+ },
471
582
  findById(id) {
472
583
  const internal = stores.objectRecords.get(id);
473
584
  if (!internal) return Promise.resolve(null);
474
585
  const { tenantId: _, ...record } = internal;
475
586
  return Promise.resolve(record);
476
587
  },
588
+ findByIds(ids) {
589
+ const results = [];
590
+ for (const id of ids) {
591
+ const internal = stores.objectRecords.get(id);
592
+ if (internal) {
593
+ const { tenantId: _, ...record } = internal;
594
+ results.push(record);
595
+ }
596
+ }
597
+ return Promise.resolve(results);
598
+ },
477
599
  create(data) {
478
600
  const internal = {
479
601
  id: generateId(),
@@ -586,6 +708,85 @@ function createMockObjectRecordsRepository(stores) {
586
708
  };
587
709
  });
588
710
  return Promise.resolve({ results, total });
711
+ },
712
+ // -------------------------------------------------------------------------
713
+ // Schema Integrity Methods
714
+ // -------------------------------------------------------------------------
715
+ countRecordsReferencingId(targetId) {
716
+ const relationAttrs = [];
717
+ for (const attr of stores.attributes.values()) {
718
+ if (attr.type === "relation") {
719
+ relationAttrs.push({ objectId: attr.objectId, attrName: attr.name });
720
+ }
721
+ }
722
+ const countsByObjectId = /* @__PURE__ */ new Map();
723
+ for (const record of stores.objectRecords.values()) {
724
+ for (const { objectId, attrName } of relationAttrs) {
725
+ if (record.objectId !== objectId) continue;
726
+ const value = record.values[attrName];
727
+ let isReferencing = false;
728
+ if (typeof value === "string" && value === targetId) {
729
+ isReferencing = true;
730
+ } else if (Array.isArray(value) && value.includes(targetId)) {
731
+ isReferencing = true;
732
+ }
733
+ if (isReferencing) {
734
+ const current = countsByObjectId.get(objectId) ?? 0;
735
+ countsByObjectId.set(objectId, current + 1);
736
+ }
737
+ }
738
+ }
739
+ const results = [];
740
+ for (const [objectId, count] of countsByObjectId) {
741
+ const obj = stores.objects.get(objectId);
742
+ if (obj) {
743
+ results.push({
744
+ objectName: obj.name,
745
+ objectLabel: obj.label,
746
+ count
747
+ });
748
+ }
749
+ }
750
+ return Promise.resolve(results);
751
+ },
752
+ removeAttributeData(objectId, attributeName) {
753
+ let updated = 0;
754
+ for (const record of stores.objectRecords.values()) {
755
+ if (record.objectId === objectId && attributeName in record.values) {
756
+ delete record.values[attributeName];
757
+ record.updatedAt = /* @__PURE__ */ new Date();
758
+ updated++;
759
+ }
760
+ }
761
+ return Promise.resolve(updated);
762
+ },
763
+ batchRefreshLabels(objectId, computeLabel) {
764
+ let updated = 0;
765
+ for (const record of stores.objectRecords.values()) {
766
+ if (record.objectId === objectId) {
767
+ const newLabel = computeLabel(record.values);
768
+ if (newLabel !== record.label) {
769
+ record.label = newLabel;
770
+ record.updatedAt = /* @__PURE__ */ new Date();
771
+ updated++;
772
+ }
773
+ }
774
+ }
775
+ return Promise.resolve(updated);
776
+ },
777
+ batchRefreshStatus(objectId, computeStatus) {
778
+ let updated = 0;
779
+ for (const record of stores.objectRecords.values()) {
780
+ if (record.objectId === objectId) {
781
+ const newStatus = computeStatus(record.values);
782
+ if (newStatus !== record.completionStatus) {
783
+ record.completionStatus = newStatus;
784
+ record.updatedAt = /* @__PURE__ */ new Date();
785
+ updated++;
786
+ }
787
+ }
788
+ }
789
+ return Promise.resolve(updated);
589
790
  }
590
791
  };
591
792
  }
@@ -689,6 +890,157 @@ function createMockViewsRepository(stores) {
689
890
  }
690
891
  };
691
892
  }
893
+ function createMockPermissionsRepository(stores) {
894
+ return {
895
+ getRolesByTenant(tenantId) {
896
+ const results = Array.from(stores.roles.values()).filter((r) => r.tenantId === tenantId);
897
+ return Promise.resolve(results);
898
+ },
899
+ getRoleById(roleId) {
900
+ return Promise.resolve(stores.roles.get(roleId) ?? null);
901
+ },
902
+ getRoleByName(tenantId, name) {
903
+ for (const role of stores.roles.values()) {
904
+ if (role.tenantId === tenantId && role.name === name) {
905
+ return Promise.resolve(role);
906
+ }
907
+ }
908
+ return Promise.resolve(null);
909
+ },
910
+ createRole(input) {
911
+ const role = {
912
+ id: generateId(),
913
+ tenantId: input.tenantId,
914
+ name: input.name,
915
+ label: input.label,
916
+ description: input.description,
917
+ system: false,
918
+ createdAt: /* @__PURE__ */ new Date(),
919
+ updatedAt: /* @__PURE__ */ new Date()
920
+ };
921
+ stores.roles.set(role.id, role);
922
+ return Promise.resolve(role);
923
+ },
924
+ updateRole(roleId, updates) {
925
+ const existing = stores.roles.get(roleId);
926
+ if (!existing) {
927
+ return Promise.reject(new Error(`Role ${roleId} not found`));
928
+ }
929
+ const updated = {
930
+ ...existing,
931
+ ...updates,
932
+ updatedAt: /* @__PURE__ */ new Date()
933
+ };
934
+ stores.roles.set(roleId, updated);
935
+ return Promise.resolve(updated);
936
+ },
937
+ deleteRole(roleId) {
938
+ const role = stores.roles.get(roleId);
939
+ if (role?.system) {
940
+ return Promise.reject(new Error(`Cannot delete system role ${roleId}`));
941
+ }
942
+ stores.roles.delete(roleId);
943
+ for (const [id, perm] of stores.permissions.entries()) {
944
+ if (perm.roleId === roleId) {
945
+ stores.permissions.delete(id);
946
+ }
947
+ }
948
+ for (const [id, ur] of stores.userRoles.entries()) {
949
+ if (ur.roleId === roleId) {
950
+ stores.userRoles.delete(id);
951
+ }
952
+ }
953
+ return Promise.resolve();
954
+ },
955
+ getPermissionsByRole(roleId) {
956
+ const results = Array.from(stores.permissions.values()).filter((p) => p.roleId === roleId);
957
+ return Promise.resolve(results);
958
+ },
959
+ setPermissions(roleId, permissions) {
960
+ for (const [id, perm] of stores.permissions.entries()) {
961
+ if (perm.roleId === roleId) {
962
+ stores.permissions.delete(id);
963
+ }
964
+ }
965
+ for (const input of permissions) {
966
+ const perm = {
967
+ id: generateId(),
968
+ roleId,
969
+ scope: input.scope,
970
+ target: input.target,
971
+ actions: input.actions,
972
+ filter: input.filter,
973
+ createdAt: /* @__PURE__ */ new Date()
974
+ };
975
+ stores.permissions.set(perm.id, perm);
976
+ }
977
+ return Promise.resolve();
978
+ },
979
+ getUserRoles(userProfileId, tenantId) {
980
+ const roleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === userProfileId && ur.tenantId === tenantId).map((ur) => ur.roleId);
981
+ const roles = Array.from(stores.roles.values()).filter((r) => roleIds.includes(r.id));
982
+ return Promise.resolve(roles);
983
+ },
984
+ assignRole(input) {
985
+ for (const ur of stores.userRoles.values()) {
986
+ if (ur.userProfileId === input.userProfileId && ur.roleId === input.roleId && ur.tenantId === input.tenantId) {
987
+ return Promise.resolve(ur);
988
+ }
989
+ }
990
+ const userRole = {
991
+ id: generateId(),
992
+ userProfileId: input.userProfileId,
993
+ roleId: input.roleId,
994
+ tenantId: input.tenantId,
995
+ assignedAt: /* @__PURE__ */ new Date(),
996
+ assignedBy: input.assignedBy
997
+ };
998
+ stores.userRoles.set(userRole.id, userRole);
999
+ return Promise.resolve(userRole);
1000
+ },
1001
+ revokeRole(userProfileId, roleId, tenantId) {
1002
+ for (const [id, ur] of stores.userRoles.entries()) {
1003
+ if (ur.userProfileId === userProfileId && ur.roleId === roleId && ur.tenantId === tenantId) {
1004
+ stores.userRoles.delete(id);
1005
+ break;
1006
+ }
1007
+ }
1008
+ return Promise.resolve();
1009
+ },
1010
+ getEffectivePermissions(userProfileId, tenantId) {
1011
+ const userRoleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === userProfileId && ur.tenantId === tenantId).map((ur) => ur.roleId);
1012
+ const userRoles = Array.from(stores.roles.values()).filter((r) => userRoleIds.includes(r.id));
1013
+ const isAdmin = userRoles.some((r) => r.name === "admin");
1014
+ const userPermissions = Array.from(stores.permissions.values()).filter(
1015
+ (p) => userRoleIds.includes(p.roleId)
1016
+ );
1017
+ const objectPermissions = {};
1018
+ const systemPermissions = {};
1019
+ for (const perm of userPermissions) {
1020
+ if (perm.scope === "object") {
1021
+ if (!objectPermissions[perm.target]) {
1022
+ objectPermissions[perm.target] = [];
1023
+ }
1024
+ for (const action of perm.actions) {
1025
+ if (!objectPermissions[perm.target].includes(action)) {
1026
+ objectPermissions[perm.target].push(action);
1027
+ }
1028
+ }
1029
+ } else if (perm.scope === "system") {
1030
+ if (!systemPermissions[perm.target]) {
1031
+ systemPermissions[perm.target] = [];
1032
+ }
1033
+ for (const action of perm.actions) {
1034
+ if (!systemPermissions[perm.target].includes(action)) {
1035
+ systemPermissions[perm.target].push(action);
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ return Promise.resolve({ isAdmin, objectPermissions, systemPermissions });
1041
+ }
1042
+ };
1043
+ }
692
1044
  function createMockAdapter() {
693
1045
  const stores = {
694
1046
  objects: /* @__PURE__ */ new Map(),
@@ -696,7 +1048,10 @@ function createMockAdapter() {
696
1048
  userProfiles: /* @__PURE__ */ new Map(),
697
1049
  files: /* @__PURE__ */ new Map(),
698
1050
  objectRecords: /* @__PURE__ */ new Map(),
699
- views: /* @__PURE__ */ new Map()
1051
+ views: /* @__PURE__ */ new Map(),
1052
+ roles: /* @__PURE__ */ new Map(),
1053
+ permissions: /* @__PURE__ */ new Map(),
1054
+ userRoles: /* @__PURE__ */ new Map()
700
1055
  };
701
1056
  const adapter = {
702
1057
  objects: createMockObjectsRepository(stores),
@@ -705,6 +1060,7 @@ function createMockAdapter() {
705
1060
  files: createMockFilesRepository(stores),
706
1061
  objectRecords: createMockObjectRecordsRepository(stores),
707
1062
  views: createMockViewsRepository(stores),
1063
+ permissions: createMockPermissionsRepository(stores),
708
1064
  async transaction(callback) {
709
1065
  return await callback(adapter);
710
1066
  },
@@ -718,11 +1074,266 @@ function createMockAdapter() {
718
1074
  stores.files.clear();
719
1075
  stores.objectRecords.clear();
720
1076
  stores.views.clear();
1077
+ stores.roles.clear();
1078
+ stores.permissions.clear();
1079
+ stores.userRoles.clear();
721
1080
  }
722
1081
  };
723
1082
  return adapter;
724
1083
  }
725
1084
 
1085
+ // src/runtime/services/audit.helpers.ts
1086
+ function buildAuditChanges(oldValues, newValues, fieldsToCheck) {
1087
+ const changes = [];
1088
+ for (const field of fieldsToCheck) {
1089
+ if (newValues[field] !== void 0 && newValues[field] !== oldValues[field]) {
1090
+ changes.push({
1091
+ field: String(field),
1092
+ oldValue: oldValues[field],
1093
+ newValue: newValues[field]
1094
+ });
1095
+ }
1096
+ }
1097
+ return changes;
1098
+ }
1099
+
1100
+ // src/runtime/services/audit.service.ts
1101
+ var SENSITIVE_PATTERNS = [
1102
+ /password/i,
1103
+ /token/i,
1104
+ /secret/i,
1105
+ /apiKey/i,
1106
+ /api_key/i,
1107
+ /creditCard/i,
1108
+ /credit_card/i,
1109
+ /ssn/i,
1110
+ /cvv/i
1111
+ ];
1112
+ var AuditService = class {
1113
+ constructor(adapter, tenantId, options) {
1114
+ this.adapter = adapter;
1115
+ this.tenantId = tenantId;
1116
+ this.options = options;
1117
+ this.buffer = [];
1118
+ this.flushTimer = null;
1119
+ if (options?.async && options.flushIntervalMs) {
1120
+ this.startFlushTimer();
1121
+ }
1122
+ }
1123
+ // ============================================================================
1124
+ // PUBLIC LOGGING METHODS
1125
+ // ============================================================================
1126
+ /**
1127
+ * Log a record action (create, update, delete, restore)
1128
+ */
1129
+ async logRecordAction(params) {
1130
+ const entry = {
1131
+ tenantId: this.tenantId,
1132
+ actorId: params.actorId,
1133
+ actorEmail: params.actorEmail,
1134
+ actorType: "user",
1135
+ action: params.action,
1136
+ resourceType: "record",
1137
+ resourceId: params.recordId,
1138
+ resourceLabel: params.recordLabel,
1139
+ objectName: params.objectName,
1140
+ objectId: params.objectId,
1141
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
1142
+ metadata: params.metadata
1143
+ };
1144
+ await this.log(entry);
1145
+ }
1146
+ /**
1147
+ * Log a schema action (object/attribute changes)
1148
+ */
1149
+ async logSchemaAction(params) {
1150
+ const entry = {
1151
+ tenantId: this.tenantId,
1152
+ actorId: params.actorId,
1153
+ actorEmail: params.actorEmail,
1154
+ actorType: "user",
1155
+ action: params.action,
1156
+ resourceType: params.resourceType,
1157
+ resourceId: params.resourceId,
1158
+ resourceLabel: params.resourceLabel,
1159
+ objectName: params.objectName,
1160
+ objectId: params.objectId,
1161
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
1162
+ metadata: params.metadata
1163
+ };
1164
+ await this.log(entry);
1165
+ }
1166
+ /**
1167
+ * Log a user action
1168
+ */
1169
+ async logUserAction(params) {
1170
+ const entry = {
1171
+ tenantId: this.tenantId,
1172
+ actorId: params.actorId,
1173
+ actorEmail: params.actorEmail,
1174
+ actorType: "user",
1175
+ action: params.action,
1176
+ resourceType: "user",
1177
+ resourceId: params.targetUserId,
1178
+ resourceLabel: params.targetUserEmail,
1179
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
1180
+ metadata: params.metadata
1181
+ };
1182
+ await this.log(entry);
1183
+ }
1184
+ /**
1185
+ * Log a role action
1186
+ */
1187
+ async logRoleAction(params) {
1188
+ const entry = {
1189
+ tenantId: this.tenantId,
1190
+ actorId: params.actorId,
1191
+ actorEmail: params.actorEmail,
1192
+ actorType: "user",
1193
+ action: params.action,
1194
+ resourceType: "role",
1195
+ resourceId: params.roleId,
1196
+ resourceLabel: params.roleLabel,
1197
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
1198
+ metadata: {
1199
+ ...params.metadata,
1200
+ targetUserId: params.targetUserId
1201
+ }
1202
+ };
1203
+ await this.log(entry);
1204
+ }
1205
+ /**
1206
+ * Log a system action (no actor)
1207
+ */
1208
+ async logSystemAction(params) {
1209
+ const entry = {
1210
+ tenantId: this.tenantId,
1211
+ actorType: "system",
1212
+ action: params.action,
1213
+ resourceType: params.resourceType,
1214
+ resourceId: params.resourceId,
1215
+ resourceLabel: params.resourceLabel,
1216
+ objectName: params.objectName,
1217
+ objectId: params.objectId,
1218
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
1219
+ metadata: params.metadata
1220
+ };
1221
+ await this.log(entry);
1222
+ }
1223
+ // ============================================================================
1224
+ // QUERY METHODS
1225
+ // ============================================================================
1226
+ /**
1227
+ * List audit logs with filtering and pagination
1228
+ */
1229
+ async list(options) {
1230
+ if (!this.adapter.audit) {
1231
+ return { entries: [], total: 0 };
1232
+ }
1233
+ return await this.adapter.audit.list(this.tenantId, options);
1234
+ }
1235
+ /**
1236
+ * Get audit history for a specific record
1237
+ */
1238
+ async getRecordHistory(recordId) {
1239
+ if (!this.adapter.audit) {
1240
+ return [];
1241
+ }
1242
+ return await this.adapter.audit.getByResource(this.tenantId, "record", recordId, {
1243
+ orderDirection: "desc"
1244
+ });
1245
+ }
1246
+ /**
1247
+ * Get activity for a specific user
1248
+ */
1249
+ async getUserActivity(userId, options) {
1250
+ if (!this.adapter.audit) {
1251
+ return [];
1252
+ }
1253
+ return await this.adapter.audit.getByActor(this.tenantId, userId, options);
1254
+ }
1255
+ /**
1256
+ * Get audit logs for a specific resource
1257
+ */
1258
+ async getResourceHistory(resourceType, resourceId) {
1259
+ if (!this.adapter.audit) {
1260
+ return [];
1261
+ }
1262
+ return await this.adapter.audit.getByResource(this.tenantId, resourceType, resourceId, {
1263
+ orderDirection: "desc"
1264
+ });
1265
+ }
1266
+ // ============================================================================
1267
+ // BUFFER MANAGEMENT (ASYNC MODE)
1268
+ // ============================================================================
1269
+ /**
1270
+ * Flush buffered logs to the database
1271
+ */
1272
+ async flush() {
1273
+ if (this.buffer.length === 0 || !this.adapter.audit) {
1274
+ return;
1275
+ }
1276
+ const entries = [...this.buffer];
1277
+ this.buffer = [];
1278
+ await this.adapter.audit.createMany(entries);
1279
+ }
1280
+ /**
1281
+ * Clean up resources (stop timer, flush remaining logs)
1282
+ */
1283
+ async dispose() {
1284
+ if (this.flushTimer) {
1285
+ clearInterval(this.flushTimer);
1286
+ this.flushTimer = null;
1287
+ }
1288
+ await this.flush();
1289
+ }
1290
+ // ============================================================================
1291
+ // PRIVATE METHODS
1292
+ // ============================================================================
1293
+ /**
1294
+ * Log an entry (sync or async based on options)
1295
+ */
1296
+ async log(entry) {
1297
+ if (!this.adapter.audit) {
1298
+ return;
1299
+ }
1300
+ if (this.options?.async) {
1301
+ this.buffer.push(entry);
1302
+ const batchSize = this.options.batchSize ?? 10;
1303
+ if (this.buffer.length >= batchSize) {
1304
+ await this.flush();
1305
+ }
1306
+ } else {
1307
+ await this.adapter.audit.create(entry);
1308
+ }
1309
+ }
1310
+ /**
1311
+ * Start the flush timer for async mode
1312
+ */
1313
+ startFlushTimer() {
1314
+ const intervalMs = this.options?.flushIntervalMs ?? 1e3;
1315
+ this.flushTimer = setInterval(() => {
1316
+ this.flush().catch(console.error);
1317
+ }, intervalMs);
1318
+ }
1319
+ /**
1320
+ * Redact sensitive fields from changes
1321
+ */
1322
+ redactSensitiveFields(changes) {
1323
+ return changes.map((change) => {
1324
+ const isSensitive = SENSITIVE_PATTERNS.some((pattern) => pattern.test(change.field));
1325
+ if (isSensitive) {
1326
+ return {
1327
+ ...change,
1328
+ oldValue: "[REDACTED]",
1329
+ newValue: "[REDACTED]"
1330
+ };
1331
+ }
1332
+ return change;
1333
+ });
1334
+ }
1335
+ };
1336
+
726
1337
  // src/runtime/services/file.service.ts
727
1338
  var FileService = class {
728
1339
  constructor(adapter, tenantId) {
@@ -923,63 +1534,330 @@ var FileService = class {
923
1534
  }
924
1535
  };
925
1536
 
926
- // src/runtime/services/geocoding.service.ts
927
- var GeocodingService = class {
928
- constructor(adapter) {
1537
+ // src/runtime/services/flow.service.ts
1538
+ var FlowService = class {
1539
+ constructor(adapter, systemFlows = []) {
929
1540
  this.adapter = adapter;
1541
+ this.systemFlows = new Map(systemFlows.map((f) => [f.name, f]));
930
1542
  }
931
1543
  /**
932
- * Search for address suggestions as the user types
1544
+ * Get all flows for a tenant (system + custom)
933
1545
  */
934
- async autocomplete(params) {
935
- if (!params.query || params.query.trim().length < 2) {
936
- return [];
1546
+ async getAllFlows(tenantId) {
1547
+ const systemFlowsList = Array.from(this.systemFlows.values());
1548
+ if (!this.adapter.flows) {
1549
+ return systemFlowsList;
937
1550
  }
938
- return await this.adapter.autocomplete({
939
- ...params,
940
- query: params.query.trim(),
941
- limit: params.limit ?? 5
942
- });
1551
+ const dbFlows = await this.adapter.flows.findAllForTenant(tenantId);
1552
+ const customFlows = dbFlows.filter((f) => !f.system).map(this.convertDBFlowToDefinition);
1553
+ return [...systemFlowsList, ...customFlows];
943
1554
  }
944
1555
  /**
945
- * Reverse geocode coordinates to an address
1556
+ * Get published flows only
946
1557
  */
947
- async reverse(params) {
948
- if (!this.adapter.reverse) {
949
- throw new Error("Reverse geocoding is not supported by the configured adapter");
950
- }
951
- return await this.adapter.reverse(params);
1558
+ async getPublishedFlows(tenantId) {
1559
+ const allFlows = await this.getAllFlows(tenantId);
1560
+ return allFlows.filter((f) => f.status === "published");
952
1561
  }
953
1562
  /**
954
- * Geocode a structured address to coordinates
1563
+ * Get a specific flow by name
955
1564
  */
956
- async geocode(params) {
957
- if (!this.adapter.geocode) {
958
- throw new Error("Geocoding is not supported by the configured adapter");
1565
+ async getFlow(name, tenantId) {
1566
+ const systemFlow = this.systemFlows.get(name);
1567
+ if (systemFlow) {
1568
+ return systemFlow;
959
1569
  }
960
- return await this.adapter.geocode(params);
1570
+ if (!this.adapter.flows) {
1571
+ return null;
1572
+ }
1573
+ const dbFlow = await this.adapter.flows.findByName(tenantId, name);
1574
+ if (dbFlow) {
1575
+ return this.convertDBFlowToDefinition(dbFlow);
1576
+ }
1577
+ return null;
961
1578
  }
962
- };
963
-
964
- // src/runtime/services/global-search.service.ts
965
- var GlobalSearchService = class {
966
- constructor(adapter, tenantId) {
967
- this.adapter = adapter;
968
- this.tenantId = tenantId;
1579
+ /**
1580
+ * Get a flow by ID
1581
+ */
1582
+ async getFlowById(flowId) {
1583
+ if (!this.adapter.flows) {
1584
+ return null;
1585
+ }
1586
+ const dbFlow = await this.adapter.flows.findById(flowId);
1587
+ if (dbFlow) {
1588
+ return this.convertDBFlowToDefinition(dbFlow);
1589
+ }
1590
+ return null;
969
1591
  }
970
1592
  /**
971
- * Search across all object records for the tenant
972
- *
973
- * @param query - Search query string
974
- * @param options - Search options (pagination, object filters)
975
- * @returns Matching records with object metadata and total count
976
- *
977
- * @example
978
- * ```typescript
979
- * // Basic search
980
- * const { results, total } = await service.search("nike air");
981
- *
982
- * // With pagination
1593
+ * Create a new custom flow (as draft)
1594
+ */
1595
+ async createFlow(input, tenantId) {
1596
+ if (!this.adapter.flows) {
1597
+ throw new Error("Flows feature is not enabled. Database adapter does not support flows.");
1598
+ }
1599
+ this.validateFlowName(input.name);
1600
+ const existing = await this.adapter.flows.findByName(tenantId, input.name);
1601
+ if (existing) {
1602
+ throw new Error(`Flow "${input.name}" already exists`);
1603
+ }
1604
+ if (this.systemFlows.has(input.name)) {
1605
+ throw new Error(
1606
+ `Cannot create flow "${input.name}": a system flow with this name already exists`
1607
+ );
1608
+ }
1609
+ this.validateFlowStructure(input);
1610
+ const dbFlow = await this.adapter.flows.create({
1611
+ tenantId,
1612
+ name: input.name,
1613
+ label: input.label,
1614
+ description: input.description,
1615
+ icon: input.icon,
1616
+ status: "draft",
1617
+ version: 1,
1618
+ slots: input.slots,
1619
+ pages: input.pages,
1620
+ relations: input.relations,
1621
+ system: false,
1622
+ metadata: input.metadata
1623
+ });
1624
+ return this.convertDBFlowToDefinition(dbFlow);
1625
+ }
1626
+ /**
1627
+ * Update a custom flow
1628
+ */
1629
+ async updateFlow(flowId, input) {
1630
+ if (!this.adapter.flows) {
1631
+ throw new Error("Flows feature is not enabled.");
1632
+ }
1633
+ const dbFlow = await this.adapter.flows.findById(flowId);
1634
+ if (!dbFlow) {
1635
+ throw new Error(`Flow with id "${flowId}" not found`);
1636
+ }
1637
+ if (dbFlow.system) {
1638
+ throw new Error("Cannot modify system flows. System flows are protected.");
1639
+ }
1640
+ if (input.slots || input.pages || input.relations) {
1641
+ this.validateFlowStructure({
1642
+ name: dbFlow.name,
1643
+ label: input.label ?? dbFlow.label,
1644
+ slots: input.slots ?? dbFlow.slots,
1645
+ pages: input.pages ?? dbFlow.pages,
1646
+ relations: input.relations ?? dbFlow.relations
1647
+ });
1648
+ }
1649
+ const updated = await this.adapter.flows.update(flowId, {
1650
+ label: input.label,
1651
+ description: input.description,
1652
+ icon: input.icon,
1653
+ slots: input.slots,
1654
+ pages: input.pages,
1655
+ relations: input.relations,
1656
+ metadata: input.metadata
1657
+ });
1658
+ return this.convertDBFlowToDefinition(updated);
1659
+ }
1660
+ /**
1661
+ * Publish a flow
1662
+ */
1663
+ async publishFlow(flowId) {
1664
+ if (!this.adapter.flows) {
1665
+ throw new Error("Flows feature is not enabled.");
1666
+ }
1667
+ const dbFlow = await this.adapter.flows.findById(flowId);
1668
+ if (!dbFlow) {
1669
+ throw new Error(`Flow with id "${flowId}" not found`);
1670
+ }
1671
+ if (dbFlow.system) {
1672
+ throw new Error("Cannot publish system flows. They are always published.");
1673
+ }
1674
+ if (dbFlow.status === "published") {
1675
+ return this.convertDBFlowToDefinition(dbFlow);
1676
+ }
1677
+ this.validateFlowStructure({
1678
+ name: dbFlow.name,
1679
+ label: dbFlow.label,
1680
+ slots: dbFlow.slots,
1681
+ pages: dbFlow.pages,
1682
+ relations: dbFlow.relations
1683
+ });
1684
+ const updated = await this.adapter.flows.update(flowId, {
1685
+ status: "published",
1686
+ version: dbFlow.version + 1
1687
+ });
1688
+ return this.convertDBFlowToDefinition(updated);
1689
+ }
1690
+ /**
1691
+ * Archive a flow
1692
+ */
1693
+ async archiveFlow(flowId) {
1694
+ if (!this.adapter.flows) {
1695
+ throw new Error("Flows feature is not enabled.");
1696
+ }
1697
+ const dbFlow = await this.adapter.flows.findById(flowId);
1698
+ if (!dbFlow) {
1699
+ throw new Error(`Flow with id "${flowId}" not found`);
1700
+ }
1701
+ if (dbFlow.system) {
1702
+ throw new Error("Cannot archive system flows.");
1703
+ }
1704
+ const updated = await this.adapter.flows.update(flowId, {
1705
+ status: "archived"
1706
+ });
1707
+ return this.convertDBFlowToDefinition(updated);
1708
+ }
1709
+ /**
1710
+ * Delete a custom flow
1711
+ */
1712
+ async deleteFlow(flowId) {
1713
+ if (!this.adapter.flows) {
1714
+ throw new Error("Flows feature is not enabled.");
1715
+ }
1716
+ const dbFlow = await this.adapter.flows.findById(flowId);
1717
+ if (!dbFlow) {
1718
+ throw new Error(`Flow with id "${flowId}" not found`);
1719
+ }
1720
+ if (dbFlow.system) {
1721
+ throw new Error("Cannot delete system flows. System flows are protected.");
1722
+ }
1723
+ await this.adapter.flows.delete(flowId);
1724
+ }
1725
+ // ============================================================================
1726
+ // PRIVATE HELPERS
1727
+ // ============================================================================
1728
+ /**
1729
+ * Validate flow name format (kebab-case)
1730
+ */
1731
+ validateFlowName(name) {
1732
+ if (!name || name.length === 0) {
1733
+ throw new Error("Flow name cannot be empty");
1734
+ }
1735
+ if (name.length > 63) {
1736
+ throw new Error("Flow name is too long (max 63 characters)");
1737
+ }
1738
+ const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
1739
+ 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
+ );
1743
+ }
1744
+ }
1745
+ /**
1746
+ * Validate flow structure (slots, pages, relations)
1747
+ */
1748
+ validateFlowStructure(input) {
1749
+ if (!input.slots || input.slots.length === 0) {
1750
+ throw new Error("Flow must have at least one slot");
1751
+ }
1752
+ if (!input.pages || input.pages.length === 0) {
1753
+ throw new Error("Flow must have at least one page");
1754
+ }
1755
+ const slotIds = new Set(input.slots.map((s) => s.id));
1756
+ if (slotIds.size !== input.slots.length) {
1757
+ throw new Error("Duplicate slot IDs detected");
1758
+ }
1759
+ for (const page of input.pages) {
1760
+ for (const row of page.rows) {
1761
+ for (const field of row.fields) {
1762
+ if (!slotIds.has(field.slotId)) {
1763
+ throw new Error(`Field "${field.id}" references unknown slot "${field.slotId}"`);
1764
+ }
1765
+ }
1766
+ }
1767
+ }
1768
+ for (const relation of input.relations) {
1769
+ if (!slotIds.has(relation.sourceSlotId)) {
1770
+ throw new Error(`Relation references unknown source slot "${relation.sourceSlotId}"`);
1771
+ }
1772
+ if (!slotIds.has(relation.targetSlotId)) {
1773
+ throw new Error(`Relation references unknown target slot "${relation.targetSlotId}"`);
1774
+ }
1775
+ if (relation.sourceSlotId === relation.targetSlotId) {
1776
+ throw new Error(`Relation cannot link a slot to itself: "${relation.sourceSlotId}"`);
1777
+ }
1778
+ }
1779
+ }
1780
+ /**
1781
+ * Convert database flow to FlowDefinition
1782
+ */
1783
+ convertDBFlowToDefinition(dbFlow) {
1784
+ return {
1785
+ id: dbFlow.id,
1786
+ name: dbFlow.name,
1787
+ label: dbFlow.label,
1788
+ description: dbFlow.description,
1789
+ icon: dbFlow.icon,
1790
+ status: dbFlow.status,
1791
+ version: dbFlow.version,
1792
+ slots: dbFlow.slots,
1793
+ pages: dbFlow.pages,
1794
+ relations: dbFlow.relations,
1795
+ system: dbFlow.system,
1796
+ tenantId: dbFlow.tenantId,
1797
+ metadata: dbFlow.metadata,
1798
+ createdAt: dbFlow.createdAt,
1799
+ updatedAt: dbFlow.updatedAt
1800
+ };
1801
+ }
1802
+ };
1803
+
1804
+ // src/runtime/services/geocoding.service.ts
1805
+ var GeocodingService = class {
1806
+ constructor(adapter) {
1807
+ this.adapter = adapter;
1808
+ }
1809
+ /**
1810
+ * Search for address suggestions as the user types
1811
+ */
1812
+ async autocomplete(params) {
1813
+ if (!params.query || params.query.trim().length < 2) {
1814
+ return [];
1815
+ }
1816
+ return await this.adapter.autocomplete({
1817
+ ...params,
1818
+ query: params.query.trim(),
1819
+ limit: params.limit ?? 5
1820
+ });
1821
+ }
1822
+ /**
1823
+ * Reverse geocode coordinates to an address
1824
+ */
1825
+ async reverse(params) {
1826
+ if (!this.adapter.reverse) {
1827
+ throw new Error("Reverse geocoding is not supported by the configured adapter");
1828
+ }
1829
+ return await this.adapter.reverse(params);
1830
+ }
1831
+ /**
1832
+ * Geocode a structured address to coordinates
1833
+ */
1834
+ async geocode(params) {
1835
+ if (!this.adapter.geocode) {
1836
+ throw new Error("Geocoding is not supported by the configured adapter");
1837
+ }
1838
+ return await this.adapter.geocode(params);
1839
+ }
1840
+ };
1841
+
1842
+ // src/runtime/services/global-search.service.ts
1843
+ var GlobalSearchService = class {
1844
+ constructor(adapter, tenantId) {
1845
+ this.adapter = adapter;
1846
+ this.tenantId = tenantId;
1847
+ }
1848
+ /**
1849
+ * Search across all object records for the tenant
1850
+ *
1851
+ * @param query - Search query string
1852
+ * @param options - Search options (pagination, object filters)
1853
+ * @returns Matching records with object metadata and total count
1854
+ *
1855
+ * @example
1856
+ * ```typescript
1857
+ * // Basic search
1858
+ * const { results, total } = await service.search("nike air");
1859
+ *
1860
+ * // With pagination
983
1861
  * const { results, total } = await service.search("nike", {
984
1862
  * limit: 10,
985
1863
  * offset: 20
@@ -1042,8 +1920,11 @@ var GlobalSearchService = class {
1042
1920
  var import_constants = require("@stndrds/constants");
1043
1921
  var VALID_ICONS = new Set(import_constants.ICONS);
1044
1922
 
1923
+ // src/builders/flow-builder.ts
1924
+ var import_zod = require("zod");
1925
+
1045
1926
  // src/builders/object-builder.ts
1046
- var import_zod = __toESM(require("zod"));
1927
+ var import_zod2 = __toESM(require("zod"));
1047
1928
  var ObjectBuilder = class {
1048
1929
  constructor(config) {
1049
1930
  this.validateName(config.name);
@@ -1176,14 +2057,14 @@ The labelExpression defines how records are displayed in lists and relations.
1176
2057
  if (name === void 0) {
1177
2058
  return;
1178
2059
  }
1179
- const objectNameSchema = import_zod.default.string().min(1, "Object name cannot be empty").max(63, "Object name is too long (max 63 characters)").regex(/^[a-z][a-z0-9-]*$/, {
2060
+ const objectNameSchema = import_zod2.default.string().min(1, "Object name cannot be empty").max(63, "Object name is too long (max 63 characters)").regex(/^[a-z][a-z0-9-]*$/, {
1180
2061
  message: "Invalid object name format.\nName must be in kebab-case (slug format):\n \u2705 Valid: 'products', 'user-profiles', 'order-items'\n \u274C Invalid: 'Products', 'userProfiles', 'user_profiles'"
1181
2062
  });
1182
2063
  try {
1183
2064
  objectNameSchema.parse(name);
1184
2065
  } catch (error) {
1185
- if (error instanceof import_zod.default.ZodError) {
1186
- throw new Error(`[ObjectBuilder] ${error.errors[0].message}`);
2066
+ if (error instanceof import_zod2.default.ZodError) {
2067
+ throw new Error(`[ObjectBuilder] ${error.issues[0].message}`);
1187
2068
  }
1188
2069
  throw error;
1189
2070
  }
@@ -1194,38 +2075,70 @@ function object(config) {
1194
2075
  }
1195
2076
 
1196
2077
  // src/builders/view-builder.ts
1197
- var import_zod2 = require("zod");
2078
+ var import_zod3 = require("zod");
2079
+
2080
+ // src/types/errors.ts
2081
+ var RecordReferencedError = class extends Error {
2082
+ constructor(recordId, references) {
2083
+ const total = references.reduce((sum, r) => sum + r.count, 0);
2084
+ super(`Cannot delete record: referenced by ${total} record${total > 1 ? "s" : ""}`);
2085
+ this.recordId = recordId;
2086
+ this.references = references;
2087
+ this.code = "RECORD_REFERENCED";
2088
+ this.name = "RecordReferencedError";
2089
+ }
2090
+ };
2091
+ var AttributeInUseError = class extends Error {
2092
+ constructor(attributeName, usage) {
2093
+ super(`Cannot delete attribute "${attributeName}": used in ${usage}`);
2094
+ this.attributeName = attributeName;
2095
+ this.usage = usage;
2096
+ this.code = "ATTRIBUTE_IN_USE";
2097
+ this.name = "AttributeInUseError";
2098
+ }
2099
+ };
2100
+ var ObjectReferencedError = class extends Error {
2101
+ constructor(objectName, referencingObjects) {
2102
+ super(
2103
+ `Cannot delete object "${objectName}": target of relations in ${referencingObjects.join(", ")}`
2104
+ );
2105
+ this.objectName = objectName;
2106
+ this.referencingObjects = referencingObjects;
2107
+ this.code = "OBJECT_REFERENCED";
2108
+ this.name = "ObjectReferencedError";
2109
+ }
2110
+ };
1198
2111
 
1199
2112
  // src/validation/validators.ts
1200
- var import_zod3 = require("zod");
1201
- var baseConfigSchema = import_zod3.z.object({
1202
- disabled: import_zod3.z.boolean().optional(),
1203
- placeholder: import_zod3.z.string().optional(),
1204
- description: import_zod3.z.string().optional(),
1205
- defaultValue: import_zod3.z.unknown().optional(),
1206
- icon: import_zod3.z.string().optional(),
1207
- order: import_zod3.z.number().int().optional(),
1208
- hidden: import_zod3.z.boolean().optional(),
1209
- archived: import_zod3.z.boolean().optional(),
1210
- deprecated: import_zod3.z.boolean().optional(),
1211
- metadata: import_zod3.z.record(import_zod3.z.unknown()).optional()
2113
+ var import_zod4 = require("zod");
2114
+ var baseConfigSchema = import_zod4.z.object({
2115
+ disabled: import_zod4.z.boolean().optional(),
2116
+ placeholder: import_zod4.z.string().optional(),
2117
+ description: import_zod4.z.string().optional(),
2118
+ defaultValue: import_zod4.z.unknown().optional(),
2119
+ icon: import_zod4.z.string().optional(),
2120
+ order: import_zod4.z.number().int().optional(),
2121
+ hidden: import_zod4.z.boolean().optional(),
2122
+ archived: import_zod4.z.boolean().optional(),
2123
+ deprecated: import_zod4.z.boolean().optional(),
2124
+ metadata: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.unknown()).optional()
1212
2125
  });
1213
- var optionSchema = import_zod3.z.object({
1214
- id: import_zod3.z.string().min(1),
1215
- label: import_zod3.z.string().min(1),
1216
- value: import_zod3.z.string().min(1),
1217
- color: import_zod3.z.string().optional(),
1218
- icon: import_zod3.z.string().optional(),
1219
- description: import_zod3.z.string().optional(),
1220
- group: import_zod3.z.enum(["idle", "in_progress", "finished"]).optional()
2126
+ var optionSchema = import_zod4.z.object({
2127
+ id: import_zod4.z.string().min(1),
2128
+ label: import_zod4.z.string().min(1),
2129
+ value: import_zod4.z.string().min(1),
2130
+ color: import_zod4.z.string().optional(),
2131
+ icon: import_zod4.z.string().optional(),
2132
+ description: import_zod4.z.string().optional(),
2133
+ group: import_zod4.z.enum(["idle", "in_progress", "finished"]).optional()
1221
2134
  });
1222
- var relationTargetSchema = import_zod3.z.object({
1223
- object: import_zod3.z.string().min(1),
1224
- displayTemplate: import_zod3.z.string().optional(),
1225
- filter: import_zod3.z.record(import_zod3.z.unknown()).optional()
2135
+ var relationTargetSchema = import_zod4.z.object({
2136
+ object: import_zod4.z.string().min(1),
2137
+ displayTemplate: import_zod4.z.string().optional(),
2138
+ filter: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.unknown()).optional()
1226
2139
  });
1227
- var documentTypeConfigSchema = import_zod3.z.object({
1228
- type: import_zod3.z.enum([
2140
+ var documentTypeConfigSchema = import_zod4.z.object({
2141
+ type: import_zod4.z.enum([
1229
2142
  "id_card",
1230
2143
  "passport",
1231
2144
  "incorporation_certificate",
@@ -1236,87 +2149,86 @@ var documentTypeConfigSchema = import_zod3.z.object({
1236
2149
  "proof_of_address",
1237
2150
  "custom"
1238
2151
  ]),
1239
- label: import_zod3.z.string(),
1240
- faces: import_zod3.z.array(import_zod3.z.enum(["front", "back", "single"])),
1241
- attributeMapping: import_zod3.z.array(
1242
- import_zod3.z.object({
1243
- attributeId: import_zod3.z.string(),
1244
- extractedKey: import_zod3.z.string(),
1245
- face: import_zod3.z.enum(["front", "back", "single"]).optional(),
1246
- required: import_zod3.z.boolean().optional()
2152
+ label: import_zod4.z.string(),
2153
+ faces: import_zod4.z.array(import_zod4.z.enum(["front", "back", "single"])),
2154
+ attributeMapping: import_zod4.z.array(
2155
+ import_zod4.z.object({
2156
+ attributeId: import_zod4.z.string(),
2157
+ extractedKey: import_zod4.z.string(),
2158
+ face: import_zod4.z.enum(["front", "back", "single"]).optional(),
2159
+ required: import_zod4.z.boolean().optional()
1247
2160
  })
1248
2161
  ).optional()
1249
2162
  });
1250
- var fileVerificationConfigSchema = import_zod3.z.object({
1251
- enabled: import_zod3.z.boolean(),
1252
- documentTypes: import_zod3.z.array(documentTypeConfigSchema),
1253
- autoExtract: import_zod3.z.boolean().optional(),
1254
- autoValidate: import_zod3.z.boolean().optional()
2163
+ var fileVerificationConfigSchema = import_zod4.z.object({
2164
+ enabled: import_zod4.z.boolean(),
2165
+ documentTypes: import_zod4.z.array(documentTypeConfigSchema),
2166
+ autoExtract: import_zod4.z.boolean().optional(),
2167
+ autoValidate: import_zod4.z.boolean().optional()
1255
2168
  });
1256
2169
  var textConfigSchema = baseConfigSchema.extend({
1257
- minLength: import_zod3.z.number().int().min(0).optional(),
1258
- maxLength: import_zod3.z.number().int().min(1).optional(),
1259
- pattern: import_zod3.z.string().optional()
2170
+ minLength: import_zod4.z.number().int().min(0).optional(),
2171
+ maxLength: import_zod4.z.number().int().min(1).optional(),
2172
+ pattern: import_zod4.z.string().optional()
1260
2173
  });
1261
2174
  var textareaConfigSchema = baseConfigSchema;
1262
2175
  var numberConfigSchema = baseConfigSchema.extend({
1263
- min: import_zod3.z.number().optional(),
1264
- max: import_zod3.z.number().optional(),
1265
- unit: import_zod3.z.enum(["integer", "decimal", "percentage"]).optional(),
1266
- decimals: import_zod3.z.number().int().min(0).optional()
2176
+ min: import_zod4.z.number().optional(),
2177
+ max: import_zod4.z.number().optional(),
2178
+ unit: import_zod4.z.enum(["integer", "decimal", "percentage"]).optional(),
2179
+ decimals: import_zod4.z.number().int().min(0).optional()
1267
2180
  });
1268
2181
  var checkboxConfigSchema = baseConfigSchema;
1269
2182
  var dateConfigSchema = baseConfigSchema.extend({
1270
- dateFormat: import_zod3.z.enum(["short", "long", "full", "relative"]).optional(),
1271
- minDate: import_zod3.z.string().optional(),
1272
- maxDate: import_zod3.z.string().optional()
2183
+ dateFormat: import_zod4.z.enum(["short", "long", "full", "relative"]).optional(),
2184
+ minDate: import_zod4.z.string().optional(),
2185
+ maxDate: import_zod4.z.string().optional()
1273
2186
  });
1274
2187
  var phoneConfigSchema = baseConfigSchema.extend({
1275
- defaultCountryCode: import_zod3.z.string().length(3).optional()
2188
+ defaultCountryCode: import_zod4.z.string().length(3).optional()
1276
2189
  });
1277
2190
  var currencyConfigSchema = baseConfigSchema.extend({
1278
- defaultCurrency: import_zod3.z.string().length(3).optional(),
1279
- allowedCurrencies: import_zod3.z.array(import_zod3.z.string().length(3)).optional()
2191
+ defaultCurrency: import_zod4.z.string().length(3).optional(),
2192
+ allowedCurrencies: import_zod4.z.array(import_zod4.z.string().length(3)).optional()
1280
2193
  });
1281
2194
  var statusConfigSchema = baseConfigSchema.extend({
1282
- options: import_zod3.z.array(optionSchema).min(1)
2195
+ options: import_zod4.z.array(optionSchema).min(1)
1283
2196
  });
1284
2197
  var locationConfigSchema = baseConfigSchema.extend({
1285
- granularity: import_zod3.z.enum(["full", "address", "city", "state", "country", "coordinates"]),
1286
- enableAutocomplete: import_zod3.z.boolean().optional(),
1287
- enableMap: import_zod3.z.boolean().optional(),
1288
- defaultCountry: import_zod3.z.string().length(3).optional(),
1289
- allowedCountries: import_zod3.z.array(import_zod3.z.string().length(3)).optional(),
1290
- displayFormat: import_zod3.z.enum(["single_line", "multi_line", "compact"]).optional()
2198
+ granularity: import_zod4.z.enum(["full", "address", "city", "state", "country", "coordinates"]),
2199
+ enableAutocomplete: import_zod4.z.boolean().optional(),
2200
+ enableMap: import_zod4.z.boolean().optional(),
2201
+ defaultCountry: import_zod4.z.string().length(3).optional(),
2202
+ allowedCountries: import_zod4.z.array(import_zod4.z.string().length(3)).optional(),
2203
+ displayFormat: import_zod4.z.enum(["single_line", "multi_line", "compact"]).optional()
1291
2204
  });
1292
2205
  var timestampConfigSchema = baseConfigSchema.extend({
1293
- autoUpdate: import_zod3.z.boolean().optional()
2206
+ autoUpdate: import_zod4.z.boolean().optional()
1294
2207
  });
1295
2208
  var selectConfigSchema = baseConfigSchema.extend({
1296
- options: import_zod3.z.array(optionSchema).min(1)
2209
+ options: import_zod4.z.array(optionSchema).min(1)
1297
2210
  });
1298
2211
  var multiselectConfigSchema = baseConfigSchema.extend({
1299
- options: import_zod3.z.array(optionSchema).min(1)
2212
+ options: import_zod4.z.array(optionSchema).min(1)
1300
2213
  });
1301
2214
  var fileConfigSchema = baseConfigSchema.extend({
1302
- maxFiles: import_zod3.z.number().int().min(1).optional(),
1303
- maxSize: import_zod3.z.number().int().min(1).optional(),
1304
- allowedTypes: import_zod3.z.array(import_zod3.z.string()).optional(),
2215
+ maxFiles: import_zod4.z.number().int().min(1).optional(),
2216
+ maxSize: import_zod4.z.number().int().min(1).optional(),
2217
+ allowedTypes: import_zod4.z.array(import_zod4.z.string()).optional(),
1305
2218
  verification: fileVerificationConfigSchema.optional()
1306
2219
  });
1307
2220
  var userConfigSchema = baseConfigSchema.extend({
1308
- allowedRoles: import_zod3.z.array(import_zod3.z.string()).optional()
2221
+ allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional()
1309
2222
  });
1310
2223
  var relationConfigSchema = baseConfigSchema.extend({
1311
- targets: import_zod3.z.array(relationTargetSchema).min(1),
1312
- onDelete: import_zod3.z.enum(["restrict", "cascade", "set_null"]).optional(),
1313
- cardinality: import_zod3.z.enum(["one", "many"]),
1314
- minItems: import_zod3.z.number().int().min(0).optional(),
1315
- maxItems: import_zod3.z.number().int().min(1).optional()
2224
+ targets: import_zod4.z.array(relationTargetSchema).min(1),
2225
+ cardinality: import_zod4.z.enum(["one", "many"]),
2226
+ minItems: import_zod4.z.number().int().min(0).optional(),
2227
+ maxItems: import_zod4.z.number().int().min(1).optional()
1316
2228
  });
1317
2229
  var ratingConfigSchema = baseConfigSchema.extend({
1318
- max: import_zod3.z.number().int().min(1).optional(),
1319
- iconType: import_zod3.z.enum(["star", "heart", "thumbs", "number"]).optional()
2230
+ max: import_zod4.z.number().int().min(1).optional(),
2231
+ iconType: import_zod4.z.enum(["star", "heart", "thumbs", "number"]).optional()
1320
2232
  });
1321
2233
  var attributeConfigSchemas = {
1322
2234
  text: textConfigSchema,
@@ -1344,7 +2256,7 @@ function parseAttributeConfig(type, config) {
1344
2256
  return schema.strip().parse(config);
1345
2257
  }
1346
2258
  function createTextValidator(attr) {
1347
- let schema = import_zod3.z.string();
2259
+ let schema = import_zod4.z.string();
1348
2260
  if (attr.minLength !== void 0) {
1349
2261
  schema = schema.min(
1350
2262
  attr.minLength,
@@ -1363,7 +2275,7 @@ function createTextValidator(attr) {
1363
2275
  return schema;
1364
2276
  }
1365
2277
  function createNumberValidator(attr) {
1366
- let schema = import_zod3.z.number();
2278
+ let schema = import_zod4.z.number();
1367
2279
  if (attr.min !== void 0) {
1368
2280
  schema = schema.min(attr.min, `${attr.label} must be at least ${attr.min}`);
1369
2281
  }
@@ -1376,81 +2288,75 @@ function createNumberValidator(attr) {
1376
2288
  return schema;
1377
2289
  }
1378
2290
  function createCheckboxValidator(_attr) {
1379
- return import_zod3.z.boolean();
2291
+ return import_zod4.z.boolean();
1380
2292
  }
1381
2293
  function createDateValidator(attr) {
1382
- return import_zod3.z.string().datetime({ message: `${attr.label} must be a valid ISO date` });
2294
+ return import_zod4.z.string().datetime({ message: `${attr.label} must be a valid ISO date` });
1383
2295
  }
1384
2296
  function createPhoneValidator(_attr) {
1385
- return import_zod3.z.object({
1386
- countryCode: import_zod3.z.string().length(3),
1387
- phoneNumber: import_zod3.z.string().min(1)
2297
+ return import_zod4.z.object({
2298
+ countryCode: import_zod4.z.string().length(3),
2299
+ phoneNumber: import_zod4.z.string().min(1)
1388
2300
  });
1389
2301
  }
1390
2302
  function createCurrencyValidator(_attr) {
1391
- return import_zod3.z.object({
1392
- code: import_zod3.z.string().length(3),
1393
- value: import_zod3.z.number().min(0)
2303
+ return import_zod4.z.object({
2304
+ code: import_zod4.z.string().length(3),
2305
+ value: import_zod4.z.number().min(0)
1394
2306
  });
1395
2307
  }
1396
2308
  function createStatusValidator(attr) {
1397
2309
  const validValues = attr.options.map((opt) => opt.value);
1398
- return import_zod3.z.enum(validValues, {
1399
- errorMap: () => ({
1400
- message: `${attr.label} must be one of: ${validValues.join(", ")}`
1401
- })
2310
+ return import_zod4.z.enum(validValues, {
2311
+ error: `${attr.label} must be one of: ${validValues.join(", ")}`
1402
2312
  });
1403
2313
  }
1404
2314
  function createSelectValidator(attr) {
1405
2315
  const validValues = attr.options.map((opt) => opt.value);
1406
- return import_zod3.z.enum(validValues, {
1407
- errorMap: () => ({
1408
- message: `${attr.label} must be one of: ${validValues.join(", ")}`
1409
- })
2316
+ return import_zod4.z.enum(validValues, {
2317
+ error: `${attr.label} must be one of: ${validValues.join(", ")}`
1410
2318
  });
1411
2319
  }
1412
2320
  function createMultiselectValidator(attr) {
1413
2321
  const validValues = attr.options.map((opt) => opt.value);
1414
- return import_zod3.z.array(
1415
- import_zod3.z.enum(validValues, {
1416
- errorMap: () => ({
1417
- message: `Each value must be one of: ${validValues.join(", ")}`
1418
- })
2322
+ return import_zod4.z.array(
2323
+ import_zod4.z.enum(validValues, {
2324
+ error: `Each value must be one of: ${validValues.join(", ")}`
1419
2325
  })
1420
2326
  );
1421
2327
  }
1422
2328
  function createLocationValidator(_attr) {
1423
- return import_zod3.z.object({
1424
- address: import_zod3.z.string().optional(),
1425
- address2: import_zod3.z.string().optional(),
1426
- city: import_zod3.z.string().optional(),
1427
- state: import_zod3.z.string().optional(),
1428
- postalCode: import_zod3.z.string().optional(),
1429
- country: import_zod3.z.string().length(3).optional(),
1430
- latitude: import_zod3.z.number().optional(),
1431
- longitude: import_zod3.z.number().optional()
2329
+ return import_zod4.z.object({
2330
+ address: import_zod4.z.string().optional(),
2331
+ address2: import_zod4.z.string().optional(),
2332
+ city: import_zod4.z.string().optional(),
2333
+ state: import_zod4.z.string().optional(),
2334
+ postalCode: import_zod4.z.string().optional(),
2335
+ country: import_zod4.z.string().length(3).optional(),
2336
+ latitude: import_zod4.z.number().optional(),
2337
+ longitude: import_zod4.z.number().optional()
1432
2338
  });
1433
2339
  }
1434
2340
  function createTimestampValidator(_attr) {
1435
- return import_zod3.z.number().int().positive();
2341
+ return import_zod4.z.number().int().positive();
1436
2342
  }
1437
2343
  function createFileValidator(_attr) {
1438
- return import_zod3.z.string().uuid();
2344
+ return import_zod4.z.string().uuid();
1439
2345
  }
1440
2346
  function createUserValidator(_attr) {
1441
- return import_zod3.z.string().uuid();
2347
+ return import_zod4.z.string().uuid();
1442
2348
  }
1443
2349
  function createSingleRelationValidator(attr) {
1444
- const uuidSchema = import_zod3.z.string().uuid({
2350
+ const uuidSchema = import_zod4.z.string().uuid({
1445
2351
  message: `${attr.label} must be a valid record ID`
1446
2352
  });
1447
- return import_zod3.z.union([uuidSchema, import_zod3.z.null()]);
2353
+ return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
1448
2354
  }
1449
2355
  function createMultiRelationValidator(attr) {
1450
- const uuidSchema = import_zod3.z.string().uuid({
2356
+ const uuidSchema = import_zod4.z.string().uuid({
1451
2357
  message: `Each ${attr.label} item must be a valid record ID`
1452
2358
  });
1453
- let arraySchema = import_zod3.z.array(uuidSchema);
2359
+ let arraySchema = import_zod4.z.array(uuidSchema);
1454
2360
  if (attr.minItems !== void 0) {
1455
2361
  arraySchema = arraySchema.min(
1456
2362
  attr.minItems,
@@ -1472,7 +2378,7 @@ function createRelationValidator(attr) {
1472
2378
  return createSingleRelationValidator(attr);
1473
2379
  }
1474
2380
  function createRatingValidator(attr) {
1475
- let schema = import_zod3.z.number().int().min(0);
2381
+ let schema = import_zod4.z.number().int().min(0);
1476
2382
  if (attr.max !== void 0) {
1477
2383
  schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
1478
2384
  }
@@ -1511,7 +2417,7 @@ function createAttributeValidator(attr) {
1511
2417
  case "rating":
1512
2418
  return createRatingValidator(attr);
1513
2419
  default:
1514
- return import_zod3.z.unknown();
2420
+ return import_zod4.z.unknown();
1515
2421
  }
1516
2422
  }
1517
2423
  function createObjectValidator(objectDef) {
@@ -1523,7 +2429,7 @@ function createObjectValidator(objectDef) {
1523
2429
  }
1524
2430
  shape[attr.name] = validator;
1525
2431
  }
1526
- return import_zod3.z.object(shape);
2432
+ return import_zod4.z.object(shape);
1527
2433
  }
1528
2434
  function validateObject(objectDef, data) {
1529
2435
  const validator = createObjectValidator(objectDef);
@@ -1536,7 +2442,7 @@ function validateObject(objectDef, data) {
1536
2442
  }
1537
2443
  return {
1538
2444
  success: false,
1539
- errors: result.error.errors.map((err) => ({
2445
+ errors: result.error.issues.map((err) => ({
1540
2446
  path: err.path.map(String),
1541
2447
  message: err.message
1542
2448
  }))
@@ -1557,7 +2463,7 @@ function createDraftValidator(objectDef) {
1557
2463
  const validator = createAttributeValidator(attr).optional();
1558
2464
  shape[attr.name] = validator;
1559
2465
  }
1560
- return import_zod3.z.object(shape);
2466
+ return import_zod4.z.object(shape);
1561
2467
  }
1562
2468
  function validateDraft(objectDef, data) {
1563
2469
  const validator = createDraftValidator(objectDef);
@@ -1570,7 +2476,7 @@ function validateDraft(objectDef, data) {
1570
2476
  }
1571
2477
  return {
1572
2478
  success: false,
1573
- errors: result.error.errors.map((err) => ({
2479
+ errors: result.error.issues.map((err) => ({
1574
2480
  path: err.path.map(String),
1575
2481
  message: err.message
1576
2482
  }))
@@ -1617,9 +2523,12 @@ function computeRecordStatus(objectDef, data) {
1617
2523
 
1618
2524
  // src/runtime/services/object-schema.service.ts
1619
2525
  var ObjectSchemaService = class {
1620
- constructor(adapter, nativeRegistry) {
2526
+ constructor(adapter, nativeRegistry, options) {
1621
2527
  this.adapter = adapter;
1622
2528
  this.nativeRegistry = nativeRegistry;
2529
+ this.auditService = options?.auditService;
2530
+ this.userId = options?.userId;
2531
+ this.userEmail = options?.userEmail;
1623
2532
  }
1624
2533
  /**
1625
2534
  * Create a new custom object
@@ -1661,41 +2570,57 @@ var ObjectSchemaService = class {
1661
2570
  ...objectDef.metadata,
1662
2571
  icon: objectDef.icon
1663
2572
  };
1664
- const dbObject = await this.adapter.objects.create({
1665
- tenantId,
1666
- name: objectDef.name,
1667
- label: objectDef.label,
1668
- pluralLabel: objectDef.pluralLabel,
1669
- description: objectDef.description,
1670
- labelExpression: objectDef.labelExpression,
1671
- system: false,
1672
- // Always false for custom objects
1673
- metadata
1674
- });
1675
- const createdAttributes = [];
1676
- if (objectDef.attributes.length > 0) {
1677
- for (let i = 0; i < objectDef.attributes.length; i++) {
1678
- const attr = objectDef.attributes[i];
1679
- const config = validatedConfigs[i];
1680
- const dbAttr = await this.adapter.attributes.create({
1681
- objectId: dbObject.id,
1682
- name: attr.name,
1683
- label: attr.label,
1684
- type: attr.type,
1685
- required: attr.required ?? false,
1686
- unique: false,
1687
- system: false,
1688
- config
1689
- });
1690
- createdAttributes.push(this.convertDBAttributeToAttribute(dbAttr));
2573
+ const { dbObject, createdAttributes } = await this.adapter.transaction(async (tx) => {
2574
+ const dbObj = await tx.objects.create({
2575
+ tenantId,
2576
+ name: objectDef.name,
2577
+ label: objectDef.label,
2578
+ pluralLabel: objectDef.pluralLabel,
2579
+ description: objectDef.description,
2580
+ labelExpression: objectDef.labelExpression,
2581
+ system: false,
2582
+ // Always false for custom objects
2583
+ metadata
2584
+ });
2585
+ const attrs = [];
2586
+ if (objectDef.attributes.length > 0) {
2587
+ for (let i = 0; i < objectDef.attributes.length; i++) {
2588
+ const attr = objectDef.attributes[i];
2589
+ const config = validatedConfigs[i];
2590
+ const dbAttr = await tx.attributes.create({
2591
+ objectId: dbObj.id,
2592
+ name: attr.name,
2593
+ label: attr.label,
2594
+ type: attr.type,
2595
+ required: attr.required ?? false,
2596
+ unique: false,
2597
+ system: false,
2598
+ config
2599
+ });
2600
+ attrs.push(this.convertDBAttributeToAttribute(dbAttr));
2601
+ }
1691
2602
  }
1692
- }
1693
- return {
2603
+ return { dbObject: dbObj, createdAttributes: attrs };
2604
+ });
2605
+ const result = {
1694
2606
  ...objectDef,
1695
2607
  id: dbObject.id,
1696
2608
  attributes: createdAttributes,
1697
2609
  system: false
1698
2610
  };
2611
+ if (this.auditService && this.userId) {
2612
+ await this.auditService.logSchemaAction({
2613
+ action: "object.created",
2614
+ actorId: this.userId,
2615
+ actorEmail: this.userEmail,
2616
+ resourceType: "object",
2617
+ resourceId: dbObject.id,
2618
+ resourceLabel: dbObject.label,
2619
+ objectName: dbObject.name,
2620
+ objectId: dbObject.id
2621
+ });
2622
+ }
2623
+ return result;
1699
2624
  }
1700
2625
  /**
1701
2626
  * Add a custom attribute to an existing object
@@ -1731,6 +2656,18 @@ var ObjectSchemaService = class {
1731
2656
  // Always false for attributes added via API
1732
2657
  config
1733
2658
  });
2659
+ if (this.auditService && this.userId) {
2660
+ await this.auditService.logSchemaAction({
2661
+ action: "attribute.created",
2662
+ actorId: this.userId,
2663
+ actorEmail: this.userEmail,
2664
+ resourceType: "attribute",
2665
+ resourceId: dbAttr.id,
2666
+ resourceLabel: dbAttr.label,
2667
+ objectName: dbObject.name,
2668
+ objectId: dbObject.id
2669
+ });
2670
+ }
1734
2671
  return this.convertDBAttributeToAttribute(dbAttr);
1735
2672
  }
1736
2673
  /**
@@ -1774,11 +2711,39 @@ var ObjectSchemaService = class {
1774
2711
  if (dbAttr.type === "relation" && updates.targets) {
1775
2712
  await this.validateRelationTargets(mergedInput, tenantId);
1776
2713
  }
2714
+ const oldValues = {
2715
+ label: dbAttr.label,
2716
+ required: dbAttr.required
2717
+ };
1777
2718
  const updatedDbAttr = await this.adapter.attributes.update(attributeId, {
1778
2719
  label: updates.label,
1779
2720
  required: updates.required,
1780
2721
  config
1781
2722
  });
2723
+ if (this.auditService && this.userId) {
2724
+ const changes = buildAuditChanges(oldValues, updates, ["label", "required"]);
2725
+ if (changes.length > 0) {
2726
+ const dbObject = await this.adapter.objects.findById(dbAttr.objectId);
2727
+ await this.auditService.logSchemaAction({
2728
+ action: "attribute.updated",
2729
+ actorId: this.userId,
2730
+ actorEmail: this.userEmail,
2731
+ resourceType: "attribute",
2732
+ resourceId: attributeId,
2733
+ resourceLabel: updatedDbAttr.label,
2734
+ objectName: dbObject?.name,
2735
+ objectId: dbAttr.objectId,
2736
+ changes
2737
+ });
2738
+ }
2739
+ }
2740
+ if (updates.required !== void 0 && updates.required !== oldValues.required) {
2741
+ const schema = await this.getObjectSchema(dbAttr.objectId);
2742
+ await this.adapter.objectRecords.batchRefreshStatus(
2743
+ dbAttr.objectId,
2744
+ (values) => computeRecordStatus(schema, values)
2745
+ );
2746
+ }
1782
2747
  return this.convertDBAttributeToAttribute(updatedDbAttr);
1783
2748
  }
1784
2749
  /**
@@ -1797,7 +2762,27 @@ var ObjectSchemaService = class {
1797
2762
  `Cannot delete system attribute "${dbAttr.name}". System attributes are protected.`
1798
2763
  );
1799
2764
  }
2765
+ const dbObject = await this.adapter.objects.findById(dbAttr.objectId);
2766
+ if (dbObject?.labelExpression) {
2767
+ const usedAttributes = extractAttributeNames(dbObject.labelExpression);
2768
+ if (usedAttributes.includes(dbAttr.name)) {
2769
+ throw new AttributeInUseError(dbAttr.name, "labelExpression");
2770
+ }
2771
+ }
1800
2772
  await this.adapter.attributes.delete(attributeId);
2773
+ await this.adapter.objectRecords.removeAttributeData(dbAttr.objectId, dbAttr.name);
2774
+ if (this.auditService && this.userId) {
2775
+ await this.auditService.logSchemaAction({
2776
+ action: "attribute.deleted",
2777
+ actorId: this.userId,
2778
+ actorEmail: this.userEmail,
2779
+ resourceType: "attribute",
2780
+ resourceId: attributeId,
2781
+ resourceLabel: dbAttr.label,
2782
+ objectName: dbObject?.name,
2783
+ objectId: dbAttr.objectId
2784
+ });
2785
+ }
1801
2786
  }
1802
2787
  /**
1803
2788
  * List attributes for an object
@@ -1842,6 +2827,13 @@ var ObjectSchemaService = class {
1842
2827
  `Cannot modify system object "${dbObject.name}". System objects are protected.`
1843
2828
  );
1844
2829
  }
2830
+ const oldValues = {
2831
+ label: dbObject.label,
2832
+ pluralLabel: dbObject.pluralLabel,
2833
+ description: dbObject.description,
2834
+ labelExpression: dbObject.labelExpression,
2835
+ icon: dbObject.metadata?.icon
2836
+ };
1845
2837
  let metadata = dbObject.metadata;
1846
2838
  if (updates.icon !== void 0 || updates.metadata !== void 0) {
1847
2839
  metadata = {
@@ -1857,6 +2849,35 @@ var ObjectSchemaService = class {
1857
2849
  labelExpression: updates.labelExpression,
1858
2850
  metadata
1859
2851
  });
2852
+ if (this.auditService && this.userId) {
2853
+ const changes = buildAuditChanges(oldValues, { ...updates, icon: updates.icon }, [
2854
+ "label",
2855
+ "pluralLabel",
2856
+ "description",
2857
+ "labelExpression",
2858
+ "icon"
2859
+ ]);
2860
+ if (changes.length > 0) {
2861
+ await this.auditService.logSchemaAction({
2862
+ action: "object.updated",
2863
+ actorId: this.userId,
2864
+ actorEmail: this.userEmail,
2865
+ resourceType: "object",
2866
+ resourceId: objectId,
2867
+ resourceLabel: updatedDbObject.label,
2868
+ objectName: updatedDbObject.name,
2869
+ objectId,
2870
+ changes
2871
+ });
2872
+ }
2873
+ }
2874
+ if (updates.labelExpression !== void 0 && updates.labelExpression !== oldValues.labelExpression) {
2875
+ const newExpression = updates.labelExpression;
2876
+ await this.adapter.objectRecords.batchRefreshLabels(
2877
+ objectId,
2878
+ (values) => renderLabelExpression(newExpression, values)
2879
+ );
2880
+ }
1860
2881
  const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
1861
2882
  return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
1862
2883
  }
@@ -1872,7 +2893,7 @@ var ObjectSchemaService = class {
1872
2893
  * await service.deleteObject("obj-123");
1873
2894
  * ```
1874
2895
  */
1875
- async deleteObject(objectId) {
2896
+ async deleteObject(objectId, tenantId) {
1876
2897
  const dbObject = await this.adapter.objects.findById(objectId);
1877
2898
  if (!dbObject) {
1878
2899
  throw new Error(`Object with id "${objectId}" not found`);
@@ -1882,7 +2903,29 @@ var ObjectSchemaService = class {
1882
2903
  `Cannot delete system object "${dbObject.name}". System objects are protected.`
1883
2904
  );
1884
2905
  }
2906
+ const effectiveTenantId = tenantId ?? dbObject.tenantId;
2907
+ if (effectiveTenantId) {
2908
+ const referencingObjects = await this.findObjectsWithRelationTo(
2909
+ dbObject.name,
2910
+ effectiveTenantId
2911
+ );
2912
+ if (referencingObjects.length > 0) {
2913
+ throw new ObjectReferencedError(dbObject.name, referencingObjects);
2914
+ }
2915
+ }
1885
2916
  await this.adapter.objects.delete(objectId);
2917
+ if (this.auditService && this.userId) {
2918
+ await this.auditService.logSchemaAction({
2919
+ action: "object.deleted",
2920
+ actorId: this.userId,
2921
+ actorEmail: this.userEmail,
2922
+ resourceType: "object",
2923
+ resourceId: objectId,
2924
+ resourceLabel: dbObject.label,
2925
+ objectName: dbObject.name,
2926
+ objectId
2927
+ });
2928
+ }
1886
2929
  }
1887
2930
  /**
1888
2931
  * Validate attribute name format (variable identifier)
@@ -1975,148 +3018,654 @@ var ObjectSchemaService = class {
1975
3018
  return schemas;
1976
3019
  }
1977
3020
  /**
1978
- * Merge native object from registry with custom attributes from DB
1979
- * @internal
3021
+ * Merge native object from registry with custom attributes from DB
3022
+ * @internal
3023
+ */
3024
+ mergeNativeObject(dbObject, dbAttributes) {
3025
+ const nativeObject = this.nativeRegistry.getByName(dbObject.name);
3026
+ if (!nativeObject) {
3027
+ throw new Error(
3028
+ `Native object "${dbObject.name}" not found in registry. Make sure to register it before querying.`
3029
+ );
3030
+ }
3031
+ const systemAttributesFromDB = dbAttributes.filter((attr) => attr.system);
3032
+ const customAttributes = dbAttributes.filter((attr) => !attr.system);
3033
+ const dbAttrByName = new Map(systemAttributesFromDB.map((attr) => [attr.name, attr]));
3034
+ const systemAttrs = nativeObject.attributes.map((registryAttr) => {
3035
+ const dbAttr = dbAttrByName.get(registryAttr.name);
3036
+ if (dbAttr) {
3037
+ return {
3038
+ ...registryAttr,
3039
+ id: dbAttr.id
3040
+ };
3041
+ }
3042
+ return registryAttr;
3043
+ });
3044
+ const customAttrs = customAttributes.map(
3045
+ (dbAttr) => this.convertDBAttributeToAttribute(dbAttr)
3046
+ );
3047
+ return {
3048
+ ...nativeObject,
3049
+ id: dbObject.id,
3050
+ attributes: [
3051
+ ...systemAttrs,
3052
+ // System attributes from registry (with DB IDs if synced)
3053
+ ...customAttrs
3054
+ // Custom attributes from DB
3055
+ ]
3056
+ };
3057
+ }
3058
+ /**
3059
+ * Convert DB object + attributes to ObjectDefinition
3060
+ * @internal
3061
+ */
3062
+ convertDBObjectToDefinition(dbObject, dbAttributes) {
3063
+ const attributes = dbAttributes.map(
3064
+ (dbAttr) => this.convertDBAttributeToAttribute(dbAttr)
3065
+ );
3066
+ return {
3067
+ id: dbObject.id,
3068
+ name: dbObject.name,
3069
+ label: dbObject.label,
3070
+ pluralLabel: dbObject.pluralLabel,
3071
+ description: dbObject.description,
3072
+ icon: dbObject.metadata?.icon,
3073
+ labelExpression: dbObject.labelExpression,
3074
+ attributes,
3075
+ system: dbObject.system,
3076
+ metadata: dbObject.metadata
3077
+ };
3078
+ }
3079
+ /**
3080
+ * Validate attribute input and return parsed config
3081
+ * Validates: name format, config schema, relation targets
3082
+ * @internal
3083
+ */
3084
+ async validateAttributeInput(attribute, tenantId) {
3085
+ this.validateAttributeName(attribute.name);
3086
+ const config = this.buildAttributeConfig(attribute);
3087
+ if (attribute.type === "relation") {
3088
+ await this.validateRelationTargets(attribute, tenantId);
3089
+ }
3090
+ return config;
3091
+ }
3092
+ /**
3093
+ * Build and validate attribute config from input
3094
+ * Uses Zod schema to validate and strip unknown properties
3095
+ * @internal
3096
+ */
3097
+ buildAttributeConfig(attribute) {
3098
+ const DB_COLUMN_FIELDS = ["name", "label", "type", "required", "unique"];
3099
+ const configInput = {};
3100
+ for (const [key, value] of Object.entries(attribute)) {
3101
+ if (!DB_COLUMN_FIELDS.includes(key)) {
3102
+ configInput[key] = value;
3103
+ }
3104
+ }
3105
+ try {
3106
+ return parseAttributeConfig(attribute.type, configInput);
3107
+ } catch (error) {
3108
+ if (error instanceof Error) {
3109
+ throw new Error(`Invalid config for ${attribute.type} attribute: ${error.message}`);
3110
+ }
3111
+ throw error;
3112
+ }
3113
+ }
3114
+ /**
3115
+ * Check if an object exists (in tenant, system, or native registry)
3116
+ * @internal
3117
+ */
3118
+ async objectExists(name, tenantId) {
3119
+ if (tenantId) {
3120
+ const obj = await this.adapter.objects.findByName(tenantId, name);
3121
+ if (obj) return true;
3122
+ }
3123
+ const systemObj = await this.adapter.objects.findSystemByName(name);
3124
+ if (systemObj) return true;
3125
+ return this.nativeRegistry.getByName(name) !== void 0;
3126
+ }
3127
+ /**
3128
+ * Validate that relation target objects exist
3129
+ * @internal
3130
+ */
3131
+ async validateRelationTargets(attribute, tenantId) {
3132
+ const targets = attribute.targets;
3133
+ if (!targets || targets.length === 0) {
3134
+ throw new Error("Relation attribute must have at least one target object");
3135
+ }
3136
+ const cardinality = attribute.cardinality;
3137
+ const isValidCardinality = cardinality === "one" || cardinality === "many";
3138
+ if (!isValidCardinality) {
3139
+ throw new Error("Relation attribute must specify cardinality: 'one' or 'many'");
3140
+ }
3141
+ for (const target of targets) {
3142
+ if (!target.object) {
3143
+ throw new Error("Each relation target must specify an object name");
3144
+ }
3145
+ const exists = await this.objectExists(target.object, tenantId);
3146
+ if (!exists) {
3147
+ throw new Error(
3148
+ `Relation target object "${target.object}" does not exist. Make sure the object is created before adding a relation to it.`
3149
+ );
3150
+ }
3151
+ }
3152
+ }
3153
+ /**
3154
+ * Find objects that have a relation attribute targeting the given object.
3155
+ * Used to prevent deletion of objects that are relation targets.
3156
+ * @internal
3157
+ */
3158
+ async findObjectsWithRelationTo(targetObjectName, tenantId) {
3159
+ const allObjects = await this.adapter.objects.list(tenantId);
3160
+ const referencing = [];
3161
+ for (const obj of allObjects) {
3162
+ if (obj.name === targetObjectName) continue;
3163
+ const attrs = await this.adapter.attributes.findByObjectId(obj.id);
3164
+ const hasRelationToTarget = attrs.some((attr) => {
3165
+ if (attr.type !== "relation") return false;
3166
+ const config = attr.config;
3167
+ return config?.targets?.some((t) => t.object === targetObjectName) ?? false;
3168
+ });
3169
+ if (hasRelationToTarget) {
3170
+ referencing.push(obj.name);
3171
+ }
3172
+ }
3173
+ return referencing;
3174
+ }
3175
+ /**
3176
+ * Convert DB attribute to Attribute type
3177
+ * @internal
3178
+ */
3179
+ convertDBAttributeToAttribute(dbAttr) {
3180
+ const baseAttr = {
3181
+ ...dbAttr.config,
3182
+ id: dbAttr.id,
3183
+ name: dbAttr.name,
3184
+ label: dbAttr.label,
3185
+ type: dbAttr.type,
3186
+ required: dbAttr.required,
3187
+ system: dbAttr.system,
3188
+ unique: dbAttr.unique
3189
+ };
3190
+ return baseAttr;
3191
+ }
3192
+ };
3193
+
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
+ // src/runtime/services/permission.service.ts
3254
+ var PermissionService = class {
3255
+ constructor(adapter, tenantId, options) {
3256
+ this.adapter = adapter;
3257
+ this.tenantId = tenantId;
3258
+ this.cache = /* @__PURE__ */ new Map();
3259
+ if (!adapter.permissions) {
3260
+ throw new Error(
3261
+ "PermissionService requires a DatabaseAdapter with permissions repository. Make sure your adapter implements the permissions property."
3262
+ );
3263
+ }
3264
+ this.permissionsRepo = adapter.permissions;
3265
+ this.cacheTtlMs = options?.cacheTtlMs ?? 6e4;
3266
+ this.maxCacheSize = options?.maxCacheSize ?? 1e4;
3267
+ this.auditService = options?.auditService;
3268
+ this.userId = options?.userId;
3269
+ this.userEmail = options?.userEmail;
3270
+ }
3271
+ // ============================================================================
3272
+ // PERMISSION CHECKS
3273
+ // ============================================================================
3274
+ /**
3275
+ * Check if user can perform an action on an object.
3276
+ *
3277
+ * @param userProfileId - User profile ID to check
3278
+ * @param objectName - Object name to check access for
3279
+ * @param action - Action to check (read, create, update, delete)
3280
+ * @returns true if user has permission, false otherwise
3281
+ */
3282
+ async canAccessObject(userProfileId, objectName, action) {
3283
+ const permissions = await this.getEffectivePermissions(userProfileId);
3284
+ if (permissions.isAdmin) {
3285
+ return true;
3286
+ }
3287
+ const wildcardPerms = permissions.objectPermissions["*"];
3288
+ if (wildcardPerms?.includes(action)) {
3289
+ return true;
3290
+ }
3291
+ const objectPerms = permissions.objectPermissions[objectName];
3292
+ return objectPerms?.includes(action) ?? false;
3293
+ }
3294
+ /**
3295
+ * Check if user can access an object, throw ForbiddenError if not.
3296
+ *
3297
+ * @param userProfileId - User profile ID to check
3298
+ * @param objectName - Object name to check access for
3299
+ * @param action - Action to check
3300
+ * @throws ForbiddenError if user lacks permission
3301
+ */
3302
+ async checkObjectAccess(userProfileId, objectName, action) {
3303
+ const canAccess = await this.canAccessObject(userProfileId, objectName, action);
3304
+ if (!canAccess) {
3305
+ throw new ForbiddenError(objectName, action, userProfileId);
3306
+ }
3307
+ }
3308
+ /**
3309
+ * Get permissions for a specific object as boolean flags.
3310
+ * Optimized to fetch permissions once and compute all flags.
3311
+ *
3312
+ * @param userProfileId - User profile ID to check
3313
+ * @param objectName - Object name to check
3314
+ * @returns Object with canRead, canCreate, canUpdate, canDelete flags
3315
+ */
3316
+ async getObjectPermissions(userProfileId, objectName) {
3317
+ const permissions = await this.getEffectivePermissions(userProfileId);
3318
+ if (permissions.isAdmin) {
3319
+ return { canRead: true, canCreate: true, canUpdate: true, canDelete: true };
3320
+ }
3321
+ const wildcardPerms = permissions.objectPermissions["*"] ?? [];
3322
+ const objectPerms = permissions.objectPermissions[objectName] ?? [];
3323
+ const allPerms = /* @__PURE__ */ new Set([...wildcardPerms, ...objectPerms]);
3324
+ return {
3325
+ canRead: allPerms.has("read"),
3326
+ canCreate: allPerms.has("create"),
3327
+ canUpdate: allPerms.has("update"),
3328
+ canDelete: allPerms.has("delete")
3329
+ };
3330
+ }
3331
+ // ============================================================================
3332
+ // SYSTEM PERMISSION CHECKS
3333
+ // ============================================================================
3334
+ /**
3335
+ * Check if user can perform an action on a system resource.
3336
+ *
3337
+ * @param userProfileId - User profile ID to check
3338
+ * @param resource - System resource (users, schema, roles, settings)
3339
+ * @param action - Action to check (read, create, update, delete)
3340
+ * @returns true if user has permission, false otherwise
3341
+ */
3342
+ async canAccessSystem(userProfileId, resource, action) {
3343
+ const permissions = await this.getEffectivePermissions(userProfileId);
3344
+ if (permissions.isAdmin) {
3345
+ return true;
3346
+ }
3347
+ const wildcardPerms = permissions.systemPermissions?.["*"];
3348
+ if (wildcardPerms?.includes(action)) {
3349
+ return true;
3350
+ }
3351
+ const resourcePerms = permissions.systemPermissions?.[resource];
3352
+ return resourcePerms?.includes(action) ?? false;
3353
+ }
3354
+ /**
3355
+ * Check if user can access a system resource, throw ForbiddenError if not.
3356
+ *
3357
+ * @param userProfileId - User profile ID to check
3358
+ * @param resource - System resource to check access for
3359
+ * @param action - Action to check
3360
+ * @throws ForbiddenError if user lacks permission
3361
+ */
3362
+ async checkSystemAccess(userProfileId, resource, action) {
3363
+ const canAccess = await this.canAccessSystem(userProfileId, resource, action);
3364
+ if (!canAccess) {
3365
+ throw new ForbiddenError(`_${resource}`, action, userProfileId);
3366
+ }
3367
+ }
3368
+ /**
3369
+ * Get permissions for a specific system resource as boolean flags.
3370
+ * Optimized to fetch permissions once and compute all flags.
3371
+ *
3372
+ * @param userProfileId - User profile ID to check
3373
+ * @param resource - System resource to check
3374
+ * @returns Object with canRead, canCreate, canUpdate, canDelete flags
3375
+ */
3376
+ async getSystemPermissions(userProfileId, resource) {
3377
+ const permissions = await this.getEffectivePermissions(userProfileId);
3378
+ if (permissions.isAdmin) {
3379
+ return { canRead: true, canCreate: true, canUpdate: true, canDelete: true };
3380
+ }
3381
+ const wildcardPerms = permissions.systemPermissions?.["*"] ?? [];
3382
+ const resourcePerms = permissions.systemPermissions?.[resource] ?? [];
3383
+ const allPerms = /* @__PURE__ */ new Set([...wildcardPerms, ...resourcePerms]);
3384
+ return {
3385
+ canRead: allPerms.has("read"),
3386
+ canCreate: allPerms.has("create"),
3387
+ canUpdate: allPerms.has("update"),
3388
+ canDelete: allPerms.has("delete")
3389
+ };
3390
+ }
3391
+ // ============================================================================
3392
+ // EFFECTIVE PERMISSIONS
3393
+ // ============================================================================
3394
+ /**
3395
+ * Get effective permissions for a user.
3396
+ * Results are cached with TTL and size limit.
3397
+ *
3398
+ * @param userProfileId - User profile ID
3399
+ * @returns Merged permissions from all user's roles
3400
+ */
3401
+ async getEffectivePermissions(userProfileId) {
3402
+ const cacheKey = this.getCacheKey(userProfileId);
3403
+ const cached = this.cache.get(cacheKey);
3404
+ if (cached && cached.expiresAt > Date.now()) {
3405
+ return cached.permissions;
3406
+ }
3407
+ const permissions = await this.permissionsRepo.getEffectivePermissions(
3408
+ userProfileId,
3409
+ this.tenantId
3410
+ );
3411
+ if (this.cache.size >= this.maxCacheSize) {
3412
+ const firstKey = this.cache.keys().next().value;
3413
+ if (firstKey) {
3414
+ this.cache.delete(firstKey);
3415
+ }
3416
+ }
3417
+ this.cache.set(cacheKey, {
3418
+ permissions,
3419
+ expiresAt: Date.now() + this.cacheTtlMs
3420
+ });
3421
+ return permissions;
3422
+ }
3423
+ // ============================================================================
3424
+ // CACHE MANAGEMENT
3425
+ // ============================================================================
3426
+ /**
3427
+ * Invalidate cached permissions for a specific user.
3428
+ * Call this after role/permission changes.
3429
+ */
3430
+ invalidateCache(userProfileId) {
3431
+ this.cache.delete(this.getCacheKey(userProfileId));
3432
+ }
3433
+ /**
3434
+ * Invalidate all cached permissions.
3435
+ * Call this after bulk role/permission changes.
3436
+ */
3437
+ invalidateAllCache() {
3438
+ this.cache.clear();
3439
+ }
3440
+ /**
3441
+ * Get current cache size (for monitoring).
3442
+ */
3443
+ getCacheSize() {
3444
+ return this.cache.size;
3445
+ }
3446
+ getCacheKey(userProfileId) {
3447
+ return `${this.tenantId}:${userProfileId}`;
3448
+ }
3449
+ // ============================================================================
3450
+ // ROLE MANAGEMENT
3451
+ // ============================================================================
3452
+ /**
3453
+ * Get all roles for the current tenant.
3454
+ */
3455
+ async getRoles() {
3456
+ return await this.permissionsRepo.getRolesByTenant(this.tenantId);
3457
+ }
3458
+ /**
3459
+ * Get a role by ID.
3460
+ */
3461
+ async getRoleById(roleId) {
3462
+ return await this.permissionsRepo.getRoleById(roleId);
3463
+ }
3464
+ /**
3465
+ * Create a new role.
3466
+ */
3467
+ async createRole(input) {
3468
+ const role = await this.permissionsRepo.createRole({
3469
+ ...input,
3470
+ tenantId: this.tenantId
3471
+ });
3472
+ if (this.auditService && this.userId) {
3473
+ await this.auditService.logRoleAction({
3474
+ action: "role.created",
3475
+ actorId: this.userId,
3476
+ actorEmail: this.userEmail,
3477
+ roleId: role.id,
3478
+ roleLabel: role.label
3479
+ });
3480
+ }
3481
+ return role;
3482
+ }
3483
+ /**
3484
+ * Update a role.
3485
+ */
3486
+ async updateRole(roleId, updates) {
3487
+ const oldRole = await this.permissionsRepo.getRoleById(roleId);
3488
+ const role = await this.permissionsRepo.updateRole(roleId, updates);
3489
+ if (this.auditService && this.userId && oldRole) {
3490
+ const changes = [];
3491
+ if (updates.label !== void 0 && updates.label !== oldRole.label) {
3492
+ changes.push({ field: "label", oldValue: oldRole.label, newValue: updates.label });
3493
+ }
3494
+ if (updates.description !== void 0 && updates.description !== oldRole.description) {
3495
+ changes.push({
3496
+ field: "description",
3497
+ oldValue: oldRole.description,
3498
+ newValue: updates.description
3499
+ });
3500
+ }
3501
+ if (changes.length > 0) {
3502
+ await this.auditService.logRoleAction({
3503
+ action: "role.updated",
3504
+ actorId: this.userId,
3505
+ actorEmail: this.userEmail,
3506
+ roleId: role.id,
3507
+ roleLabel: role.label,
3508
+ changes
3509
+ });
3510
+ }
3511
+ }
3512
+ return role;
3513
+ }
3514
+ /**
3515
+ * Delete a role.
3516
+ */
3517
+ async deleteRole(roleId) {
3518
+ const role = await this.permissionsRepo.getRoleById(roleId);
3519
+ await this.permissionsRepo.deleteRole(roleId);
3520
+ this.invalidateAllCache();
3521
+ if (this.auditService && this.userId && role) {
3522
+ await this.auditService.logRoleAction({
3523
+ action: "role.deleted",
3524
+ actorId: this.userId,
3525
+ actorEmail: this.userEmail,
3526
+ roleId,
3527
+ roleLabel: role.label
3528
+ });
3529
+ }
3530
+ }
3531
+ // ============================================================================
3532
+ // PERMISSION MANAGEMENT
3533
+ // ============================================================================
3534
+ /**
3535
+ * Get all permissions for a role.
1980
3536
  */
1981
- mergeNativeObject(dbObject, dbAttributes) {
1982
- const nativeObject = this.nativeRegistry.getByName(dbObject.name);
1983
- if (!nativeObject) {
1984
- throw new Error(
1985
- `Native object "${dbObject.name}" not found in registry. Make sure to register it before querying.`
1986
- );
3537
+ async getPermissionsByRole(roleId) {
3538
+ return await this.permissionsRepo.getPermissionsByRole(roleId);
3539
+ }
3540
+ /**
3541
+ * Set permissions for a role (replaces existing).
3542
+ */
3543
+ async setPermissions(roleId, permissions) {
3544
+ await this.permissionsRepo.setPermissions(roleId, permissions);
3545
+ this.invalidateAllCache();
3546
+ if (this.auditService && this.userId) {
3547
+ const role = await this.permissionsRepo.getRoleById(roleId);
3548
+ await this.auditService.logRoleAction({
3549
+ action: "role.updated",
3550
+ actorId: this.userId,
3551
+ actorEmail: this.userEmail,
3552
+ roleId,
3553
+ roleLabel: role?.label ?? roleId,
3554
+ metadata: { permissionsUpdated: true, permissionCount: permissions.length }
3555
+ });
1987
3556
  }
1988
- const systemAttributesFromDB = dbAttributes.filter((attr) => attr.system);
1989
- const customAttributes = dbAttributes.filter((attr) => !attr.system);
1990
- const dbAttrByName = new Map(systemAttributesFromDB.map((attr) => [attr.name, attr]));
1991
- const systemAttrs = nativeObject.attributes.map((registryAttr) => {
1992
- const dbAttr = dbAttrByName.get(registryAttr.name);
1993
- if (dbAttr) {
1994
- return {
1995
- ...registryAttr,
1996
- id: dbAttr.id
1997
- };
1998
- }
1999
- return registryAttr;
2000
- });
2001
- const customAttrs = customAttributes.map(
2002
- (dbAttr) => this.convertDBAttributeToAttribute(dbAttr)
2003
- );
2004
- return {
2005
- ...nativeObject,
2006
- id: dbObject.id,
2007
- attributes: [
2008
- ...systemAttrs,
2009
- // System attributes from registry (with DB IDs if synced)
2010
- ...customAttrs
2011
- // Custom attributes from DB
2012
- ]
2013
- };
2014
3557
  }
3558
+ // ============================================================================
3559
+ // USER ROLE MANAGEMENT
3560
+ // ============================================================================
2015
3561
  /**
2016
- * Convert DB object + attributes to ObjectDefinition
2017
- * @internal
3562
+ * Get all roles assigned to a user.
2018
3563
  */
2019
- convertDBObjectToDefinition(dbObject, dbAttributes) {
2020
- const attributes = dbAttributes.map(
2021
- (dbAttr) => this.convertDBAttributeToAttribute(dbAttr)
2022
- );
2023
- return {
2024
- id: dbObject.id,
2025
- name: dbObject.name,
2026
- label: dbObject.label,
2027
- pluralLabel: dbObject.pluralLabel,
2028
- description: dbObject.description,
2029
- icon: dbObject.metadata?.icon,
2030
- labelExpression: dbObject.labelExpression,
2031
- attributes,
2032
- system: dbObject.system,
2033
- metadata: dbObject.metadata
2034
- };
3564
+ async getUserRoles(userProfileId) {
3565
+ return await this.permissionsRepo.getUserRoles(userProfileId, this.tenantId);
2035
3566
  }
2036
3567
  /**
2037
- * Validate attribute input and return parsed config
2038
- * Validates: name format, config schema, relation targets
2039
- * @internal
3568
+ * Assign a role to a user.
2040
3569
  */
2041
- async validateAttributeInput(attribute, tenantId) {
2042
- this.validateAttributeName(attribute.name);
2043
- const config = this.buildAttributeConfig(attribute);
2044
- if (attribute.type === "relation") {
2045
- await this.validateRelationTargets(attribute, tenantId);
3570
+ async assignRole(userProfileId, roleId, assignedBy) {
3571
+ const result = await this.permissionsRepo.assignRole({
3572
+ userProfileId,
3573
+ roleId,
3574
+ tenantId: this.tenantId,
3575
+ assignedBy
3576
+ });
3577
+ this.invalidateCache(userProfileId);
3578
+ if (this.auditService && this.userId) {
3579
+ const role = await this.permissionsRepo.getRoleById(roleId);
3580
+ await this.auditService.logRoleAction({
3581
+ action: "role.assigned",
3582
+ actorId: this.userId,
3583
+ actorEmail: this.userEmail,
3584
+ roleId,
3585
+ roleLabel: role?.label ?? roleId,
3586
+ targetUserId: userProfileId
3587
+ });
2046
3588
  }
2047
- return config;
3589
+ return result;
2048
3590
  }
2049
3591
  /**
2050
- * Build and validate attribute config from input
2051
- * Uses Zod schema to validate and strip unknown properties
2052
- * @internal
3592
+ * Revoke a role from a user.
2053
3593
  */
2054
- buildAttributeConfig(attribute) {
2055
- const {
2056
- name: _,
2057
- label: __,
2058
- type: ___,
2059
- required: ____,
2060
- unique: _____,
2061
- ...configInput
2062
- } = attribute;
2063
- try {
2064
- return parseAttributeConfig(attribute.type, configInput);
2065
- } catch (error) {
2066
- if (error instanceof Error) {
2067
- throw new Error(`Invalid config for ${attribute.type} attribute: ${error.message}`);
2068
- }
2069
- throw error;
3594
+ async revokeRole(userProfileId, roleId) {
3595
+ const role = await this.permissionsRepo.getRoleById(roleId);
3596
+ await this.permissionsRepo.revokeRole(userProfileId, roleId, this.tenantId);
3597
+ this.invalidateCache(userProfileId);
3598
+ if (this.auditService && this.userId) {
3599
+ await this.auditService.logRoleAction({
3600
+ action: "role.revoked",
3601
+ actorId: this.userId,
3602
+ actorEmail: this.userEmail,
3603
+ roleId,
3604
+ roleLabel: role?.label ?? roleId,
3605
+ targetUserId: userProfileId
3606
+ });
2070
3607
  }
2071
3608
  }
3609
+ // ============================================================================
3610
+ // INITIALIZATION
3611
+ // ============================================================================
2072
3612
  /**
2073
- * Validate that relation target objects exist
2074
- * @internal
3613
+ * Initialize default roles for the tenant if they don't exist.
3614
+ *
3615
+ * Creates the following roles with their default permissions:
3616
+ * - **admin**: Full access to all system resources and objects
3617
+ * - **member**: No system access, can read/create/update objects (no delete)
3618
+ * - **guest**: No system access, read-only access to objects
3619
+ *
3620
+ * This method is idempotent - it only creates roles that don't already exist.
3621
+ * Should be called during tenant bootstrap or on first admin login.
3622
+ *
3623
+ * @example
3624
+ * ```typescript
3625
+ * const service = new PermissionService(adapter, tenantId);
3626
+ * await service.initializeDefaultRoles();
3627
+ * ```
2075
3628
  */
2076
- async validateRelationTargets(attribute, tenantId) {
2077
- const targets = attribute.targets;
2078
- if (!targets || targets.length === 0) {
2079
- throw new Error("Relation attribute must have at least one target object");
2080
- }
2081
- const cardinality = attribute.cardinality;
2082
- if (!(cardinality && ["one", "many"].includes(cardinality))) {
2083
- throw new Error("Relation attribute must specify cardinality: 'one' or 'many'");
2084
- }
2085
- for (const target of targets) {
2086
- if (!target.object) {
2087
- throw new Error("Each relation target must specify an object name");
3629
+ async initializeDefaultRoles() {
3630
+ const {
3631
+ DEFAULT_ROLES: DEFAULT_ROLES2,
3632
+ DEFAULT_ROLE_LABELS: DEFAULT_ROLE_LABELS2,
3633
+ DEFAULT_ROLE_DESCRIPTIONS: DEFAULT_ROLE_DESCRIPTIONS2,
3634
+ DEFAULT_ROLE_PERMISSIONS: DEFAULT_ROLE_PERMISSIONS2
3635
+ } = await Promise.resolve().then(() => (init_default_roles(), default_roles_exports));
3636
+ const existingRoles = await this.getRoles();
3637
+ const existingRoleNames = new Set(existingRoles.map((r) => r.name));
3638
+ for (const roleName of Object.values(DEFAULT_ROLES2)) {
3639
+ if (existingRoleNames.has(roleName)) {
3640
+ continue;
2088
3641
  }
2089
- let targetObject = tenantId ? await this.adapter.objects.findByName(tenantId, target.object) : null;
2090
- if (!targetObject) {
2091
- targetObject = await this.adapter.objects.findSystemByName(target.object);
3642
+ const role = await this.permissionsRepo.createRole({
3643
+ tenantId: this.tenantId,
3644
+ name: roleName,
3645
+ label: DEFAULT_ROLE_LABELS2[roleName],
3646
+ description: DEFAULT_ROLE_DESCRIPTIONS2[roleName]
3647
+ });
3648
+ const permConfig = DEFAULT_ROLE_PERMISSIONS2[roleName];
3649
+ const permissionInputs = [];
3650
+ for (const perm of permConfig.system) {
3651
+ permissionInputs.push({
3652
+ scope: "system",
3653
+ target: perm.target,
3654
+ actions: perm.actions
3655
+ });
2092
3656
  }
2093
- if (!targetObject) {
2094
- const nativeObject = this.nativeRegistry.getByName(target.object);
2095
- if (!nativeObject) {
2096
- throw new Error(
2097
- `Relation target object "${target.object}" does not exist. Make sure the object is created before adding a relation to it.`
2098
- );
2099
- }
3657
+ for (const perm of permConfig.object) {
3658
+ permissionInputs.push({
3659
+ scope: "object",
3660
+ target: perm.target,
3661
+ actions: perm.actions
3662
+ });
3663
+ }
3664
+ if (permissionInputs.length > 0) {
3665
+ await this.setPermissions(role.id, permissionInputs);
2100
3666
  }
2101
3667
  }
2102
3668
  }
2103
- /**
2104
- * Convert DB attribute to Attribute type
2105
- * @internal
2106
- */
2107
- convertDBAttributeToAttribute(dbAttr) {
2108
- const baseAttr = {
2109
- ...dbAttr.config,
2110
- id: dbAttr.id,
2111
- name: dbAttr.name,
2112
- label: dbAttr.label,
2113
- type: dbAttr.type,
2114
- required: dbAttr.required,
2115
- system: dbAttr.system,
2116
- unique: dbAttr.unique
2117
- };
2118
- return baseAttr;
2119
- }
2120
3669
  };
2121
3670
 
2122
3671
  // src/native/registry.ts
@@ -2439,8 +3988,7 @@ var RelationService = class {
2439
3988
  objectName: objectSchema.name,
2440
3989
  objectLabel: objectSchema.label,
2441
3990
  objectIcon: objectSchema.icon,
2442
- label,
2443
- data: record.values
3991
+ label
2444
3992
  });
2445
3993
  }
2446
3994
  }
@@ -2455,40 +4003,58 @@ var RelationService = class {
2455
4003
  * Resolve record IDs to their display labels
2456
4004
  * Useful for displaying current values in the UI
2457
4005
  *
4006
+ * Uses batch fetching for performance - fetches all records in one query,
4007
+ * then groups by objectId to minimize schema lookups.
4008
+ *
2458
4009
  * @param ids - Record IDs to resolve
2459
- * @param tenantId - Tenant ID for multi-tenant isolation
4010
+ * @param _tenantId - Tenant ID for multi-tenant isolation (unused but kept for API compatibility)
4011
+ * @param attributeId - Relation attribute ID to use its displayTemplate for label rendering
2460
4012
  *
2461
4013
  * @example
2462
4014
  * ```typescript
2463
- * const resolved = await relationService.resolveIds(["rec-1", "rec-2"], "tenant-1");
4015
+ * const resolved = await relationService.resolveIds(["rec-1", "rec-2"], "tenant-1", "attr-123");
2464
4016
  * // [{ id: "rec-1", label: "Nike Air Max", objectName: "products", ... }]
2465
4017
  * ```
2466
4018
  */
2467
- async resolveIds(ids, tenantId) {
4019
+ async resolveIds(ids, _tenantId, attributeId) {
2468
4020
  if (!ids || ids.length === 0) {
2469
4021
  return [];
2470
4022
  }
2471
- const recordService = new RecordService(this.adapter, tenantId);
4023
+ const attribute = await this.findAttributeById(attributeId);
4024
+ const records = await this.adapter.objectRecords.findByIds(ids);
4025
+ if (records.length === 0) {
4026
+ return [];
4027
+ }
4028
+ const recordsByObjectId = /* @__PURE__ */ new Map();
4029
+ for (const record of records) {
4030
+ const existing = recordsByObjectId.get(record.objectId) ?? [];
4031
+ existing.push(record);
4032
+ recordsByObjectId.set(record.objectId, existing);
4033
+ }
2472
4034
  const resolved = [];
2473
- for (const id of ids) {
2474
- const record = await recordService.getRecord(id);
2475
- if (!record) {
2476
- continue;
2477
- }
2478
- const objectSchema = await this.schemaService.getObjectSchema(record.objectId);
4035
+ for (const [objectId, objectRecords] of recordsByObjectId) {
4036
+ const objectSchema = await this.schemaService.getObjectSchema(objectId);
2479
4037
  if (!objectSchema) {
2480
4038
  continue;
2481
4039
  }
2482
- const label = renderLabelExpression(objectSchema.labelExpression, record.values);
2483
- resolved.push({
2484
- id: record.id,
2485
- objectId: record.objectId,
2486
- objectName: objectSchema.name,
2487
- objectLabel: objectSchema.label,
2488
- objectIcon: objectSchema.icon,
2489
- label,
2490
- data: record.values
2491
- });
4040
+ let template = objectSchema.labelExpression;
4041
+ if (attribute?.targets) {
4042
+ const targetConfig = attribute.targets.find((t) => t.object === objectSchema.name);
4043
+ if (targetConfig?.displayTemplate) {
4044
+ template = targetConfig.displayTemplate;
4045
+ }
4046
+ }
4047
+ for (const record of objectRecords) {
4048
+ const label = renderLabelExpression(template, record.values);
4049
+ resolved.push({
4050
+ id: record.id,
4051
+ objectId: record.objectId,
4052
+ objectName: objectSchema.name,
4053
+ objectLabel: objectSchema.label,
4054
+ objectIcon: objectSchema.icon,
4055
+ label
4056
+ });
4057
+ }
2492
4058
  }
2493
4059
  return resolved;
2494
4060
  }
@@ -2515,6 +4081,70 @@ var RecordService = class {
2515
4081
  this.schemaService = new ObjectSchemaService(adapter, registry);
2516
4082
  this.relationService = new RelationService(adapter, registry);
2517
4083
  this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
4084
+ this.permissionService = options?.permissionService;
4085
+ this.auditService = options?.auditService;
4086
+ this.userId = options?.userId;
4087
+ this.userEmail = options?.userEmail;
4088
+ }
4089
+ /**
4090
+ * Check permission for an action on an object.
4091
+ * Only checks if permissionService and userId are configured.
4092
+ * @internal
4093
+ */
4094
+ async checkPermission(objectName, action) {
4095
+ if (this.permissionService && this.userId) {
4096
+ await this.permissionService.checkObjectAccess(this.userId, objectName, action);
4097
+ }
4098
+ }
4099
+ /**
4100
+ * Resolve relation IDs to their display labels
4101
+ * @internal
4102
+ */
4103
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complexity is ok here
4104
+ async resolveRelationLabels(relationAttrs, values) {
4105
+ const resolvedMap = /* @__PURE__ */ new Map();
4106
+ const idsWithAttrId = [];
4107
+ const idsWithoutAttrId = [];
4108
+ for (const attr of relationAttrs) {
4109
+ const val = values[attr.name];
4110
+ const ids = this.extractRelationIds(val);
4111
+ if (ids.length > 0) {
4112
+ if (attr.id) {
4113
+ idsWithAttrId.push({ attrId: attr.id, ids });
4114
+ } else {
4115
+ idsWithoutAttrId.push(...ids);
4116
+ }
4117
+ }
4118
+ }
4119
+ await Promise.all(
4120
+ idsWithAttrId.map(async ({ attrId, ids }) => {
4121
+ const resolved = await this.relationService.resolveIds(ids, this.tenantId, attrId);
4122
+ for (const r of resolved) {
4123
+ resolvedMap.set(r.id, r.label);
4124
+ }
4125
+ })
4126
+ );
4127
+ if (idsWithoutAttrId.length > 0) {
4128
+ const uniqueIds = [...new Set(idsWithoutAttrId)].filter((id) => !resolvedMap.has(id));
4129
+ if (uniqueIds.length > 0) {
4130
+ const records = await this.adapter.objectRecords.findByIds(uniqueIds);
4131
+ for (const record of records) {
4132
+ if (record.label) {
4133
+ resolvedMap.set(record.id, record.label);
4134
+ }
4135
+ }
4136
+ }
4137
+ }
4138
+ return resolvedMap;
4139
+ }
4140
+ /**
4141
+ * Extract relation IDs from a value (string or array)
4142
+ * @internal
4143
+ */
4144
+ extractRelationIds(val) {
4145
+ if (typeof val === "string") return [val];
4146
+ if (Array.isArray(val) && typeof val[0] === "string") return [val[0]];
4147
+ return [];
2518
4148
  }
2519
4149
  /**
2520
4150
  * Compute display label from schema expression
@@ -2529,27 +4159,16 @@ var RecordService = class {
2529
4159
  if (relationAttrs.length === 0) {
2530
4160
  return renderLabelExpression(schema.labelExpression, values);
2531
4161
  }
2532
- const idsToResolve = [];
2533
- for (const attr of relationAttrs) {
2534
- const val = values[attr.name];
2535
- if (typeof val === "string") {
2536
- idsToResolve.push(val);
2537
- } else if (Array.isArray(val) && typeof val[0] === "string") {
2538
- idsToResolve.push(val[0]);
2539
- }
2540
- }
2541
- if (idsToResolve.length === 0) {
4162
+ const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
4163
+ if (resolvedMap.size === 0) {
2542
4164
  return renderLabelExpression(schema.labelExpression, values);
2543
4165
  }
2544
- const resolved = await this.relationService.resolveIds(idsToResolve, this.tenantId);
2545
- const resolvedMap = new Map(resolved.map((r) => [r.id, r.label]));
2546
4166
  const enrichedValues = { ...values };
2547
4167
  for (const attr of relationAttrs) {
2548
4168
  const val = values[attr.name];
2549
- if (typeof val === "string" && resolvedMap.has(val)) {
2550
- enrichedValues[attr.name] = resolvedMap.get(val);
2551
- } else if (Array.isArray(val) && typeof val[0] === "string" && resolvedMap.has(val[0])) {
2552
- enrichedValues[attr.name] = resolvedMap.get(val[0]);
4169
+ const ids = this.extractRelationIds(val);
4170
+ if (ids.length > 0 && resolvedMap.has(ids[0])) {
4171
+ enrichedValues[attr.name] = resolvedMap.get(ids[0]);
2553
4172
  }
2554
4173
  }
2555
4174
  return renderLabelExpression(schema.labelExpression, enrichedValues);
@@ -2585,6 +4204,7 @@ var RecordService = class {
2585
4204
  */
2586
4205
  async createRecord(objectId, data, options) {
2587
4206
  const schema = await this.schemaService.getObjectSchema(objectId);
4207
+ await this.checkPermission(schema.name, "create");
2588
4208
  const hookCtx = this.buildCreateHookContext(schema, data, options?.hookMetadata);
2589
4209
  if (!options?.skipHooks) {
2590
4210
  await this.hookRegistry.execute("beforeCreate", schema.name, hookCtx);
@@ -2616,6 +4236,18 @@ var RecordService = class {
2616
4236
  };
2617
4237
  await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
2618
4238
  }
4239
+ if (this.auditService && this.userId) {
4240
+ await this.auditService.logRecordAction({
4241
+ action: "record.created",
4242
+ actorId: this.userId,
4243
+ actorEmail: this.userEmail,
4244
+ objectName: schema.name,
4245
+ objectId: schema.id,
4246
+ recordId: record.id,
4247
+ recordLabel: record.label,
4248
+ metadata: options?.hookMetadata
4249
+ });
4250
+ }
2619
4251
  return record;
2620
4252
  }
2621
4253
  /**
@@ -2674,6 +4306,7 @@ var RecordService = class {
2674
4306
  async updateRecord(recordId, data, options) {
2675
4307
  const existing = await this.getRecordOrThrow(recordId);
2676
4308
  const schema = await this.schemaService.getObjectSchema(existing.objectId);
4309
+ await this.checkPermission(schema.name, "update");
2677
4310
  const mergedData = { ...existing.values, ...data };
2678
4311
  const changedAttributes = Object.keys(data).filter((key) => existing.values[key] !== data[key]);
2679
4312
  const hookCtx = this.buildHookContext(
@@ -2711,6 +4344,24 @@ var RecordService = class {
2711
4344
  };
2712
4345
  await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
2713
4346
  }
4347
+ if (this.auditService && this.userId && changedAttributes.length > 0) {
4348
+ const changes = changedAttributes.map((attr) => ({
4349
+ field: attr,
4350
+ oldValue: hookCtx.oldValues?.[attr],
4351
+ newValue: data[attr]
4352
+ }));
4353
+ await this.auditService.logRecordAction({
4354
+ action: "record.updated",
4355
+ actorId: this.userId,
4356
+ actorEmail: this.userEmail,
4357
+ objectName: schema.name,
4358
+ objectId: schema.id,
4359
+ recordId: updated.id,
4360
+ recordLabel: updated.label,
4361
+ changes,
4362
+ metadata: options?.hookMetadata
4363
+ });
4364
+ }
2714
4365
  return updated;
2715
4366
  }
2716
4367
  /**
@@ -2800,11 +4451,18 @@ var RecordService = class {
2800
4451
  async deleteRecord(recordId, options) {
2801
4452
  const record = await this.getRecordOrThrow(recordId);
2802
4453
  const schema = await this.schemaService.getObjectSchema(record.objectId);
4454
+ await this.checkPermission(schema.name, "delete");
2803
4455
  if (options?.checkSystem) {
2804
4456
  if (schema.system) {
2805
4457
  throw new Error(`Cannot delete record of system object "${schema.label}"`);
2806
4458
  }
2807
4459
  }
4460
+ if (!options?.skipReferenceCheck) {
4461
+ const references = await this.adapter.objectRecords.countRecordsReferencingId(recordId);
4462
+ if (references.length > 0) {
4463
+ throw new RecordReferencedError(recordId, references);
4464
+ }
4465
+ }
2808
4466
  const hookCtx = this.buildDeleteHookContext(schema, record, options?.hookMetadata);
2809
4467
  if (!options?.skipHooks) {
2810
4468
  await this.hookRegistry.execute("beforeDelete", schema.name, hookCtx);
@@ -2813,6 +4471,110 @@ var RecordService = class {
2813
4471
  if (!options?.skipHooks) {
2814
4472
  await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
2815
4473
  }
4474
+ if (this.auditService && this.userId) {
4475
+ await this.auditService.logRecordAction({
4476
+ action: "record.deleted",
4477
+ actorId: this.userId,
4478
+ actorEmail: this.userEmail,
4479
+ objectName: schema.name,
4480
+ objectId: schema.id,
4481
+ recordId: record.id,
4482
+ recordLabel: record.label,
4483
+ metadata: options?.hookMetadata
4484
+ });
4485
+ }
4486
+ }
4487
+ /**
4488
+ * Restore a soft-deleted record
4489
+ *
4490
+ * Triggers beforeRestore and afterRestore hooks if a HookRegistry is configured.
4491
+ *
4492
+ * @param recordId - Record UUID
4493
+ * @param options - Restore options
4494
+ * @returns Restored record
4495
+ *
4496
+ * @example
4497
+ * ```typescript
4498
+ * // Restore a deleted record
4499
+ * const restored = await service.restoreRecord("rec-123");
4500
+ * console.log(restored.deletedAt); // null
4501
+ * ```
4502
+ */
4503
+ async restoreRecord(recordId, options) {
4504
+ const record = await this.getRecordOrThrow(recordId);
4505
+ if (!record.deletedAt) {
4506
+ throw new Error(`Record "${recordId}" is not deleted`);
4507
+ }
4508
+ const schema = await this.schemaService.getObjectSchema(record.objectId);
4509
+ await this.checkPermission(schema.name, "update");
4510
+ const hookCtx = this.buildRestoreHookContext(schema, record, options?.hookMetadata);
4511
+ if (!options?.skipHooks) {
4512
+ await this.hookRegistry.execute("beforeRestore", schema.name, hookCtx);
4513
+ }
4514
+ const restored = await this.adapter.objectRecords.restore(recordId);
4515
+ if (!options?.skipHooks) {
4516
+ const afterCtx = {
4517
+ ...hookCtx,
4518
+ record: restored
4519
+ };
4520
+ await this.hookRegistry.execute("afterRestore", schema.name, afterCtx);
4521
+ }
4522
+ if (this.auditService && this.userId) {
4523
+ await this.auditService.logRecordAction({
4524
+ action: "record.restored",
4525
+ actorId: this.userId,
4526
+ actorEmail: this.userEmail,
4527
+ objectName: schema.name,
4528
+ objectId: schema.id,
4529
+ recordId: restored.id,
4530
+ recordLabel: restored.label,
4531
+ metadata: options?.hookMetadata
4532
+ });
4533
+ }
4534
+ return restored;
4535
+ }
4536
+ /**
4537
+ * Build hook context for restore operations
4538
+ * @internal
4539
+ */
4540
+ buildRestoreHookContext(schema, record, metadata) {
4541
+ return {
4542
+ objectId: schema.id,
4543
+ objectName: schema.name,
4544
+ recordId: record.id,
4545
+ tenantId: this.tenantId,
4546
+ record,
4547
+ oldValues: record.values,
4548
+ newValues: record.values,
4549
+ changedAttributes: [],
4550
+ getChange: (attr) => ({
4551
+ oldValue: record.values[attr],
4552
+ newValue: record.values[attr],
4553
+ changed: false
4554
+ }),
4555
+ metadata: metadata ?? {},
4556
+ timestamp: /* @__PURE__ */ new Date()
4557
+ };
4558
+ }
4559
+ /**
4560
+ * Permanently delete a record (hard delete)
4561
+ *
4562
+ * This cannot be undone. Use with caution - prefer soft delete for data safety.
4563
+ * Does NOT trigger delete hooks (already triggered on soft delete).
4564
+ *
4565
+ * @param recordId - Record UUID
4566
+ *
4567
+ * @example
4568
+ * ```typescript
4569
+ * // Permanently delete a record
4570
+ * await service.hardDeleteRecord("rec-123");
4571
+ * ```
4572
+ */
4573
+ async hardDeleteRecord(recordId) {
4574
+ const record = await this.getRecordOrThrow(recordId);
4575
+ const schema = await this.schemaService.getObjectSchema(record.objectId);
4576
+ await this.checkPermission(schema.name, "delete");
4577
+ await this.adapter.objectRecords.hardDelete(recordId);
2816
4578
  }
2817
4579
  /**
2818
4580
  * List records for an object with pagination
@@ -2822,6 +4584,10 @@ var RecordService = class {
2822
4584
  * @returns Records and total count
2823
4585
  */
2824
4586
  async listRecords(objectId, options) {
4587
+ if (this.permissionService && this.userId) {
4588
+ const schema = await this.schemaService.getObjectSchema(objectId);
4589
+ await this.checkPermission(schema.name, "read");
4590
+ }
2825
4591
  return await this.adapter.objectRecords.list(this.tenantId, objectId, options);
2826
4592
  }
2827
4593
  /**
@@ -2833,6 +4599,10 @@ var RecordService = class {
2833
4599
  * @returns Matching records and total count
2834
4600
  */
2835
4601
  async searchRecords(objectId, query, options) {
4602
+ if (this.permissionService && this.userId) {
4603
+ const schema = await this.schemaService.getObjectSchema(objectId);
4604
+ await this.checkPermission(schema.name, "read");
4605
+ }
2836
4606
  return await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
2837
4607
  }
2838
4608
  /**
@@ -2880,9 +4650,12 @@ var RecordService = class {
2880
4650
 
2881
4651
  // src/runtime/services/user-profile.service.ts
2882
4652
  var UserProfileService = class {
2883
- constructor(adapter, tenantId) {
4653
+ constructor(adapter, tenantId, options) {
2884
4654
  this.adapter = adapter;
2885
4655
  this.tenantId = tenantId;
4656
+ this.auditService = options?.auditService;
4657
+ this.userId = options?.userId;
4658
+ this.userEmail = options?.userEmail;
2886
4659
  }
2887
4660
  /**
2888
4661
  * Create a new user profile (typically after first auth)
@@ -2920,7 +4693,17 @@ var UserProfileService = class {
2920
4693
  if (existingEmail) {
2921
4694
  throw new Error(`User profile with email "${data.email}" already exists in this tenant`);
2922
4695
  }
2923
- return await this.adapter.userProfiles.create(data);
4696
+ const profile = await this.adapter.userProfiles.create(data);
4697
+ if (this.auditService && this.userId) {
4698
+ await this.auditService.logUserAction({
4699
+ action: "user.created",
4700
+ actorId: this.userId,
4701
+ actorEmail: this.userEmail,
4702
+ targetUserId: profile.id,
4703
+ targetUserEmail: profile.email
4704
+ });
4705
+ }
4706
+ return profile;
2924
4707
  }
2925
4708
  /**
2926
4709
  * Get user profile by ID
@@ -2981,8 +4764,27 @@ var UserProfileService = class {
2981
4764
  * Update user profile
2982
4765
  */
2983
4766
  async updateProfile(profileId, data) {
2984
- await this.getProfileOrThrow(profileId);
2985
- return await this.adapter.userProfiles.update(profileId, data);
4767
+ const existing = await this.getProfileOrThrow(profileId);
4768
+ const updated = await this.adapter.userProfiles.update(profileId, data);
4769
+ if (this.auditService && this.userId) {
4770
+ const changes = buildAuditChanges(existing, data, [
4771
+ "firstName",
4772
+ "lastName",
4773
+ "role",
4774
+ "status"
4775
+ ]);
4776
+ if (changes.length > 0) {
4777
+ await this.auditService.logUserAction({
4778
+ action: "user.updated",
4779
+ actorId: this.userId,
4780
+ actorEmail: this.userEmail,
4781
+ targetUserId: profileId,
4782
+ targetUserEmail: updated.email,
4783
+ changes
4784
+ });
4785
+ }
4786
+ }
4787
+ return updated;
2986
4788
  }
2987
4789
  /**
2988
4790
  * Delete user profile
@@ -2991,17 +4793,25 @@ var UserProfileService = class {
2991
4793
  * @param options - Delete options
2992
4794
  */
2993
4795
  async deleteProfile(profileId, options) {
4796
+ const profile = await this.getProfileOrThrow(profileId);
2994
4797
  if (options?.checkAdmin) {
2995
- const profile = await this.getProfileOrThrow(profileId);
2996
4798
  if (profile.role === "admin") {
2997
- const allProfiles = await this.listProfiles();
2998
- const adminCount = allProfiles.filter((p) => p.role === "admin").length;
4799
+ const adminCount = await this.adapter.userProfiles.countByRole(this.tenantId, "admin");
2999
4800
  if (adminCount <= 1) {
3000
4801
  throw new Error("Cannot delete the last admin user");
3001
4802
  }
3002
4803
  }
3003
4804
  }
3004
4805
  await this.adapter.userProfiles.delete(profileId);
4806
+ if (this.auditService && this.userId) {
4807
+ await this.auditService.logUserAction({
4808
+ action: "user.deleted",
4809
+ actorId: this.userId,
4810
+ actorEmail: this.userEmail,
4811
+ targetUserId: profileId,
4812
+ targetUserEmail: profile.email
4813
+ });
4814
+ }
3005
4815
  }
3006
4816
  /**
3007
4817
  * List all user profiles for the tenant
@@ -3138,9 +4948,6 @@ var ViewService = class {
3138
4948
  `Cannot create view "${input.name}": a system view with this name already exists`
3139
4949
  );
3140
4950
  }
3141
- if (!input.tabs || input.tabs.length === 0) {
3142
- throw new Error("View must have at least one tab");
3143
- }
3144
4951
  const dbView = await this.adapter.views.create({
3145
4952
  tenantId,
3146
4953
  objectName: input.objectName,
@@ -3148,7 +4955,7 @@ var ViewService = class {
3148
4955
  label: input.label,
3149
4956
  description: input.description,
3150
4957
  icon: input.icon,
3151
- tabs: input.tabs,
4958
+ tabs: input.tabs ?? [],
3152
4959
  default: input.default ?? false,
3153
4960
  system: false,
3154
4961
  // Custom views are never system
@@ -3171,9 +4978,6 @@ var ViewService = class {
3171
4978
  if (dbView.system) {
3172
4979
  throw new Error("Cannot modify system views. System views are protected.");
3173
4980
  }
3174
- if (input.tabs && input.tabs.length === 0) {
3175
- throw new Error("View must have at least one tab");
3176
- }
3177
4981
  const updated = await this.adapter.views.update(viewId, {
3178
4982
  label: input.label,
3179
4983
  description: input.description,
@@ -3601,17 +5405,21 @@ var NoopGeocodingAdapter = class {
3601
5405
  };
3602
5406
  // Annotate the CommonJS export names for ESM import in node:
3603
5407
  0 && (module.exports = {
5408
+ AuditService,
3604
5409
  DEFAULT_LABEL_FALLBACK,
3605
5410
  FileService,
5411
+ FlowService,
3606
5412
  GeocodingService,
3607
5413
  GlobalSearchService,
3608
5414
  NoopGeocodingAdapter,
3609
5415
  NoopHookRegistry,
3610
5416
  ObjectSchemaService,
5417
+ PermissionService,
3611
5418
  RecordService,
3612
5419
  RelationService,
3613
5420
  UserProfileService,
3614
5421
  ViewService,
5422
+ buildAuditChanges,
3615
5423
  createMockAdapter,
3616
5424
  extractAttributeNames,
3617
5425
  getSyncPreview,