@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.
- package/dist/chunk-2ASXJITY.mjs +5360 -0
- package/dist/chunk-5AWYVQKR.mjs +5310 -0
- package/dist/chunk-65HUQZHK.mjs +5374 -0
- package/dist/chunk-73P4F2OU.mjs +5809 -0
- package/dist/chunk-BHKKVBPP.mjs +5527 -0
- package/dist/chunk-BUWVNOSK.mjs +5457 -0
- package/dist/chunk-CCEHW4E7.mjs +6071 -0
- package/dist/chunk-CNR3BZLU.mjs +5163 -0
- package/dist/chunk-DLMVX6DN.mjs +6074 -0
- package/dist/chunk-DUUORFHI.mjs +5379 -0
- package/dist/chunk-DWTD7YPJ.mjs +74 -0
- package/dist/chunk-FAR5ENGR.mjs +78 -0
- package/dist/chunk-ITWOVZOW.mjs +5809 -0
- package/dist/chunk-KJLKY5X7.mjs +6314 -0
- package/dist/chunk-KTJXMGBR.mjs +5397 -0
- package/dist/chunk-NOVHQ7B2.mjs +6327 -0
- package/dist/chunk-PMFQ4ZL4.mjs +6230 -0
- package/dist/chunk-QC47SMCZ.mjs +5467 -0
- package/dist/chunk-Z6LIJ5PO.mjs +5362 -0
- package/dist/default-roles-G4TOWUVM.mjs +20 -0
- package/dist/default-roles-QEZPRSUL.mjs +20 -0
- package/dist/index.d.mts +193 -15
- package/dist/index.d.ts +193 -15
- package/dist/index.js +1657 -118
- package/dist/index.mjs +56 -163
- package/dist/runtime-B1_syHVj.d.mts +4787 -0
- package/dist/runtime-B1_syHVj.d.ts +4787 -0
- package/dist/runtime-B7N-oycb.d.mts +4854 -0
- package/dist/runtime-B7N-oycb.d.ts +4854 -0
- package/dist/runtime-BDoAIuTe.d.mts +4872 -0
- package/dist/runtime-BDoAIuTe.d.ts +4872 -0
- package/dist/runtime-BVRmzg3d.d.mts +4718 -0
- package/dist/runtime-BVRmzg3d.d.ts +4718 -0
- package/dist/runtime-BrXwIidf.d.mts +4794 -0
- package/dist/runtime-BrXwIidf.d.ts +4794 -0
- package/dist/runtime-CC_HZhVN.d.mts +5224 -0
- package/dist/runtime-CC_HZhVN.d.ts +5224 -0
- package/dist/runtime-CEx57sBZ.d.mts +4791 -0
- package/dist/runtime-CEx57sBZ.d.ts +4791 -0
- package/dist/runtime-COGvuXIq.d.mts +4790 -0
- package/dist/runtime-COGvuXIq.d.ts +4790 -0
- package/dist/runtime-CSGWjztI.d.mts +5166 -0
- package/dist/runtime-CSGWjztI.d.ts +5166 -0
- package/dist/runtime-DUxWyRcq.d.mts +5399 -0
- package/dist/runtime-DUxWyRcq.d.ts +5399 -0
- package/dist/runtime-LO17NDk1.d.mts +5292 -0
- package/dist/runtime-LO17NDk1.d.ts +5292 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1645 -97
- package/dist/runtime.mjs +7 -1
- 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.
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2090
|
-
if (!
|
|
2091
|
-
|
|
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
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
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
|
|
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,
|
|
3754
|
+
async resolveIds(ids, _tenantId, attributeId) {
|
|
2468
3755
|
if (!ids || ids.length === 0) {
|
|
2469
3756
|
return [];
|
|
2470
3757
|
}
|
|
2471
|
-
const
|
|
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
|
|
2474
|
-
const
|
|
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
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
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
|
|
2533
|
-
|
|
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
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|