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