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

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 (52) 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-BUWVNOSK.mjs +5457 -0
  7. package/dist/chunk-CCEHW4E7.mjs +6071 -0
  8. package/dist/chunk-CNR3BZLU.mjs +5163 -0
  9. package/dist/chunk-DLMVX6DN.mjs +6074 -0
  10. package/dist/chunk-DUUORFHI.mjs +5379 -0
  11. package/dist/chunk-DWTD7YPJ.mjs +74 -0
  12. package/dist/chunk-FAR5ENGR.mjs +78 -0
  13. package/dist/chunk-ITWOVZOW.mjs +5809 -0
  14. package/dist/chunk-KJLKY5X7.mjs +6314 -0
  15. package/dist/chunk-KTJXMGBR.mjs +5397 -0
  16. package/dist/chunk-NOVHQ7B2.mjs +6327 -0
  17. package/dist/chunk-PMFQ4ZL4.mjs +6230 -0
  18. package/dist/chunk-QC47SMCZ.mjs +5467 -0
  19. package/dist/chunk-Z6LIJ5PO.mjs +5362 -0
  20. package/dist/default-roles-G4TOWUVM.mjs +20 -0
  21. package/dist/default-roles-QEZPRSUL.mjs +20 -0
  22. package/dist/index.d.mts +193 -15
  23. package/dist/index.d.ts +193 -15
  24. package/dist/index.js +1657 -118
  25. package/dist/index.mjs +56 -163
  26. package/dist/runtime-B1_syHVj.d.mts +4787 -0
  27. package/dist/runtime-B1_syHVj.d.ts +4787 -0
  28. package/dist/runtime-B7N-oycb.d.mts +4854 -0
  29. package/dist/runtime-B7N-oycb.d.ts +4854 -0
  30. package/dist/runtime-BDoAIuTe.d.mts +4872 -0
  31. package/dist/runtime-BDoAIuTe.d.ts +4872 -0
  32. package/dist/runtime-BVRmzg3d.d.mts +4718 -0
  33. package/dist/runtime-BVRmzg3d.d.ts +4718 -0
  34. package/dist/runtime-BrXwIidf.d.mts +4794 -0
  35. package/dist/runtime-BrXwIidf.d.ts +4794 -0
  36. package/dist/runtime-CC_HZhVN.d.mts +5224 -0
  37. package/dist/runtime-CC_HZhVN.d.ts +5224 -0
  38. package/dist/runtime-CEx57sBZ.d.mts +4791 -0
  39. package/dist/runtime-CEx57sBZ.d.ts +4791 -0
  40. package/dist/runtime-COGvuXIq.d.mts +4790 -0
  41. package/dist/runtime-COGvuXIq.d.ts +4790 -0
  42. package/dist/runtime-CSGWjztI.d.mts +5166 -0
  43. package/dist/runtime-CSGWjztI.d.ts +5166 -0
  44. package/dist/runtime-DUxWyRcq.d.mts +5399 -0
  45. package/dist/runtime-DUxWyRcq.d.ts +5399 -0
  46. package/dist/runtime-LO17NDk1.d.mts +5292 -0
  47. package/dist/runtime-LO17NDk1.d.ts +5292 -0
  48. package/dist/runtime.d.mts +1 -1
  49. package/dist/runtime.d.ts +1 -1
  50. package/dist/runtime.js +1645 -97
  51. package/dist/runtime.mjs +7 -1
  52. 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,9 +30,95 @@ 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,
35
124
  GeocodingService: () => GeocodingService,
@@ -37,10 +126,12 @@ __export(runtime_exports, {
37
126
  NoopGeocodingAdapter: () => NoopGeocodingAdapter,
38
127
  NoopHookRegistry: () => NoopHookRegistry,
39
128
  ObjectSchemaService: () => ObjectSchemaService,
129
+ PermissionService: () => PermissionService,
40
130
  RecordService: () => RecordService,
41
131
  RelationService: () => RelationService,
42
132
  UserProfileService: () => UserProfileService,
43
133
  ViewService: () => ViewService,
134
+ buildAuditChanges: () => buildAuditChanges,
44
135
  createMockAdapter: () => createMockAdapter,
45
136
  extractAttributeNames: () => extractAttributeNames,
46
137
  getSyncPreview: () => getSyncPreview,
@@ -375,6 +466,12 @@ function createMockUserProfilesRepository(stores) {
375
466
  }
376
467
  return Promise.resolve(results);
377
468
  },
469
+ countByRole(tenantId, role) {
470
+ const count = Array.from(stores.userProfiles.values()).filter(
471
+ (profile) => profile.tenantId === tenantId && profile.role === role
472
+ ).length;
473
+ return Promise.resolve(count);
474
+ },
378
475
  updateLastLogin(id) {
379
476
  const profile = stores.userProfiles.get(id);
380
477
  if (profile) {
@@ -468,12 +565,36 @@ function createMockFilesRepository(stores) {
468
565
  }
469
566
  function createMockObjectRecordsRepository(stores) {
470
567
  return {
568
+ hardDelete(id) {
569
+ stores.objectRecords.delete(id);
570
+ return Promise.resolve();
571
+ },
572
+ restore(id) {
573
+ const existing = stores.objectRecords.get(id);
574
+ if (!existing) {
575
+ return Promise.reject(new Error(`ObjectRecord ${id} not found`));
576
+ }
577
+ existing.deletedAt = null;
578
+ stores.objectRecords.set(id, existing);
579
+ return Promise.resolve(existing);
580
+ },
471
581
  findById(id) {
472
582
  const internal = stores.objectRecords.get(id);
473
583
  if (!internal) return Promise.resolve(null);
474
584
  const { tenantId: _, ...record } = internal;
475
585
  return Promise.resolve(record);
476
586
  },
587
+ findByIds(ids) {
588
+ const results = [];
589
+ for (const id of ids) {
590
+ const internal = stores.objectRecords.get(id);
591
+ if (internal) {
592
+ const { tenantId: _, ...record } = internal;
593
+ results.push(record);
594
+ }
595
+ }
596
+ return Promise.resolve(results);
597
+ },
477
598
  create(data) {
478
599
  const internal = {
479
600
  id: generateId(),
@@ -586,6 +707,85 @@ function createMockObjectRecordsRepository(stores) {
586
707
  };
587
708
  });
588
709
  return Promise.resolve({ results, total });
710
+ },
711
+ // -------------------------------------------------------------------------
712
+ // Schema Integrity Methods
713
+ // -------------------------------------------------------------------------
714
+ countRecordsReferencingId(targetId) {
715
+ const relationAttrs = [];
716
+ for (const attr of stores.attributes.values()) {
717
+ if (attr.type === "relation") {
718
+ relationAttrs.push({ objectId: attr.objectId, attrName: attr.name });
719
+ }
720
+ }
721
+ const countsByObjectId = /* @__PURE__ */ new Map();
722
+ for (const record of stores.objectRecords.values()) {
723
+ for (const { objectId, attrName } of relationAttrs) {
724
+ if (record.objectId !== objectId) continue;
725
+ const value = record.values[attrName];
726
+ let isReferencing = false;
727
+ if (typeof value === "string" && value === targetId) {
728
+ isReferencing = true;
729
+ } else if (Array.isArray(value) && value.includes(targetId)) {
730
+ isReferencing = true;
731
+ }
732
+ if (isReferencing) {
733
+ const current = countsByObjectId.get(objectId) ?? 0;
734
+ countsByObjectId.set(objectId, current + 1);
735
+ }
736
+ }
737
+ }
738
+ const results = [];
739
+ for (const [objectId, count] of countsByObjectId) {
740
+ const obj = stores.objects.get(objectId);
741
+ if (obj) {
742
+ results.push({
743
+ objectName: obj.name,
744
+ objectLabel: obj.label,
745
+ count
746
+ });
747
+ }
748
+ }
749
+ return Promise.resolve(results);
750
+ },
751
+ removeAttributeData(objectId, attributeName) {
752
+ let updated = 0;
753
+ for (const record of stores.objectRecords.values()) {
754
+ if (record.objectId === objectId && attributeName in record.values) {
755
+ delete record.values[attributeName];
756
+ record.updatedAt = /* @__PURE__ */ new Date();
757
+ updated++;
758
+ }
759
+ }
760
+ return Promise.resolve(updated);
761
+ },
762
+ batchRefreshLabels(objectId, computeLabel) {
763
+ let updated = 0;
764
+ for (const record of stores.objectRecords.values()) {
765
+ if (record.objectId === objectId) {
766
+ const newLabel = computeLabel(record.values);
767
+ if (newLabel !== record.label) {
768
+ record.label = newLabel;
769
+ record.updatedAt = /* @__PURE__ */ new Date();
770
+ updated++;
771
+ }
772
+ }
773
+ }
774
+ return Promise.resolve(updated);
775
+ },
776
+ batchRefreshStatus(objectId, computeStatus) {
777
+ let updated = 0;
778
+ for (const record of stores.objectRecords.values()) {
779
+ if (record.objectId === objectId) {
780
+ const newStatus = computeStatus(record.values);
781
+ if (newStatus !== record.completionStatus) {
782
+ record.completionStatus = newStatus;
783
+ record.updatedAt = /* @__PURE__ */ new Date();
784
+ updated++;
785
+ }
786
+ }
787
+ }
788
+ return Promise.resolve(updated);
589
789
  }
590
790
  };
591
791
  }
@@ -689,6 +889,157 @@ function createMockViewsRepository(stores) {
689
889
  }
690
890
  };
691
891
  }
892
+ function createMockPermissionsRepository(stores) {
893
+ return {
894
+ getRolesByTenant(tenantId) {
895
+ const results = Array.from(stores.roles.values()).filter((r) => r.tenantId === tenantId);
896
+ return Promise.resolve(results);
897
+ },
898
+ getRoleById(roleId) {
899
+ return Promise.resolve(stores.roles.get(roleId) ?? null);
900
+ },
901
+ getRoleByName(tenantId, name) {
902
+ for (const role of stores.roles.values()) {
903
+ if (role.tenantId === tenantId && role.name === name) {
904
+ return Promise.resolve(role);
905
+ }
906
+ }
907
+ return Promise.resolve(null);
908
+ },
909
+ createRole(input) {
910
+ const role = {
911
+ id: generateId(),
912
+ tenantId: input.tenantId,
913
+ name: input.name,
914
+ label: input.label,
915
+ description: input.description,
916
+ system: false,
917
+ createdAt: /* @__PURE__ */ new Date(),
918
+ updatedAt: /* @__PURE__ */ new Date()
919
+ };
920
+ stores.roles.set(role.id, role);
921
+ return Promise.resolve(role);
922
+ },
923
+ updateRole(roleId, updates) {
924
+ const existing = stores.roles.get(roleId);
925
+ if (!existing) {
926
+ return Promise.reject(new Error(`Role ${roleId} not found`));
927
+ }
928
+ const updated = {
929
+ ...existing,
930
+ ...updates,
931
+ updatedAt: /* @__PURE__ */ new Date()
932
+ };
933
+ stores.roles.set(roleId, updated);
934
+ return Promise.resolve(updated);
935
+ },
936
+ deleteRole(roleId) {
937
+ const role = stores.roles.get(roleId);
938
+ if (role?.system) {
939
+ return Promise.reject(new Error(`Cannot delete system role ${roleId}`));
940
+ }
941
+ stores.roles.delete(roleId);
942
+ for (const [id, perm] of stores.permissions.entries()) {
943
+ if (perm.roleId === roleId) {
944
+ stores.permissions.delete(id);
945
+ }
946
+ }
947
+ for (const [id, ur] of stores.userRoles.entries()) {
948
+ if (ur.roleId === roleId) {
949
+ stores.userRoles.delete(id);
950
+ }
951
+ }
952
+ return Promise.resolve();
953
+ },
954
+ getPermissionsByRole(roleId) {
955
+ const results = Array.from(stores.permissions.values()).filter((p) => p.roleId === roleId);
956
+ return Promise.resolve(results);
957
+ },
958
+ setPermissions(roleId, permissions) {
959
+ for (const [id, perm] of stores.permissions.entries()) {
960
+ if (perm.roleId === roleId) {
961
+ stores.permissions.delete(id);
962
+ }
963
+ }
964
+ for (const input of permissions) {
965
+ const perm = {
966
+ id: generateId(),
967
+ roleId,
968
+ scope: input.scope,
969
+ target: input.target,
970
+ actions: input.actions,
971
+ filter: input.filter,
972
+ createdAt: /* @__PURE__ */ new Date()
973
+ };
974
+ stores.permissions.set(perm.id, perm);
975
+ }
976
+ return Promise.resolve();
977
+ },
978
+ getUserRoles(userProfileId, tenantId) {
979
+ const roleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === userProfileId && ur.tenantId === tenantId).map((ur) => ur.roleId);
980
+ const roles = Array.from(stores.roles.values()).filter((r) => roleIds.includes(r.id));
981
+ return Promise.resolve(roles);
982
+ },
983
+ assignRole(input) {
984
+ for (const ur of stores.userRoles.values()) {
985
+ if (ur.userProfileId === input.userProfileId && ur.roleId === input.roleId && ur.tenantId === input.tenantId) {
986
+ return Promise.resolve(ur);
987
+ }
988
+ }
989
+ const userRole = {
990
+ id: generateId(),
991
+ userProfileId: input.userProfileId,
992
+ roleId: input.roleId,
993
+ tenantId: input.tenantId,
994
+ assignedAt: /* @__PURE__ */ new Date(),
995
+ assignedBy: input.assignedBy
996
+ };
997
+ stores.userRoles.set(userRole.id, userRole);
998
+ return Promise.resolve(userRole);
999
+ },
1000
+ revokeRole(userProfileId, roleId, tenantId) {
1001
+ for (const [id, ur] of stores.userRoles.entries()) {
1002
+ if (ur.userProfileId === userProfileId && ur.roleId === roleId && ur.tenantId === tenantId) {
1003
+ stores.userRoles.delete(id);
1004
+ break;
1005
+ }
1006
+ }
1007
+ return Promise.resolve();
1008
+ },
1009
+ getEffectivePermissions(userProfileId, tenantId) {
1010
+ const userRoleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === userProfileId && ur.tenantId === tenantId).map((ur) => ur.roleId);
1011
+ const userRoles = Array.from(stores.roles.values()).filter((r) => userRoleIds.includes(r.id));
1012
+ const isAdmin = userRoles.some((r) => r.name === "admin");
1013
+ const userPermissions = Array.from(stores.permissions.values()).filter(
1014
+ (p) => userRoleIds.includes(p.roleId)
1015
+ );
1016
+ const objectPermissions = {};
1017
+ const systemPermissions = {};
1018
+ for (const perm of userPermissions) {
1019
+ if (perm.scope === "object") {
1020
+ if (!objectPermissions[perm.target]) {
1021
+ objectPermissions[perm.target] = [];
1022
+ }
1023
+ for (const action of perm.actions) {
1024
+ if (!objectPermissions[perm.target].includes(action)) {
1025
+ objectPermissions[perm.target].push(action);
1026
+ }
1027
+ }
1028
+ } else if (perm.scope === "system") {
1029
+ if (!systemPermissions[perm.target]) {
1030
+ systemPermissions[perm.target] = [];
1031
+ }
1032
+ for (const action of perm.actions) {
1033
+ if (!systemPermissions[perm.target].includes(action)) {
1034
+ systemPermissions[perm.target].push(action);
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1039
+ return Promise.resolve({ isAdmin, objectPermissions, systemPermissions });
1040
+ }
1041
+ };
1042
+ }
692
1043
  function createMockAdapter() {
693
1044
  const stores = {
694
1045
  objects: /* @__PURE__ */ new Map(),
@@ -696,7 +1047,10 @@ function createMockAdapter() {
696
1047
  userProfiles: /* @__PURE__ */ new Map(),
697
1048
  files: /* @__PURE__ */ new Map(),
698
1049
  objectRecords: /* @__PURE__ */ new Map(),
699
- views: /* @__PURE__ */ new Map()
1050
+ views: /* @__PURE__ */ new Map(),
1051
+ roles: /* @__PURE__ */ new Map(),
1052
+ permissions: /* @__PURE__ */ new Map(),
1053
+ userRoles: /* @__PURE__ */ new Map()
700
1054
  };
701
1055
  const adapter = {
702
1056
  objects: createMockObjectsRepository(stores),
@@ -705,6 +1059,7 @@ function createMockAdapter() {
705
1059
  files: createMockFilesRepository(stores),
706
1060
  objectRecords: createMockObjectRecordsRepository(stores),
707
1061
  views: createMockViewsRepository(stores),
1062
+ permissions: createMockPermissionsRepository(stores),
708
1063
  async transaction(callback) {
709
1064
  return await callback(adapter);
710
1065
  },
@@ -718,11 +1073,266 @@ function createMockAdapter() {
718
1073
  stores.files.clear();
719
1074
  stores.objectRecords.clear();
720
1075
  stores.views.clear();
1076
+ stores.roles.clear();
1077
+ stores.permissions.clear();
1078
+ stores.userRoles.clear();
721
1079
  }
722
1080
  };
723
1081
  return adapter;
724
1082
  }
725
1083
 
1084
+ // src/runtime/services/audit.helpers.ts
1085
+ function buildAuditChanges(oldValues, newValues, fieldsToCheck) {
1086
+ const changes = [];
1087
+ for (const field of fieldsToCheck) {
1088
+ if (newValues[field] !== void 0 && newValues[field] !== oldValues[field]) {
1089
+ changes.push({
1090
+ field: String(field),
1091
+ oldValue: oldValues[field],
1092
+ newValue: newValues[field]
1093
+ });
1094
+ }
1095
+ }
1096
+ return changes;
1097
+ }
1098
+
1099
+ // src/runtime/services/audit.service.ts
1100
+ var SENSITIVE_PATTERNS = [
1101
+ /password/i,
1102
+ /token/i,
1103
+ /secret/i,
1104
+ /apiKey/i,
1105
+ /api_key/i,
1106
+ /creditCard/i,
1107
+ /credit_card/i,
1108
+ /ssn/i,
1109
+ /cvv/i
1110
+ ];
1111
+ var AuditService = class {
1112
+ constructor(adapter, tenantId, options) {
1113
+ this.adapter = adapter;
1114
+ this.tenantId = tenantId;
1115
+ this.options = options;
1116
+ this.buffer = [];
1117
+ this.flushTimer = null;
1118
+ if (options?.async && options.flushIntervalMs) {
1119
+ this.startFlushTimer();
1120
+ }
1121
+ }
1122
+ // ============================================================================
1123
+ // PUBLIC LOGGING METHODS
1124
+ // ============================================================================
1125
+ /**
1126
+ * Log a record action (create, update, delete, restore)
1127
+ */
1128
+ async logRecordAction(params) {
1129
+ const entry = {
1130
+ tenantId: this.tenantId,
1131
+ actorId: params.actorId,
1132
+ actorEmail: params.actorEmail,
1133
+ actorType: "user",
1134
+ action: params.action,
1135
+ resourceType: "record",
1136
+ resourceId: params.recordId,
1137
+ resourceLabel: params.recordLabel,
1138
+ objectName: params.objectName,
1139
+ objectId: params.objectId,
1140
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
1141
+ metadata: params.metadata
1142
+ };
1143
+ await this.log(entry);
1144
+ }
1145
+ /**
1146
+ * Log a schema action (object/attribute changes)
1147
+ */
1148
+ async logSchemaAction(params) {
1149
+ const entry = {
1150
+ tenantId: this.tenantId,
1151
+ actorId: params.actorId,
1152
+ actorEmail: params.actorEmail,
1153
+ actorType: "user",
1154
+ action: params.action,
1155
+ resourceType: params.resourceType,
1156
+ resourceId: params.resourceId,
1157
+ resourceLabel: params.resourceLabel,
1158
+ objectName: params.objectName,
1159
+ objectId: params.objectId,
1160
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
1161
+ metadata: params.metadata
1162
+ };
1163
+ await this.log(entry);
1164
+ }
1165
+ /**
1166
+ * Log a user action
1167
+ */
1168
+ async logUserAction(params) {
1169
+ const entry = {
1170
+ tenantId: this.tenantId,
1171
+ actorId: params.actorId,
1172
+ actorEmail: params.actorEmail,
1173
+ actorType: "user",
1174
+ action: params.action,
1175
+ resourceType: "user",
1176
+ resourceId: params.targetUserId,
1177
+ resourceLabel: params.targetUserEmail,
1178
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
1179
+ metadata: params.metadata
1180
+ };
1181
+ await this.log(entry);
1182
+ }
1183
+ /**
1184
+ * Log a role action
1185
+ */
1186
+ async logRoleAction(params) {
1187
+ const entry = {
1188
+ tenantId: this.tenantId,
1189
+ actorId: params.actorId,
1190
+ actorEmail: params.actorEmail,
1191
+ actorType: "user",
1192
+ action: params.action,
1193
+ resourceType: "role",
1194
+ resourceId: params.roleId,
1195
+ resourceLabel: params.roleLabel,
1196
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
1197
+ metadata: {
1198
+ ...params.metadata,
1199
+ targetUserId: params.targetUserId
1200
+ }
1201
+ };
1202
+ await this.log(entry);
1203
+ }
1204
+ /**
1205
+ * Log a system action (no actor)
1206
+ */
1207
+ async logSystemAction(params) {
1208
+ const entry = {
1209
+ tenantId: this.tenantId,
1210
+ actorType: "system",
1211
+ action: params.action,
1212
+ resourceType: params.resourceType,
1213
+ resourceId: params.resourceId,
1214
+ resourceLabel: params.resourceLabel,
1215
+ objectName: params.objectName,
1216
+ objectId: params.objectId,
1217
+ changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
1218
+ metadata: params.metadata
1219
+ };
1220
+ await this.log(entry);
1221
+ }
1222
+ // ============================================================================
1223
+ // QUERY METHODS
1224
+ // ============================================================================
1225
+ /**
1226
+ * List audit logs with filtering and pagination
1227
+ */
1228
+ async list(options) {
1229
+ if (!this.adapter.audit) {
1230
+ return { entries: [], total: 0 };
1231
+ }
1232
+ return await this.adapter.audit.list(this.tenantId, options);
1233
+ }
1234
+ /**
1235
+ * Get audit history for a specific record
1236
+ */
1237
+ async getRecordHistory(recordId) {
1238
+ if (!this.adapter.audit) {
1239
+ return [];
1240
+ }
1241
+ return await this.adapter.audit.getByResource(this.tenantId, "record", recordId, {
1242
+ orderDirection: "desc"
1243
+ });
1244
+ }
1245
+ /**
1246
+ * Get activity for a specific user
1247
+ */
1248
+ async getUserActivity(userId, options) {
1249
+ if (!this.adapter.audit) {
1250
+ return [];
1251
+ }
1252
+ return await this.adapter.audit.getByActor(this.tenantId, userId, options);
1253
+ }
1254
+ /**
1255
+ * Get audit logs for a specific resource
1256
+ */
1257
+ async getResourceHistory(resourceType, resourceId) {
1258
+ if (!this.adapter.audit) {
1259
+ return [];
1260
+ }
1261
+ return await this.adapter.audit.getByResource(this.tenantId, resourceType, resourceId, {
1262
+ orderDirection: "desc"
1263
+ });
1264
+ }
1265
+ // ============================================================================
1266
+ // BUFFER MANAGEMENT (ASYNC MODE)
1267
+ // ============================================================================
1268
+ /**
1269
+ * Flush buffered logs to the database
1270
+ */
1271
+ async flush() {
1272
+ if (this.buffer.length === 0 || !this.adapter.audit) {
1273
+ return;
1274
+ }
1275
+ const entries = [...this.buffer];
1276
+ this.buffer = [];
1277
+ await this.adapter.audit.createMany(entries);
1278
+ }
1279
+ /**
1280
+ * Clean up resources (stop timer, flush remaining logs)
1281
+ */
1282
+ async dispose() {
1283
+ if (this.flushTimer) {
1284
+ clearInterval(this.flushTimer);
1285
+ this.flushTimer = null;
1286
+ }
1287
+ await this.flush();
1288
+ }
1289
+ // ============================================================================
1290
+ // PRIVATE METHODS
1291
+ // ============================================================================
1292
+ /**
1293
+ * Log an entry (sync or async based on options)
1294
+ */
1295
+ async log(entry) {
1296
+ if (!this.adapter.audit) {
1297
+ return;
1298
+ }
1299
+ if (this.options?.async) {
1300
+ this.buffer.push(entry);
1301
+ const batchSize = this.options.batchSize ?? 10;
1302
+ if (this.buffer.length >= batchSize) {
1303
+ await this.flush();
1304
+ }
1305
+ } else {
1306
+ await this.adapter.audit.create(entry);
1307
+ }
1308
+ }
1309
+ /**
1310
+ * Start the flush timer for async mode
1311
+ */
1312
+ startFlushTimer() {
1313
+ const intervalMs = this.options?.flushIntervalMs ?? 1e3;
1314
+ this.flushTimer = setInterval(() => {
1315
+ this.flush().catch(console.error);
1316
+ }, intervalMs);
1317
+ }
1318
+ /**
1319
+ * Redact sensitive fields from changes
1320
+ */
1321
+ redactSensitiveFields(changes) {
1322
+ return changes.map((change) => {
1323
+ const isSensitive = SENSITIVE_PATTERNS.some((pattern) => pattern.test(change.field));
1324
+ if (isSensitive) {
1325
+ return {
1326
+ ...change,
1327
+ oldValue: "[REDACTED]",
1328
+ newValue: "[REDACTED]"
1329
+ };
1330
+ }
1331
+ return change;
1332
+ });
1333
+ }
1334
+ };
1335
+
726
1336
  // src/runtime/services/file.service.ts
727
1337
  var FileService = class {
728
1338
  constructor(adapter, tenantId) {
@@ -1196,6 +1806,38 @@ function object(config) {
1196
1806
  // src/builders/view-builder.ts
1197
1807
  var import_zod2 = require("zod");
1198
1808
 
1809
+ // src/types/errors.ts
1810
+ var RecordReferencedError = class extends Error {
1811
+ constructor(recordId, references) {
1812
+ const total = references.reduce((sum, r) => sum + r.count, 0);
1813
+ super(`Cannot delete record: referenced by ${total} record${total > 1 ? "s" : ""}`);
1814
+ this.recordId = recordId;
1815
+ this.references = references;
1816
+ this.code = "RECORD_REFERENCED";
1817
+ this.name = "RecordReferencedError";
1818
+ }
1819
+ };
1820
+ var AttributeInUseError = class extends Error {
1821
+ constructor(attributeName, usage) {
1822
+ super(`Cannot delete attribute "${attributeName}": used in ${usage}`);
1823
+ this.attributeName = attributeName;
1824
+ this.usage = usage;
1825
+ this.code = "ATTRIBUTE_IN_USE";
1826
+ this.name = "AttributeInUseError";
1827
+ }
1828
+ };
1829
+ var ObjectReferencedError = class extends Error {
1830
+ constructor(objectName, referencingObjects) {
1831
+ super(
1832
+ `Cannot delete object "${objectName}": target of relations in ${referencingObjects.join(", ")}`
1833
+ );
1834
+ this.objectName = objectName;
1835
+ this.referencingObjects = referencingObjects;
1836
+ this.code = "OBJECT_REFERENCED";
1837
+ this.name = "ObjectReferencedError";
1838
+ }
1839
+ };
1840
+
1199
1841
  // src/validation/validators.ts
1200
1842
  var import_zod3 = require("zod");
1201
1843
  var baseConfigSchema = import_zod3.z.object({
@@ -1309,7 +1951,6 @@ var userConfigSchema = baseConfigSchema.extend({
1309
1951
  });
1310
1952
  var relationConfigSchema = baseConfigSchema.extend({
1311
1953
  targets: import_zod3.z.array(relationTargetSchema).min(1),
1312
- onDelete: import_zod3.z.enum(["restrict", "cascade", "set_null"]).optional(),
1313
1954
  cardinality: import_zod3.z.enum(["one", "many"]),
1314
1955
  minItems: import_zod3.z.number().int().min(0).optional(),
1315
1956
  maxItems: import_zod3.z.number().int().min(1).optional()
@@ -1617,9 +2258,12 @@ function computeRecordStatus(objectDef, data) {
1617
2258
 
1618
2259
  // src/runtime/services/object-schema.service.ts
1619
2260
  var ObjectSchemaService = class {
1620
- constructor(adapter, nativeRegistry) {
2261
+ constructor(adapter, nativeRegistry, options) {
1621
2262
  this.adapter = adapter;
1622
2263
  this.nativeRegistry = nativeRegistry;
2264
+ this.auditService = options?.auditService;
2265
+ this.userId = options?.userId;
2266
+ this.userEmail = options?.userEmail;
1623
2267
  }
1624
2268
  /**
1625
2269
  * Create a new custom object
@@ -1661,41 +2305,57 @@ var ObjectSchemaService = class {
1661
2305
  ...objectDef.metadata,
1662
2306
  icon: objectDef.icon
1663
2307
  };
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));
2308
+ const { dbObject, createdAttributes } = await this.adapter.transaction(async (tx) => {
2309
+ const dbObj = await tx.objects.create({
2310
+ tenantId,
2311
+ name: objectDef.name,
2312
+ label: objectDef.label,
2313
+ pluralLabel: objectDef.pluralLabel,
2314
+ description: objectDef.description,
2315
+ labelExpression: objectDef.labelExpression,
2316
+ system: false,
2317
+ // Always false for custom objects
2318
+ metadata
2319
+ });
2320
+ const attrs = [];
2321
+ if (objectDef.attributes.length > 0) {
2322
+ for (let i = 0; i < objectDef.attributes.length; i++) {
2323
+ const attr = objectDef.attributes[i];
2324
+ const config = validatedConfigs[i];
2325
+ const dbAttr = await tx.attributes.create({
2326
+ objectId: dbObj.id,
2327
+ name: attr.name,
2328
+ label: attr.label,
2329
+ type: attr.type,
2330
+ required: attr.required ?? false,
2331
+ unique: false,
2332
+ system: false,
2333
+ config
2334
+ });
2335
+ attrs.push(this.convertDBAttributeToAttribute(dbAttr));
2336
+ }
1691
2337
  }
1692
- }
1693
- return {
2338
+ return { dbObject: dbObj, createdAttributes: attrs };
2339
+ });
2340
+ const result = {
1694
2341
  ...objectDef,
1695
2342
  id: dbObject.id,
1696
2343
  attributes: createdAttributes,
1697
2344
  system: false
1698
2345
  };
2346
+ if (this.auditService && this.userId) {
2347
+ await this.auditService.logSchemaAction({
2348
+ action: "object.created",
2349
+ actorId: this.userId,
2350
+ actorEmail: this.userEmail,
2351
+ resourceType: "object",
2352
+ resourceId: dbObject.id,
2353
+ resourceLabel: dbObject.label,
2354
+ objectName: dbObject.name,
2355
+ objectId: dbObject.id
2356
+ });
2357
+ }
2358
+ return result;
1699
2359
  }
1700
2360
  /**
1701
2361
  * Add a custom attribute to an existing object
@@ -1731,6 +2391,18 @@ var ObjectSchemaService = class {
1731
2391
  // Always false for attributes added via API
1732
2392
  config
1733
2393
  });
2394
+ if (this.auditService && this.userId) {
2395
+ await this.auditService.logSchemaAction({
2396
+ action: "attribute.created",
2397
+ actorId: this.userId,
2398
+ actorEmail: this.userEmail,
2399
+ resourceType: "attribute",
2400
+ resourceId: dbAttr.id,
2401
+ resourceLabel: dbAttr.label,
2402
+ objectName: dbObject.name,
2403
+ objectId: dbObject.id
2404
+ });
2405
+ }
1734
2406
  return this.convertDBAttributeToAttribute(dbAttr);
1735
2407
  }
1736
2408
  /**
@@ -1774,11 +2446,39 @@ var ObjectSchemaService = class {
1774
2446
  if (dbAttr.type === "relation" && updates.targets) {
1775
2447
  await this.validateRelationTargets(mergedInput, tenantId);
1776
2448
  }
2449
+ const oldValues = {
2450
+ label: dbAttr.label,
2451
+ required: dbAttr.required
2452
+ };
1777
2453
  const updatedDbAttr = await this.adapter.attributes.update(attributeId, {
1778
2454
  label: updates.label,
1779
2455
  required: updates.required,
1780
2456
  config
1781
2457
  });
2458
+ if (this.auditService && this.userId) {
2459
+ const changes = buildAuditChanges(oldValues, updates, ["label", "required"]);
2460
+ if (changes.length > 0) {
2461
+ const dbObject = await this.adapter.objects.findById(dbAttr.objectId);
2462
+ await this.auditService.logSchemaAction({
2463
+ action: "attribute.updated",
2464
+ actorId: this.userId,
2465
+ actorEmail: this.userEmail,
2466
+ resourceType: "attribute",
2467
+ resourceId: attributeId,
2468
+ resourceLabel: updatedDbAttr.label,
2469
+ objectName: dbObject?.name,
2470
+ objectId: dbAttr.objectId,
2471
+ changes
2472
+ });
2473
+ }
2474
+ }
2475
+ if (updates.required !== void 0 && updates.required !== oldValues.required) {
2476
+ const schema = await this.getObjectSchema(dbAttr.objectId);
2477
+ await this.adapter.objectRecords.batchRefreshStatus(
2478
+ dbAttr.objectId,
2479
+ (values) => computeRecordStatus(schema, values)
2480
+ );
2481
+ }
1782
2482
  return this.convertDBAttributeToAttribute(updatedDbAttr);
1783
2483
  }
1784
2484
  /**
@@ -1797,7 +2497,27 @@ var ObjectSchemaService = class {
1797
2497
  `Cannot delete system attribute "${dbAttr.name}". System attributes are protected.`
1798
2498
  );
1799
2499
  }
2500
+ const dbObject = await this.adapter.objects.findById(dbAttr.objectId);
2501
+ if (dbObject?.labelExpression) {
2502
+ const usedAttributes = extractAttributeNames(dbObject.labelExpression);
2503
+ if (usedAttributes.includes(dbAttr.name)) {
2504
+ throw new AttributeInUseError(dbAttr.name, "labelExpression");
2505
+ }
2506
+ }
1800
2507
  await this.adapter.attributes.delete(attributeId);
2508
+ await this.adapter.objectRecords.removeAttributeData(dbAttr.objectId, dbAttr.name);
2509
+ if (this.auditService && this.userId) {
2510
+ await this.auditService.logSchemaAction({
2511
+ action: "attribute.deleted",
2512
+ actorId: this.userId,
2513
+ actorEmail: this.userEmail,
2514
+ resourceType: "attribute",
2515
+ resourceId: attributeId,
2516
+ resourceLabel: dbAttr.label,
2517
+ objectName: dbObject?.name,
2518
+ objectId: dbAttr.objectId
2519
+ });
2520
+ }
1801
2521
  }
1802
2522
  /**
1803
2523
  * List attributes for an object
@@ -1842,6 +2562,13 @@ var ObjectSchemaService = class {
1842
2562
  `Cannot modify system object "${dbObject.name}". System objects are protected.`
1843
2563
  );
1844
2564
  }
2565
+ const oldValues = {
2566
+ label: dbObject.label,
2567
+ pluralLabel: dbObject.pluralLabel,
2568
+ description: dbObject.description,
2569
+ labelExpression: dbObject.labelExpression,
2570
+ icon: dbObject.metadata?.icon
2571
+ };
1845
2572
  let metadata = dbObject.metadata;
1846
2573
  if (updates.icon !== void 0 || updates.metadata !== void 0) {
1847
2574
  metadata = {
@@ -1857,6 +2584,35 @@ var ObjectSchemaService = class {
1857
2584
  labelExpression: updates.labelExpression,
1858
2585
  metadata
1859
2586
  });
2587
+ if (this.auditService && this.userId) {
2588
+ const changes = buildAuditChanges(oldValues, { ...updates, icon: updates.icon }, [
2589
+ "label",
2590
+ "pluralLabel",
2591
+ "description",
2592
+ "labelExpression",
2593
+ "icon"
2594
+ ]);
2595
+ if (changes.length > 0) {
2596
+ await this.auditService.logSchemaAction({
2597
+ action: "object.updated",
2598
+ actorId: this.userId,
2599
+ actorEmail: this.userEmail,
2600
+ resourceType: "object",
2601
+ resourceId: objectId,
2602
+ resourceLabel: updatedDbObject.label,
2603
+ objectName: updatedDbObject.name,
2604
+ objectId,
2605
+ changes
2606
+ });
2607
+ }
2608
+ }
2609
+ if (updates.labelExpression !== void 0 && updates.labelExpression !== oldValues.labelExpression) {
2610
+ const newExpression = updates.labelExpression;
2611
+ await this.adapter.objectRecords.batchRefreshLabels(
2612
+ objectId,
2613
+ (values) => renderLabelExpression(newExpression, values)
2614
+ );
2615
+ }
1860
2616
  const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
1861
2617
  return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
1862
2618
  }
@@ -1872,7 +2628,7 @@ var ObjectSchemaService = class {
1872
2628
  * await service.deleteObject("obj-123");
1873
2629
  * ```
1874
2630
  */
1875
- async deleteObject(objectId) {
2631
+ async deleteObject(objectId, tenantId) {
1876
2632
  const dbObject = await this.adapter.objects.findById(objectId);
1877
2633
  if (!dbObject) {
1878
2634
  throw new Error(`Object with id "${objectId}" not found`);
@@ -1882,7 +2638,29 @@ var ObjectSchemaService = class {
1882
2638
  `Cannot delete system object "${dbObject.name}". System objects are protected.`
1883
2639
  );
1884
2640
  }
2641
+ const effectiveTenantId = tenantId ?? dbObject.tenantId;
2642
+ if (effectiveTenantId) {
2643
+ const referencingObjects = await this.findObjectsWithRelationTo(
2644
+ dbObject.name,
2645
+ effectiveTenantId
2646
+ );
2647
+ if (referencingObjects.length > 0) {
2648
+ throw new ObjectReferencedError(dbObject.name, referencingObjects);
2649
+ }
2650
+ }
1885
2651
  await this.adapter.objects.delete(objectId);
2652
+ if (this.auditService && this.userId) {
2653
+ await this.auditService.logSchemaAction({
2654
+ action: "object.deleted",
2655
+ actorId: this.userId,
2656
+ actorEmail: this.userEmail,
2657
+ resourceType: "object",
2658
+ resourceId: objectId,
2659
+ resourceLabel: dbObject.label,
2660
+ objectName: dbObject.name,
2661
+ objectId
2662
+ });
2663
+ }
1886
2664
  }
1887
2665
  /**
1888
2666
  * Validate attribute name format (variable identifier)
@@ -2052,14 +2830,13 @@ var ObjectSchemaService = class {
2052
2830
  * @internal
2053
2831
  */
2054
2832
  buildAttributeConfig(attribute) {
2055
- const {
2056
- name: _,
2057
- label: __,
2058
- type: ___,
2059
- required: ____,
2060
- unique: _____,
2061
- ...configInput
2062
- } = attribute;
2833
+ const DB_COLUMN_FIELDS = ["name", "label", "type", "required", "unique"];
2834
+ const configInput = {};
2835
+ for (const [key, value] of Object.entries(attribute)) {
2836
+ if (!DB_COLUMN_FIELDS.includes(key)) {
2837
+ configInput[key] = value;
2838
+ }
2839
+ }
2063
2840
  try {
2064
2841
  return parseAttributeConfig(attribute.type, configInput);
2065
2842
  } catch (error) {
@@ -2069,6 +2846,19 @@ var ObjectSchemaService = class {
2069
2846
  throw error;
2070
2847
  }
2071
2848
  }
2849
+ /**
2850
+ * Check if an object exists (in tenant, system, or native registry)
2851
+ * @internal
2852
+ */
2853
+ async objectExists(name, tenantId) {
2854
+ if (tenantId) {
2855
+ const obj = await this.adapter.objects.findByName(tenantId, name);
2856
+ if (obj) return true;
2857
+ }
2858
+ const systemObj = await this.adapter.objects.findSystemByName(name);
2859
+ if (systemObj) return true;
2860
+ return this.nativeRegistry.getByName(name) !== void 0;
2861
+ }
2072
2862
  /**
2073
2863
  * Validate that relation target objects exist
2074
2864
  * @internal
@@ -2079,26 +2869,43 @@ var ObjectSchemaService = class {
2079
2869
  throw new Error("Relation attribute must have at least one target object");
2080
2870
  }
2081
2871
  const cardinality = attribute.cardinality;
2082
- if (!(cardinality && ["one", "many"].includes(cardinality))) {
2872
+ const isValidCardinality = cardinality === "one" || cardinality === "many";
2873
+ if (!isValidCardinality) {
2083
2874
  throw new Error("Relation attribute must specify cardinality: 'one' or 'many'");
2084
2875
  }
2085
2876
  for (const target of targets) {
2086
2877
  if (!target.object) {
2087
2878
  throw new Error("Each relation target must specify an object name");
2088
2879
  }
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);
2880
+ const exists = await this.objectExists(target.object, tenantId);
2881
+ if (!exists) {
2882
+ throw new Error(
2883
+ `Relation target object "${target.object}" does not exist. Make sure the object is created before adding a relation to it.`
2884
+ );
2092
2885
  }
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
- }
2886
+ }
2887
+ }
2888
+ /**
2889
+ * Find objects that have a relation attribute targeting the given object.
2890
+ * Used to prevent deletion of objects that are relation targets.
2891
+ * @internal
2892
+ */
2893
+ async findObjectsWithRelationTo(targetObjectName, tenantId) {
2894
+ const allObjects = await this.adapter.objects.list(tenantId);
2895
+ const referencing = [];
2896
+ for (const obj of allObjects) {
2897
+ if (obj.name === targetObjectName) continue;
2898
+ const attrs = await this.adapter.attributes.findByObjectId(obj.id);
2899
+ const hasRelationToTarget = attrs.some((attr) => {
2900
+ if (attr.type !== "relation") return false;
2901
+ const config = attr.config;
2902
+ return config?.targets?.some((t) => t.object === targetObjectName) ?? false;
2903
+ });
2904
+ if (hasRelationToTarget) {
2905
+ referencing.push(obj.name);
2100
2906
  }
2101
2907
  }
2908
+ return referencing;
2102
2909
  }
2103
2910
  /**
2104
2911
  * Convert DB attribute to Attribute type
@@ -2119,6 +2926,483 @@ var ObjectSchemaService = class {
2119
2926
  }
2120
2927
  };
2121
2928
 
2929
+ // src/exceptions.ts
2930
+ var SchemaErrorCode = {
2931
+ // Generic
2932
+ UNKNOWN: "SCHEMA_UNKNOWN_ERROR",
2933
+ // Not Found
2934
+ OBJECT_NOT_FOUND: "SCHEMA_OBJECT_NOT_FOUND",
2935
+ ATTRIBUTE_NOT_FOUND: "SCHEMA_ATTRIBUTE_NOT_FOUND",
2936
+ RECORD_NOT_FOUND: "SCHEMA_RECORD_NOT_FOUND",
2937
+ USER_PROFILE_NOT_FOUND: "SCHEMA_USER_PROFILE_NOT_FOUND",
2938
+ FILE_NOT_FOUND: "SCHEMA_FILE_NOT_FOUND",
2939
+ ROLE_NOT_FOUND: "SCHEMA_ROLE_NOT_FOUND",
2940
+ // Validation
2941
+ VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED",
2942
+ INVALID_ATTRIBUTE_NAME: "SCHEMA_INVALID_ATTRIBUTE_NAME",
2943
+ INVALID_OBJECT_NAME: "SCHEMA_INVALID_OBJECT_NAME",
2944
+ // Protected Resources
2945
+ PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
2946
+ PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
2947
+ PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
2948
+ // Permissions
2949
+ FORBIDDEN: "SCHEMA_FORBIDDEN",
2950
+ // Sync
2951
+ SYNC_FAILED: "SCHEMA_SYNC_FAILED",
2952
+ NOT_SYSTEM_OBJECT: "SCHEMA_NOT_SYSTEM_OBJECT",
2953
+ // Duplicates
2954
+ DUPLICATE_OBJECT: "SCHEMA_DUPLICATE_OBJECT",
2955
+ DUPLICATE_ATTRIBUTE: "SCHEMA_DUPLICATE_ATTRIBUTE"
2956
+ };
2957
+ var SchemaError = class extends Error {
2958
+ constructor(message, code = SchemaErrorCode.UNKNOWN, details) {
2959
+ super(message);
2960
+ this.name = "SchemaError";
2961
+ this.code = code;
2962
+ this.details = details;
2963
+ Object.setPrototypeOf(this, new.target.prototype);
2964
+ }
2965
+ toJSON() {
2966
+ return {
2967
+ name: this.name,
2968
+ code: this.code,
2969
+ message: this.message,
2970
+ details: this.details
2971
+ };
2972
+ }
2973
+ };
2974
+ var ForbiddenError = class extends SchemaError {
2975
+ constructor(objectName, action, userId) {
2976
+ super(`No ${action} permission on object "${objectName}"`, SchemaErrorCode.FORBIDDEN, {
2977
+ objectName,
2978
+ action,
2979
+ userId
2980
+ });
2981
+ this.name = "ForbiddenError";
2982
+ this.objectName = objectName;
2983
+ this.action = action;
2984
+ this.userId = userId;
2985
+ }
2986
+ };
2987
+
2988
+ // src/runtime/services/permission.service.ts
2989
+ var PermissionService = class {
2990
+ constructor(adapter, tenantId, options) {
2991
+ this.adapter = adapter;
2992
+ this.tenantId = tenantId;
2993
+ this.cache = /* @__PURE__ */ new Map();
2994
+ if (!adapter.permissions) {
2995
+ throw new Error(
2996
+ "PermissionService requires a DatabaseAdapter with permissions repository. Make sure your adapter implements the permissions property."
2997
+ );
2998
+ }
2999
+ this.permissionsRepo = adapter.permissions;
3000
+ this.cacheTtlMs = options?.cacheTtlMs ?? 6e4;
3001
+ this.maxCacheSize = options?.maxCacheSize ?? 1e4;
3002
+ this.auditService = options?.auditService;
3003
+ this.userId = options?.userId;
3004
+ this.userEmail = options?.userEmail;
3005
+ }
3006
+ // ============================================================================
3007
+ // PERMISSION CHECKS
3008
+ // ============================================================================
3009
+ /**
3010
+ * Check if user can perform an action on an object.
3011
+ *
3012
+ * @param userProfileId - User profile ID to check
3013
+ * @param objectName - Object name to check access for
3014
+ * @param action - Action to check (read, create, update, delete)
3015
+ * @returns true if user has permission, false otherwise
3016
+ */
3017
+ async canAccessObject(userProfileId, objectName, action) {
3018
+ const permissions = await this.getEffectivePermissions(userProfileId);
3019
+ if (permissions.isAdmin) {
3020
+ return true;
3021
+ }
3022
+ const wildcardPerms = permissions.objectPermissions["*"];
3023
+ if (wildcardPerms?.includes(action)) {
3024
+ return true;
3025
+ }
3026
+ const objectPerms = permissions.objectPermissions[objectName];
3027
+ return objectPerms?.includes(action) ?? false;
3028
+ }
3029
+ /**
3030
+ * Check if user can access an object, throw ForbiddenError if not.
3031
+ *
3032
+ * @param userProfileId - User profile ID to check
3033
+ * @param objectName - Object name to check access for
3034
+ * @param action - Action to check
3035
+ * @throws ForbiddenError if user lacks permission
3036
+ */
3037
+ async checkObjectAccess(userProfileId, objectName, action) {
3038
+ const canAccess = await this.canAccessObject(userProfileId, objectName, action);
3039
+ if (!canAccess) {
3040
+ throw new ForbiddenError(objectName, action, userProfileId);
3041
+ }
3042
+ }
3043
+ /**
3044
+ * Get permissions for a specific object as boolean flags.
3045
+ * Optimized to fetch permissions once and compute all flags.
3046
+ *
3047
+ * @param userProfileId - User profile ID to check
3048
+ * @param objectName - Object name to check
3049
+ * @returns Object with canRead, canCreate, canUpdate, canDelete flags
3050
+ */
3051
+ async getObjectPermissions(userProfileId, objectName) {
3052
+ const permissions = await this.getEffectivePermissions(userProfileId);
3053
+ if (permissions.isAdmin) {
3054
+ return { canRead: true, canCreate: true, canUpdate: true, canDelete: true };
3055
+ }
3056
+ const wildcardPerms = permissions.objectPermissions["*"] ?? [];
3057
+ const objectPerms = permissions.objectPermissions[objectName] ?? [];
3058
+ const allPerms = /* @__PURE__ */ new Set([...wildcardPerms, ...objectPerms]);
3059
+ return {
3060
+ canRead: allPerms.has("read"),
3061
+ canCreate: allPerms.has("create"),
3062
+ canUpdate: allPerms.has("update"),
3063
+ canDelete: allPerms.has("delete")
3064
+ };
3065
+ }
3066
+ // ============================================================================
3067
+ // SYSTEM PERMISSION CHECKS
3068
+ // ============================================================================
3069
+ /**
3070
+ * Check if user can perform an action on a system resource.
3071
+ *
3072
+ * @param userProfileId - User profile ID to check
3073
+ * @param resource - System resource (users, schema, roles, settings)
3074
+ * @param action - Action to check (read, create, update, delete)
3075
+ * @returns true if user has permission, false otherwise
3076
+ */
3077
+ async canAccessSystem(userProfileId, resource, action) {
3078
+ const permissions = await this.getEffectivePermissions(userProfileId);
3079
+ if (permissions.isAdmin) {
3080
+ return true;
3081
+ }
3082
+ const wildcardPerms = permissions.systemPermissions?.["*"];
3083
+ if (wildcardPerms?.includes(action)) {
3084
+ return true;
3085
+ }
3086
+ const resourcePerms = permissions.systemPermissions?.[resource];
3087
+ return resourcePerms?.includes(action) ?? false;
3088
+ }
3089
+ /**
3090
+ * Check if user can access a system resource, throw ForbiddenError if not.
3091
+ *
3092
+ * @param userProfileId - User profile ID to check
3093
+ * @param resource - System resource to check access for
3094
+ * @param action - Action to check
3095
+ * @throws ForbiddenError if user lacks permission
3096
+ */
3097
+ async checkSystemAccess(userProfileId, resource, action) {
3098
+ const canAccess = await this.canAccessSystem(userProfileId, resource, action);
3099
+ if (!canAccess) {
3100
+ throw new ForbiddenError(`_${resource}`, action, userProfileId);
3101
+ }
3102
+ }
3103
+ /**
3104
+ * Get permissions for a specific system resource as boolean flags.
3105
+ * Optimized to fetch permissions once and compute all flags.
3106
+ *
3107
+ * @param userProfileId - User profile ID to check
3108
+ * @param resource - System resource to check
3109
+ * @returns Object with canRead, canCreate, canUpdate, canDelete flags
3110
+ */
3111
+ async getSystemPermissions(userProfileId, resource) {
3112
+ const permissions = await this.getEffectivePermissions(userProfileId);
3113
+ if (permissions.isAdmin) {
3114
+ return { canRead: true, canCreate: true, canUpdate: true, canDelete: true };
3115
+ }
3116
+ const wildcardPerms = permissions.systemPermissions?.["*"] ?? [];
3117
+ const resourcePerms = permissions.systemPermissions?.[resource] ?? [];
3118
+ const allPerms = /* @__PURE__ */ new Set([...wildcardPerms, ...resourcePerms]);
3119
+ return {
3120
+ canRead: allPerms.has("read"),
3121
+ canCreate: allPerms.has("create"),
3122
+ canUpdate: allPerms.has("update"),
3123
+ canDelete: allPerms.has("delete")
3124
+ };
3125
+ }
3126
+ // ============================================================================
3127
+ // EFFECTIVE PERMISSIONS
3128
+ // ============================================================================
3129
+ /**
3130
+ * Get effective permissions for a user.
3131
+ * Results are cached with TTL and size limit.
3132
+ *
3133
+ * @param userProfileId - User profile ID
3134
+ * @returns Merged permissions from all user's roles
3135
+ */
3136
+ async getEffectivePermissions(userProfileId) {
3137
+ const cacheKey = this.getCacheKey(userProfileId);
3138
+ const cached = this.cache.get(cacheKey);
3139
+ if (cached && cached.expiresAt > Date.now()) {
3140
+ return cached.permissions;
3141
+ }
3142
+ const permissions = await this.permissionsRepo.getEffectivePermissions(
3143
+ userProfileId,
3144
+ this.tenantId
3145
+ );
3146
+ if (this.cache.size >= this.maxCacheSize) {
3147
+ const firstKey = this.cache.keys().next().value;
3148
+ if (firstKey) {
3149
+ this.cache.delete(firstKey);
3150
+ }
3151
+ }
3152
+ this.cache.set(cacheKey, {
3153
+ permissions,
3154
+ expiresAt: Date.now() + this.cacheTtlMs
3155
+ });
3156
+ return permissions;
3157
+ }
3158
+ // ============================================================================
3159
+ // CACHE MANAGEMENT
3160
+ // ============================================================================
3161
+ /**
3162
+ * Invalidate cached permissions for a specific user.
3163
+ * Call this after role/permission changes.
3164
+ */
3165
+ invalidateCache(userProfileId) {
3166
+ this.cache.delete(this.getCacheKey(userProfileId));
3167
+ }
3168
+ /**
3169
+ * Invalidate all cached permissions.
3170
+ * Call this after bulk role/permission changes.
3171
+ */
3172
+ invalidateAllCache() {
3173
+ this.cache.clear();
3174
+ }
3175
+ /**
3176
+ * Get current cache size (for monitoring).
3177
+ */
3178
+ getCacheSize() {
3179
+ return this.cache.size;
3180
+ }
3181
+ getCacheKey(userProfileId) {
3182
+ return `${this.tenantId}:${userProfileId}`;
3183
+ }
3184
+ // ============================================================================
3185
+ // ROLE MANAGEMENT
3186
+ // ============================================================================
3187
+ /**
3188
+ * Get all roles for the current tenant.
3189
+ */
3190
+ async getRoles() {
3191
+ return await this.permissionsRepo.getRolesByTenant(this.tenantId);
3192
+ }
3193
+ /**
3194
+ * Get a role by ID.
3195
+ */
3196
+ async getRoleById(roleId) {
3197
+ return await this.permissionsRepo.getRoleById(roleId);
3198
+ }
3199
+ /**
3200
+ * Create a new role.
3201
+ */
3202
+ async createRole(input) {
3203
+ const role = await this.permissionsRepo.createRole({
3204
+ ...input,
3205
+ tenantId: this.tenantId
3206
+ });
3207
+ if (this.auditService && this.userId) {
3208
+ await this.auditService.logRoleAction({
3209
+ action: "role.created",
3210
+ actorId: this.userId,
3211
+ actorEmail: this.userEmail,
3212
+ roleId: role.id,
3213
+ roleLabel: role.label
3214
+ });
3215
+ }
3216
+ return role;
3217
+ }
3218
+ /**
3219
+ * Update a role.
3220
+ */
3221
+ async updateRole(roleId, updates) {
3222
+ const oldRole = await this.permissionsRepo.getRoleById(roleId);
3223
+ const role = await this.permissionsRepo.updateRole(roleId, updates);
3224
+ if (this.auditService && this.userId && oldRole) {
3225
+ const changes = [];
3226
+ if (updates.label !== void 0 && updates.label !== oldRole.label) {
3227
+ changes.push({ field: "label", oldValue: oldRole.label, newValue: updates.label });
3228
+ }
3229
+ if (updates.description !== void 0 && updates.description !== oldRole.description) {
3230
+ changes.push({
3231
+ field: "description",
3232
+ oldValue: oldRole.description,
3233
+ newValue: updates.description
3234
+ });
3235
+ }
3236
+ if (changes.length > 0) {
3237
+ await this.auditService.logRoleAction({
3238
+ action: "role.updated",
3239
+ actorId: this.userId,
3240
+ actorEmail: this.userEmail,
3241
+ roleId: role.id,
3242
+ roleLabel: role.label,
3243
+ changes
3244
+ });
3245
+ }
3246
+ }
3247
+ return role;
3248
+ }
3249
+ /**
3250
+ * Delete a role.
3251
+ */
3252
+ async deleteRole(roleId) {
3253
+ const role = await this.permissionsRepo.getRoleById(roleId);
3254
+ await this.permissionsRepo.deleteRole(roleId);
3255
+ this.invalidateAllCache();
3256
+ if (this.auditService && this.userId && role) {
3257
+ await this.auditService.logRoleAction({
3258
+ action: "role.deleted",
3259
+ actorId: this.userId,
3260
+ actorEmail: this.userEmail,
3261
+ roleId,
3262
+ roleLabel: role.label
3263
+ });
3264
+ }
3265
+ }
3266
+ // ============================================================================
3267
+ // PERMISSION MANAGEMENT
3268
+ // ============================================================================
3269
+ /**
3270
+ * Get all permissions for a role.
3271
+ */
3272
+ async getPermissionsByRole(roleId) {
3273
+ return await this.permissionsRepo.getPermissionsByRole(roleId);
3274
+ }
3275
+ /**
3276
+ * Set permissions for a role (replaces existing).
3277
+ */
3278
+ async setPermissions(roleId, permissions) {
3279
+ await this.permissionsRepo.setPermissions(roleId, permissions);
3280
+ this.invalidateAllCache();
3281
+ if (this.auditService && this.userId) {
3282
+ const role = await this.permissionsRepo.getRoleById(roleId);
3283
+ await this.auditService.logRoleAction({
3284
+ action: "role.updated",
3285
+ actorId: this.userId,
3286
+ actorEmail: this.userEmail,
3287
+ roleId,
3288
+ roleLabel: role?.label ?? roleId,
3289
+ metadata: { permissionsUpdated: true, permissionCount: permissions.length }
3290
+ });
3291
+ }
3292
+ }
3293
+ // ============================================================================
3294
+ // USER ROLE MANAGEMENT
3295
+ // ============================================================================
3296
+ /**
3297
+ * Get all roles assigned to a user.
3298
+ */
3299
+ async getUserRoles(userProfileId) {
3300
+ return await this.permissionsRepo.getUserRoles(userProfileId, this.tenantId);
3301
+ }
3302
+ /**
3303
+ * Assign a role to a user.
3304
+ */
3305
+ async assignRole(userProfileId, roleId, assignedBy) {
3306
+ const result = await this.permissionsRepo.assignRole({
3307
+ userProfileId,
3308
+ roleId,
3309
+ tenantId: this.tenantId,
3310
+ assignedBy
3311
+ });
3312
+ this.invalidateCache(userProfileId);
3313
+ if (this.auditService && this.userId) {
3314
+ const role = await this.permissionsRepo.getRoleById(roleId);
3315
+ await this.auditService.logRoleAction({
3316
+ action: "role.assigned",
3317
+ actorId: this.userId,
3318
+ actorEmail: this.userEmail,
3319
+ roleId,
3320
+ roleLabel: role?.label ?? roleId,
3321
+ targetUserId: userProfileId
3322
+ });
3323
+ }
3324
+ return result;
3325
+ }
3326
+ /**
3327
+ * Revoke a role from a user.
3328
+ */
3329
+ async revokeRole(userProfileId, roleId) {
3330
+ const role = await this.permissionsRepo.getRoleById(roleId);
3331
+ await this.permissionsRepo.revokeRole(userProfileId, roleId, this.tenantId);
3332
+ this.invalidateCache(userProfileId);
3333
+ if (this.auditService && this.userId) {
3334
+ await this.auditService.logRoleAction({
3335
+ action: "role.revoked",
3336
+ actorId: this.userId,
3337
+ actorEmail: this.userEmail,
3338
+ roleId,
3339
+ roleLabel: role?.label ?? roleId,
3340
+ targetUserId: userProfileId
3341
+ });
3342
+ }
3343
+ }
3344
+ // ============================================================================
3345
+ // INITIALIZATION
3346
+ // ============================================================================
3347
+ /**
3348
+ * Initialize default roles for the tenant if they don't exist.
3349
+ *
3350
+ * Creates the following roles with their default permissions:
3351
+ * - **admin**: Full access to all system resources and objects
3352
+ * - **member**: No system access, can read/create/update objects (no delete)
3353
+ * - **guest**: No system access, read-only access to objects
3354
+ *
3355
+ * This method is idempotent - it only creates roles that don't already exist.
3356
+ * Should be called during tenant bootstrap or on first admin login.
3357
+ *
3358
+ * @example
3359
+ * ```typescript
3360
+ * const service = new PermissionService(adapter, tenantId);
3361
+ * await service.initializeDefaultRoles();
3362
+ * ```
3363
+ */
3364
+ async initializeDefaultRoles() {
3365
+ const {
3366
+ DEFAULT_ROLES: DEFAULT_ROLES2,
3367
+ DEFAULT_ROLE_LABELS: DEFAULT_ROLE_LABELS2,
3368
+ DEFAULT_ROLE_DESCRIPTIONS: DEFAULT_ROLE_DESCRIPTIONS2,
3369
+ DEFAULT_ROLE_PERMISSIONS: DEFAULT_ROLE_PERMISSIONS2
3370
+ } = await Promise.resolve().then(() => (init_default_roles(), default_roles_exports));
3371
+ const existingRoles = await this.getRoles();
3372
+ const existingRoleNames = new Set(existingRoles.map((r) => r.name));
3373
+ for (const roleName of Object.values(DEFAULT_ROLES2)) {
3374
+ if (existingRoleNames.has(roleName)) {
3375
+ continue;
3376
+ }
3377
+ const role = await this.permissionsRepo.createRole({
3378
+ tenantId: this.tenantId,
3379
+ name: roleName,
3380
+ label: DEFAULT_ROLE_LABELS2[roleName],
3381
+ description: DEFAULT_ROLE_DESCRIPTIONS2[roleName]
3382
+ });
3383
+ const permConfig = DEFAULT_ROLE_PERMISSIONS2[roleName];
3384
+ const permissionInputs = [];
3385
+ for (const perm of permConfig.system) {
3386
+ permissionInputs.push({
3387
+ scope: "system",
3388
+ target: perm.target,
3389
+ actions: perm.actions
3390
+ });
3391
+ }
3392
+ for (const perm of permConfig.object) {
3393
+ permissionInputs.push({
3394
+ scope: "object",
3395
+ target: perm.target,
3396
+ actions: perm.actions
3397
+ });
3398
+ }
3399
+ if (permissionInputs.length > 0) {
3400
+ await this.setPermissions(role.id, permissionInputs);
3401
+ }
3402
+ }
3403
+ }
3404
+ };
3405
+
2122
3406
  // src/native/registry.ts
2123
3407
  var NativeObjectRegistryClass = class {
2124
3408
  constructor() {
@@ -2439,8 +3723,7 @@ var RelationService = class {
2439
3723
  objectName: objectSchema.name,
2440
3724
  objectLabel: objectSchema.label,
2441
3725
  objectIcon: objectSchema.icon,
2442
- label,
2443
- data: record.values
3726
+ label
2444
3727
  });
2445
3728
  }
2446
3729
  }
@@ -2455,40 +3738,58 @@ var RelationService = class {
2455
3738
  * Resolve record IDs to their display labels
2456
3739
  * Useful for displaying current values in the UI
2457
3740
  *
3741
+ * Uses batch fetching for performance - fetches all records in one query,
3742
+ * then groups by objectId to minimize schema lookups.
3743
+ *
2458
3744
  * @param ids - Record IDs to resolve
2459
- * @param tenantId - Tenant ID for multi-tenant isolation
3745
+ * @param _tenantId - Tenant ID for multi-tenant isolation (unused but kept for API compatibility)
3746
+ * @param attributeId - Relation attribute ID to use its displayTemplate for label rendering
2460
3747
  *
2461
3748
  * @example
2462
3749
  * ```typescript
2463
- * const resolved = await relationService.resolveIds(["rec-1", "rec-2"], "tenant-1");
3750
+ * const resolved = await relationService.resolveIds(["rec-1", "rec-2"], "tenant-1", "attr-123");
2464
3751
  * // [{ id: "rec-1", label: "Nike Air Max", objectName: "products", ... }]
2465
3752
  * ```
2466
3753
  */
2467
- async resolveIds(ids, tenantId) {
3754
+ async resolveIds(ids, _tenantId, attributeId) {
2468
3755
  if (!ids || ids.length === 0) {
2469
3756
  return [];
2470
3757
  }
2471
- const recordService = new RecordService(this.adapter, tenantId);
3758
+ const attribute = await this.findAttributeById(attributeId);
3759
+ const records = await this.adapter.objectRecords.findByIds(ids);
3760
+ if (records.length === 0) {
3761
+ return [];
3762
+ }
3763
+ const recordsByObjectId = /* @__PURE__ */ new Map();
3764
+ for (const record of records) {
3765
+ const existing = recordsByObjectId.get(record.objectId) ?? [];
3766
+ existing.push(record);
3767
+ recordsByObjectId.set(record.objectId, existing);
3768
+ }
2472
3769
  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);
3770
+ for (const [objectId, objectRecords] of recordsByObjectId) {
3771
+ const objectSchema = await this.schemaService.getObjectSchema(objectId);
2479
3772
  if (!objectSchema) {
2480
3773
  continue;
2481
3774
  }
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
- });
3775
+ let template = objectSchema.labelExpression;
3776
+ if (attribute?.targets) {
3777
+ const targetConfig = attribute.targets.find((t) => t.object === objectSchema.name);
3778
+ if (targetConfig?.displayTemplate) {
3779
+ template = targetConfig.displayTemplate;
3780
+ }
3781
+ }
3782
+ for (const record of objectRecords) {
3783
+ const label = renderLabelExpression(template, record.values);
3784
+ resolved.push({
3785
+ id: record.id,
3786
+ objectId: record.objectId,
3787
+ objectName: objectSchema.name,
3788
+ objectLabel: objectSchema.label,
3789
+ objectIcon: objectSchema.icon,
3790
+ label
3791
+ });
3792
+ }
2492
3793
  }
2493
3794
  return resolved;
2494
3795
  }
@@ -2515,6 +3816,70 @@ var RecordService = class {
2515
3816
  this.schemaService = new ObjectSchemaService(adapter, registry);
2516
3817
  this.relationService = new RelationService(adapter, registry);
2517
3818
  this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
3819
+ this.permissionService = options?.permissionService;
3820
+ this.auditService = options?.auditService;
3821
+ this.userId = options?.userId;
3822
+ this.userEmail = options?.userEmail;
3823
+ }
3824
+ /**
3825
+ * Check permission for an action on an object.
3826
+ * Only checks if permissionService and userId are configured.
3827
+ * @internal
3828
+ */
3829
+ async checkPermission(objectName, action) {
3830
+ if (this.permissionService && this.userId) {
3831
+ await this.permissionService.checkObjectAccess(this.userId, objectName, action);
3832
+ }
3833
+ }
3834
+ /**
3835
+ * Resolve relation IDs to their display labels
3836
+ * @internal
3837
+ */
3838
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complexity is ok here
3839
+ async resolveRelationLabels(relationAttrs, values) {
3840
+ const resolvedMap = /* @__PURE__ */ new Map();
3841
+ const idsWithAttrId = [];
3842
+ const idsWithoutAttrId = [];
3843
+ for (const attr of relationAttrs) {
3844
+ const val = values[attr.name];
3845
+ const ids = this.extractRelationIds(val);
3846
+ if (ids.length > 0) {
3847
+ if (attr.id) {
3848
+ idsWithAttrId.push({ attrId: attr.id, ids });
3849
+ } else {
3850
+ idsWithoutAttrId.push(...ids);
3851
+ }
3852
+ }
3853
+ }
3854
+ await Promise.all(
3855
+ idsWithAttrId.map(async ({ attrId, ids }) => {
3856
+ const resolved = await this.relationService.resolveIds(ids, this.tenantId, attrId);
3857
+ for (const r of resolved) {
3858
+ resolvedMap.set(r.id, r.label);
3859
+ }
3860
+ })
3861
+ );
3862
+ if (idsWithoutAttrId.length > 0) {
3863
+ const uniqueIds = [...new Set(idsWithoutAttrId)].filter((id) => !resolvedMap.has(id));
3864
+ if (uniqueIds.length > 0) {
3865
+ const records = await this.adapter.objectRecords.findByIds(uniqueIds);
3866
+ for (const record of records) {
3867
+ if (record.label) {
3868
+ resolvedMap.set(record.id, record.label);
3869
+ }
3870
+ }
3871
+ }
3872
+ }
3873
+ return resolvedMap;
3874
+ }
3875
+ /**
3876
+ * Extract relation IDs from a value (string or array)
3877
+ * @internal
3878
+ */
3879
+ extractRelationIds(val) {
3880
+ if (typeof val === "string") return [val];
3881
+ if (Array.isArray(val) && typeof val[0] === "string") return [val[0]];
3882
+ return [];
2518
3883
  }
2519
3884
  /**
2520
3885
  * Compute display label from schema expression
@@ -2529,27 +3894,16 @@ var RecordService = class {
2529
3894
  if (relationAttrs.length === 0) {
2530
3895
  return renderLabelExpression(schema.labelExpression, values);
2531
3896
  }
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) {
3897
+ const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
3898
+ if (resolvedMap.size === 0) {
2542
3899
  return renderLabelExpression(schema.labelExpression, values);
2543
3900
  }
2544
- const resolved = await this.relationService.resolveIds(idsToResolve, this.tenantId);
2545
- const resolvedMap = new Map(resolved.map((r) => [r.id, r.label]));
2546
3901
  const enrichedValues = { ...values };
2547
3902
  for (const attr of relationAttrs) {
2548
3903
  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]);
3904
+ const ids = this.extractRelationIds(val);
3905
+ if (ids.length > 0 && resolvedMap.has(ids[0])) {
3906
+ enrichedValues[attr.name] = resolvedMap.get(ids[0]);
2553
3907
  }
2554
3908
  }
2555
3909
  return renderLabelExpression(schema.labelExpression, enrichedValues);
@@ -2585,6 +3939,7 @@ var RecordService = class {
2585
3939
  */
2586
3940
  async createRecord(objectId, data, options) {
2587
3941
  const schema = await this.schemaService.getObjectSchema(objectId);
3942
+ await this.checkPermission(schema.name, "create");
2588
3943
  const hookCtx = this.buildCreateHookContext(schema, data, options?.hookMetadata);
2589
3944
  if (!options?.skipHooks) {
2590
3945
  await this.hookRegistry.execute("beforeCreate", schema.name, hookCtx);
@@ -2616,6 +3971,18 @@ var RecordService = class {
2616
3971
  };
2617
3972
  await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
2618
3973
  }
3974
+ if (this.auditService && this.userId) {
3975
+ await this.auditService.logRecordAction({
3976
+ action: "record.created",
3977
+ actorId: this.userId,
3978
+ actorEmail: this.userEmail,
3979
+ objectName: schema.name,
3980
+ objectId: schema.id,
3981
+ recordId: record.id,
3982
+ recordLabel: record.label,
3983
+ metadata: options?.hookMetadata
3984
+ });
3985
+ }
2619
3986
  return record;
2620
3987
  }
2621
3988
  /**
@@ -2674,6 +4041,7 @@ var RecordService = class {
2674
4041
  async updateRecord(recordId, data, options) {
2675
4042
  const existing = await this.getRecordOrThrow(recordId);
2676
4043
  const schema = await this.schemaService.getObjectSchema(existing.objectId);
4044
+ await this.checkPermission(schema.name, "update");
2677
4045
  const mergedData = { ...existing.values, ...data };
2678
4046
  const changedAttributes = Object.keys(data).filter((key) => existing.values[key] !== data[key]);
2679
4047
  const hookCtx = this.buildHookContext(
@@ -2711,6 +4079,24 @@ var RecordService = class {
2711
4079
  };
2712
4080
  await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
2713
4081
  }
4082
+ if (this.auditService && this.userId && changedAttributes.length > 0) {
4083
+ const changes = changedAttributes.map((attr) => ({
4084
+ field: attr,
4085
+ oldValue: hookCtx.oldValues?.[attr],
4086
+ newValue: data[attr]
4087
+ }));
4088
+ await this.auditService.logRecordAction({
4089
+ action: "record.updated",
4090
+ actorId: this.userId,
4091
+ actorEmail: this.userEmail,
4092
+ objectName: schema.name,
4093
+ objectId: schema.id,
4094
+ recordId: updated.id,
4095
+ recordLabel: updated.label,
4096
+ changes,
4097
+ metadata: options?.hookMetadata
4098
+ });
4099
+ }
2714
4100
  return updated;
2715
4101
  }
2716
4102
  /**
@@ -2800,11 +4186,18 @@ var RecordService = class {
2800
4186
  async deleteRecord(recordId, options) {
2801
4187
  const record = await this.getRecordOrThrow(recordId);
2802
4188
  const schema = await this.schemaService.getObjectSchema(record.objectId);
4189
+ await this.checkPermission(schema.name, "delete");
2803
4190
  if (options?.checkSystem) {
2804
4191
  if (schema.system) {
2805
4192
  throw new Error(`Cannot delete record of system object "${schema.label}"`);
2806
4193
  }
2807
4194
  }
4195
+ if (!options?.skipReferenceCheck) {
4196
+ const references = await this.adapter.objectRecords.countRecordsReferencingId(recordId);
4197
+ if (references.length > 0) {
4198
+ throw new RecordReferencedError(recordId, references);
4199
+ }
4200
+ }
2808
4201
  const hookCtx = this.buildDeleteHookContext(schema, record, options?.hookMetadata);
2809
4202
  if (!options?.skipHooks) {
2810
4203
  await this.hookRegistry.execute("beforeDelete", schema.name, hookCtx);
@@ -2813,6 +4206,110 @@ var RecordService = class {
2813
4206
  if (!options?.skipHooks) {
2814
4207
  await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
2815
4208
  }
4209
+ if (this.auditService && this.userId) {
4210
+ await this.auditService.logRecordAction({
4211
+ action: "record.deleted",
4212
+ actorId: this.userId,
4213
+ actorEmail: this.userEmail,
4214
+ objectName: schema.name,
4215
+ objectId: schema.id,
4216
+ recordId: record.id,
4217
+ recordLabel: record.label,
4218
+ metadata: options?.hookMetadata
4219
+ });
4220
+ }
4221
+ }
4222
+ /**
4223
+ * Restore a soft-deleted record
4224
+ *
4225
+ * Triggers beforeRestore and afterRestore hooks if a HookRegistry is configured.
4226
+ *
4227
+ * @param recordId - Record UUID
4228
+ * @param options - Restore options
4229
+ * @returns Restored record
4230
+ *
4231
+ * @example
4232
+ * ```typescript
4233
+ * // Restore a deleted record
4234
+ * const restored = await service.restoreRecord("rec-123");
4235
+ * console.log(restored.deletedAt); // null
4236
+ * ```
4237
+ */
4238
+ async restoreRecord(recordId, options) {
4239
+ const record = await this.getRecordOrThrow(recordId);
4240
+ if (!record.deletedAt) {
4241
+ throw new Error(`Record "${recordId}" is not deleted`);
4242
+ }
4243
+ const schema = await this.schemaService.getObjectSchema(record.objectId);
4244
+ await this.checkPermission(schema.name, "update");
4245
+ const hookCtx = this.buildRestoreHookContext(schema, record, options?.hookMetadata);
4246
+ if (!options?.skipHooks) {
4247
+ await this.hookRegistry.execute("beforeRestore", schema.name, hookCtx);
4248
+ }
4249
+ const restored = await this.adapter.objectRecords.restore(recordId);
4250
+ if (!options?.skipHooks) {
4251
+ const afterCtx = {
4252
+ ...hookCtx,
4253
+ record: restored
4254
+ };
4255
+ await this.hookRegistry.execute("afterRestore", schema.name, afterCtx);
4256
+ }
4257
+ if (this.auditService && this.userId) {
4258
+ await this.auditService.logRecordAction({
4259
+ action: "record.restored",
4260
+ actorId: this.userId,
4261
+ actorEmail: this.userEmail,
4262
+ objectName: schema.name,
4263
+ objectId: schema.id,
4264
+ recordId: restored.id,
4265
+ recordLabel: restored.label,
4266
+ metadata: options?.hookMetadata
4267
+ });
4268
+ }
4269
+ return restored;
4270
+ }
4271
+ /**
4272
+ * Build hook context for restore operations
4273
+ * @internal
4274
+ */
4275
+ buildRestoreHookContext(schema, record, metadata) {
4276
+ return {
4277
+ objectId: schema.id,
4278
+ objectName: schema.name,
4279
+ recordId: record.id,
4280
+ tenantId: this.tenantId,
4281
+ record,
4282
+ oldValues: record.values,
4283
+ newValues: record.values,
4284
+ changedAttributes: [],
4285
+ getChange: (attr) => ({
4286
+ oldValue: record.values[attr],
4287
+ newValue: record.values[attr],
4288
+ changed: false
4289
+ }),
4290
+ metadata: metadata ?? {},
4291
+ timestamp: /* @__PURE__ */ new Date()
4292
+ };
4293
+ }
4294
+ /**
4295
+ * Permanently delete a record (hard delete)
4296
+ *
4297
+ * This cannot be undone. Use with caution - prefer soft delete for data safety.
4298
+ * Does NOT trigger delete hooks (already triggered on soft delete).
4299
+ *
4300
+ * @param recordId - Record UUID
4301
+ *
4302
+ * @example
4303
+ * ```typescript
4304
+ * // Permanently delete a record
4305
+ * await service.hardDeleteRecord("rec-123");
4306
+ * ```
4307
+ */
4308
+ async hardDeleteRecord(recordId) {
4309
+ const record = await this.getRecordOrThrow(recordId);
4310
+ const schema = await this.schemaService.getObjectSchema(record.objectId);
4311
+ await this.checkPermission(schema.name, "delete");
4312
+ await this.adapter.objectRecords.hardDelete(recordId);
2816
4313
  }
2817
4314
  /**
2818
4315
  * List records for an object with pagination
@@ -2822,6 +4319,10 @@ var RecordService = class {
2822
4319
  * @returns Records and total count
2823
4320
  */
2824
4321
  async listRecords(objectId, options) {
4322
+ if (this.permissionService && this.userId) {
4323
+ const schema = await this.schemaService.getObjectSchema(objectId);
4324
+ await this.checkPermission(schema.name, "read");
4325
+ }
2825
4326
  return await this.adapter.objectRecords.list(this.tenantId, objectId, options);
2826
4327
  }
2827
4328
  /**
@@ -2833,6 +4334,10 @@ var RecordService = class {
2833
4334
  * @returns Matching records and total count
2834
4335
  */
2835
4336
  async searchRecords(objectId, query, options) {
4337
+ if (this.permissionService && this.userId) {
4338
+ const schema = await this.schemaService.getObjectSchema(objectId);
4339
+ await this.checkPermission(schema.name, "read");
4340
+ }
2836
4341
  return await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
2837
4342
  }
2838
4343
  /**
@@ -2880,9 +4385,12 @@ var RecordService = class {
2880
4385
 
2881
4386
  // src/runtime/services/user-profile.service.ts
2882
4387
  var UserProfileService = class {
2883
- constructor(adapter, tenantId) {
4388
+ constructor(adapter, tenantId, options) {
2884
4389
  this.adapter = adapter;
2885
4390
  this.tenantId = tenantId;
4391
+ this.auditService = options?.auditService;
4392
+ this.userId = options?.userId;
4393
+ this.userEmail = options?.userEmail;
2886
4394
  }
2887
4395
  /**
2888
4396
  * Create a new user profile (typically after first auth)
@@ -2920,7 +4428,17 @@ var UserProfileService = class {
2920
4428
  if (existingEmail) {
2921
4429
  throw new Error(`User profile with email "${data.email}" already exists in this tenant`);
2922
4430
  }
2923
- return await this.adapter.userProfiles.create(data);
4431
+ const profile = await this.adapter.userProfiles.create(data);
4432
+ if (this.auditService && this.userId) {
4433
+ await this.auditService.logUserAction({
4434
+ action: "user.created",
4435
+ actorId: this.userId,
4436
+ actorEmail: this.userEmail,
4437
+ targetUserId: profile.id,
4438
+ targetUserEmail: profile.email
4439
+ });
4440
+ }
4441
+ return profile;
2924
4442
  }
2925
4443
  /**
2926
4444
  * Get user profile by ID
@@ -2981,8 +4499,27 @@ var UserProfileService = class {
2981
4499
  * Update user profile
2982
4500
  */
2983
4501
  async updateProfile(profileId, data) {
2984
- await this.getProfileOrThrow(profileId);
2985
- return await this.adapter.userProfiles.update(profileId, data);
4502
+ const existing = await this.getProfileOrThrow(profileId);
4503
+ const updated = await this.adapter.userProfiles.update(profileId, data);
4504
+ if (this.auditService && this.userId) {
4505
+ const changes = buildAuditChanges(existing, data, [
4506
+ "firstName",
4507
+ "lastName",
4508
+ "role",
4509
+ "status"
4510
+ ]);
4511
+ if (changes.length > 0) {
4512
+ await this.auditService.logUserAction({
4513
+ action: "user.updated",
4514
+ actorId: this.userId,
4515
+ actorEmail: this.userEmail,
4516
+ targetUserId: profileId,
4517
+ targetUserEmail: updated.email,
4518
+ changes
4519
+ });
4520
+ }
4521
+ }
4522
+ return updated;
2986
4523
  }
2987
4524
  /**
2988
4525
  * Delete user profile
@@ -2991,17 +4528,25 @@ var UserProfileService = class {
2991
4528
  * @param options - Delete options
2992
4529
  */
2993
4530
  async deleteProfile(profileId, options) {
4531
+ const profile = await this.getProfileOrThrow(profileId);
2994
4532
  if (options?.checkAdmin) {
2995
- const profile = await this.getProfileOrThrow(profileId);
2996
4533
  if (profile.role === "admin") {
2997
- const allProfiles = await this.listProfiles();
2998
- const adminCount = allProfiles.filter((p) => p.role === "admin").length;
4534
+ const adminCount = await this.adapter.userProfiles.countByRole(this.tenantId, "admin");
2999
4535
  if (adminCount <= 1) {
3000
4536
  throw new Error("Cannot delete the last admin user");
3001
4537
  }
3002
4538
  }
3003
4539
  }
3004
4540
  await this.adapter.userProfiles.delete(profileId);
4541
+ if (this.auditService && this.userId) {
4542
+ await this.auditService.logUserAction({
4543
+ action: "user.deleted",
4544
+ actorId: this.userId,
4545
+ actorEmail: this.userEmail,
4546
+ targetUserId: profileId,
4547
+ targetUserEmail: profile.email
4548
+ });
4549
+ }
3005
4550
  }
3006
4551
  /**
3007
4552
  * List all user profiles for the tenant
@@ -3601,6 +5146,7 @@ var NoopGeocodingAdapter = class {
3601
5146
  };
3602
5147
  // Annotate the CommonJS export names for ESM import in node:
3603
5148
  0 && (module.exports = {
5149
+ AuditService,
3604
5150
  DEFAULT_LABEL_FALLBACK,
3605
5151
  FileService,
3606
5152
  GeocodingService,
@@ -3608,10 +5154,12 @@ var NoopGeocodingAdapter = class {
3608
5154
  NoopGeocodingAdapter,
3609
5155
  NoopHookRegistry,
3610
5156
  ObjectSchemaService,
5157
+ PermissionService,
3611
5158
  RecordService,
3612
5159
  RelationService,
3613
5160
  UserProfileService,
3614
5161
  ViewService,
5162
+ buildAuditChanges,
3615
5163
  createMockAdapter,
3616
5164
  extractAttributeNames,
3617
5165
  getSyncPreview,