@stndrds/schema 0.1.0-alpha.14 → 0.1.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2ASXJITY.mjs +5360 -0
- package/dist/chunk-5AWYVQKR.mjs +5310 -0
- package/dist/chunk-65HUQZHK.mjs +5374 -0
- package/dist/chunk-73P4F2OU.mjs +5809 -0
- package/dist/chunk-BHKKVBPP.mjs +5527 -0
- package/dist/chunk-BUWVNOSK.mjs +5457 -0
- package/dist/chunk-CCEHW4E7.mjs +6071 -0
- package/dist/chunk-CNR3BZLU.mjs +5163 -0
- package/dist/chunk-DLMVX6DN.mjs +6074 -0
- package/dist/chunk-DUUORFHI.mjs +5379 -0
- package/dist/chunk-DWTD7YPJ.mjs +74 -0
- package/dist/chunk-FAR5ENGR.mjs +78 -0
- package/dist/chunk-ITWOVZOW.mjs +5809 -0
- package/dist/chunk-KJLKY5X7.mjs +6314 -0
- package/dist/chunk-KTJXMGBR.mjs +5397 -0
- package/dist/chunk-NOVHQ7B2.mjs +6327 -0
- package/dist/chunk-PMFQ4ZL4.mjs +6230 -0
- package/dist/chunk-QC47SMCZ.mjs +5467 -0
- package/dist/chunk-Z6LIJ5PO.mjs +5362 -0
- package/dist/default-roles-G4TOWUVM.mjs +20 -0
- package/dist/default-roles-QEZPRSUL.mjs +20 -0
- package/dist/index.d.mts +193 -15
- package/dist/index.d.ts +193 -15
- package/dist/index.js +1657 -118
- package/dist/index.mjs +56 -163
- package/dist/runtime-B1_syHVj.d.mts +4787 -0
- package/dist/runtime-B1_syHVj.d.ts +4787 -0
- package/dist/runtime-B7N-oycb.d.mts +4854 -0
- package/dist/runtime-B7N-oycb.d.ts +4854 -0
- package/dist/runtime-BDoAIuTe.d.mts +4872 -0
- package/dist/runtime-BDoAIuTe.d.ts +4872 -0
- package/dist/runtime-BVRmzg3d.d.mts +4718 -0
- package/dist/runtime-BVRmzg3d.d.ts +4718 -0
- package/dist/runtime-BrXwIidf.d.mts +4794 -0
- package/dist/runtime-BrXwIidf.d.ts +4794 -0
- package/dist/runtime-CC_HZhVN.d.mts +5224 -0
- package/dist/runtime-CC_HZhVN.d.ts +5224 -0
- package/dist/runtime-CEx57sBZ.d.mts +4791 -0
- package/dist/runtime-CEx57sBZ.d.ts +4791 -0
- package/dist/runtime-COGvuXIq.d.mts +4790 -0
- package/dist/runtime-COGvuXIq.d.ts +4790 -0
- package/dist/runtime-CSGWjztI.d.mts +5166 -0
- package/dist/runtime-CSGWjztI.d.ts +5166 -0
- package/dist/runtime-DUxWyRcq.d.mts +5399 -0
- package/dist/runtime-DUxWyRcq.d.ts +5399 -0
- package/dist/runtime-LO17NDk1.d.mts +5292 -0
- package/dist/runtime-LO17NDk1.d.ts +5292 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1645 -97
- package/dist/runtime.mjs +7 -1
- package/package.json +3 -3
package/dist/index.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,15 +30,108 @@ 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/index.ts
|
|
31
119
|
var index_exports = {};
|
|
32
120
|
__export(index_exports, {
|
|
121
|
+
ALL_SYSTEM_RESOURCES: () => ALL_SYSTEM_RESOURCES,
|
|
122
|
+
AttributeInUseError: () => AttributeInUseError,
|
|
33
123
|
AttributeNotFoundError: () => AttributeNotFoundError,
|
|
124
|
+
AuditService: () => AuditService,
|
|
34
125
|
CustomTabConfig: () => CustomTabConfig,
|
|
35
126
|
DEFAULT_LABEL_FALLBACK: () => DEFAULT_LABEL_FALLBACK,
|
|
127
|
+
DEFAULT_ROLES: () => DEFAULT_ROLES,
|
|
128
|
+
DEFAULT_ROLE_DESCRIPTIONS: () => DEFAULT_ROLE_DESCRIPTIONS,
|
|
129
|
+
DEFAULT_ROLE_LABELS: () => DEFAULT_ROLE_LABELS,
|
|
130
|
+
DEFAULT_ROLE_PERMISSIONS: () => DEFAULT_ROLE_PERMISSIONS,
|
|
36
131
|
DuplicateError: () => DuplicateError,
|
|
37
132
|
FileNotFoundError: () => FileNotFoundError,
|
|
38
133
|
FileService: () => FileService,
|
|
134
|
+
ForbiddenError: () => ForbiddenError,
|
|
39
135
|
GeocodingService: () => GeocodingService,
|
|
40
136
|
GlobalSearchService: () => GlobalSearchService,
|
|
41
137
|
GroupBuilder: () => GroupBuilder,
|
|
@@ -47,11 +143,18 @@ __export(index_exports, {
|
|
|
47
143
|
OPERATORS_BY_TYPE: () => OPERATORS_BY_TYPE,
|
|
48
144
|
ObjectBuilder: () => ObjectBuilder,
|
|
49
145
|
ObjectNotFoundError: () => ObjectNotFoundError,
|
|
146
|
+
ObjectReferencedError: () => ObjectReferencedError,
|
|
50
147
|
ObjectSchemaService: () => ObjectSchemaService,
|
|
148
|
+
PermissionService: () => PermissionService,
|
|
51
149
|
ProtectedResourceError: () => ProtectedResourceError,
|
|
150
|
+
ProtectedRoleError: () => ProtectedRoleError,
|
|
52
151
|
RecordNotFoundError: () => RecordNotFoundError,
|
|
152
|
+
RecordReferencedError: () => RecordReferencedError,
|
|
53
153
|
RecordService: () => RecordService,
|
|
54
154
|
RelationService: () => RelationService,
|
|
155
|
+
RoleNotFoundError: () => RoleNotFoundError,
|
|
156
|
+
SYSTEM_RESOURCES: () => SYSTEM_RESOURCES,
|
|
157
|
+
SYSTEM_RESOURCE_LABELS: () => SYSTEM_RESOURCE_LABELS,
|
|
55
158
|
SchemaError: () => SchemaError,
|
|
56
159
|
SchemaErrorCode: () => SchemaErrorCode,
|
|
57
160
|
SyncError: () => SyncError,
|
|
@@ -63,6 +166,7 @@ __export(index_exports, {
|
|
|
63
166
|
ViewBuilder: () => ViewBuilder,
|
|
64
167
|
ViewService: () => ViewService,
|
|
65
168
|
attributeConfigSchemas: () => attributeConfigSchemas,
|
|
169
|
+
buildAuditChanges: () => buildAuditChanges,
|
|
66
170
|
checkbox: () => checkbox,
|
|
67
171
|
checkboxConfigSchema: () => checkboxConfigSchema,
|
|
68
172
|
computeRecordStatus: () => computeRecordStatus,
|
|
@@ -103,6 +207,8 @@ __export(index_exports, {
|
|
|
103
207
|
group: () => group,
|
|
104
208
|
isAdvancedFilterState: () => isAdvancedFilterState,
|
|
105
209
|
isCustomTab: () => isCustomTab,
|
|
210
|
+
isDefaultRole: () => isDefaultRole,
|
|
211
|
+
isForbiddenError: () => isForbiddenError,
|
|
106
212
|
isFormTab: () => isFormTab,
|
|
107
213
|
isLabelExpression: () => isLabelExpression,
|
|
108
214
|
isNoValueOperator: () => isNoValueOperator,
|
|
@@ -159,6 +265,38 @@ __export(index_exports, {
|
|
|
159
265
|
});
|
|
160
266
|
module.exports = __toCommonJS(index_exports);
|
|
161
267
|
|
|
268
|
+
// src/types/errors.ts
|
|
269
|
+
var RecordReferencedError = class extends Error {
|
|
270
|
+
constructor(recordId, references) {
|
|
271
|
+
const total = references.reduce((sum, r) => sum + r.count, 0);
|
|
272
|
+
super(`Cannot delete record: referenced by ${total} record${total > 1 ? "s" : ""}`);
|
|
273
|
+
this.recordId = recordId;
|
|
274
|
+
this.references = references;
|
|
275
|
+
this.code = "RECORD_REFERENCED";
|
|
276
|
+
this.name = "RecordReferencedError";
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var AttributeInUseError = class extends Error {
|
|
280
|
+
constructor(attributeName, usage) {
|
|
281
|
+
super(`Cannot delete attribute "${attributeName}": used in ${usage}`);
|
|
282
|
+
this.attributeName = attributeName;
|
|
283
|
+
this.usage = usage;
|
|
284
|
+
this.code = "ATTRIBUTE_IN_USE";
|
|
285
|
+
this.name = "AttributeInUseError";
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
var ObjectReferencedError = class extends Error {
|
|
289
|
+
constructor(objectName, referencingObjects) {
|
|
290
|
+
super(
|
|
291
|
+
`Cannot delete object "${objectName}": target of relations in ${referencingObjects.join(", ")}`
|
|
292
|
+
);
|
|
293
|
+
this.objectName = objectName;
|
|
294
|
+
this.referencingObjects = referencingObjects;
|
|
295
|
+
this.code = "OBJECT_REFERENCED";
|
|
296
|
+
this.name = "ObjectReferencedError";
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
162
300
|
// src/types/filters.ts
|
|
163
301
|
function isAdvancedFilterState(state) {
|
|
164
302
|
return "groups" in state;
|
|
@@ -293,6 +431,9 @@ function generatePrefixedId(prefix) {
|
|
|
293
431
|
return `${prefix}_${generateId()}`;
|
|
294
432
|
}
|
|
295
433
|
|
|
434
|
+
// src/constants/index.ts
|
|
435
|
+
init_default_roles();
|
|
436
|
+
|
|
296
437
|
// src/exceptions.ts
|
|
297
438
|
var SchemaErrorCode = {
|
|
298
439
|
// Generic
|
|
@@ -303,6 +444,7 @@ var SchemaErrorCode = {
|
|
|
303
444
|
RECORD_NOT_FOUND: "SCHEMA_RECORD_NOT_FOUND",
|
|
304
445
|
USER_PROFILE_NOT_FOUND: "SCHEMA_USER_PROFILE_NOT_FOUND",
|
|
305
446
|
FILE_NOT_FOUND: "SCHEMA_FILE_NOT_FOUND",
|
|
447
|
+
ROLE_NOT_FOUND: "SCHEMA_ROLE_NOT_FOUND",
|
|
306
448
|
// Validation
|
|
307
449
|
VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED",
|
|
308
450
|
INVALID_ATTRIBUTE_NAME: "SCHEMA_INVALID_ATTRIBUTE_NAME",
|
|
@@ -310,6 +452,9 @@ var SchemaErrorCode = {
|
|
|
310
452
|
// Protected Resources
|
|
311
453
|
PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
|
|
312
454
|
PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
|
|
455
|
+
PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
|
|
456
|
+
// Permissions
|
|
457
|
+
FORBIDDEN: "SCHEMA_FORBIDDEN",
|
|
313
458
|
// Sync
|
|
314
459
|
SYNC_FAILED: "SCHEMA_SYNC_FAILED",
|
|
315
460
|
NOT_SYSTEM_OBJECT: "SCHEMA_NOT_SYSTEM_OBJECT",
|
|
@@ -454,6 +599,39 @@ function isValidationError(error) {
|
|
|
454
599
|
function isProtectedResourceError(error) {
|
|
455
600
|
return error instanceof ProtectedResourceError;
|
|
456
601
|
}
|
|
602
|
+
var ForbiddenError = class extends SchemaError {
|
|
603
|
+
constructor(objectName, action, userId) {
|
|
604
|
+
super(`No ${action} permission on object "${objectName}"`, SchemaErrorCode.FORBIDDEN, {
|
|
605
|
+
objectName,
|
|
606
|
+
action,
|
|
607
|
+
userId
|
|
608
|
+
});
|
|
609
|
+
this.name = "ForbiddenError";
|
|
610
|
+
this.objectName = objectName;
|
|
611
|
+
this.action = action;
|
|
612
|
+
this.userId = userId;
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
var ProtectedRoleError = class extends SchemaError {
|
|
616
|
+
constructor(roleName, operation) {
|
|
617
|
+
super(`Cannot ${operation} system role "${roleName}"`, SchemaErrorCode.PROTECTED_ROLE, {
|
|
618
|
+
roleName,
|
|
619
|
+
operation
|
|
620
|
+
});
|
|
621
|
+
this.name = "ProtectedRoleError";
|
|
622
|
+
this.roleName = roleName;
|
|
623
|
+
this.operation = operation;
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
var RoleNotFoundError = class extends NotFoundError {
|
|
627
|
+
constructor(roleId) {
|
|
628
|
+
super("Role", roleId, SchemaErrorCode.ROLE_NOT_FOUND);
|
|
629
|
+
this.name = "RoleNotFoundError";
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
function isForbiddenError(error) {
|
|
633
|
+
return error instanceof ForbiddenError;
|
|
634
|
+
}
|
|
457
635
|
|
|
458
636
|
// src/builders/attribute-validators.ts
|
|
459
637
|
var import_constants = require("@stndrds/constants");
|
|
@@ -1009,14 +1187,6 @@ var SingleRelationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1009
1187
|
this.attr.targets?.push(target);
|
|
1010
1188
|
return this;
|
|
1011
1189
|
}
|
|
1012
|
-
/**
|
|
1013
|
-
* Set the deletion strategy when target record is deleted
|
|
1014
|
-
* @param strategy - "restrict" (default), "cascade", or "set_null"
|
|
1015
|
-
*/
|
|
1016
|
-
onDelete(strategy) {
|
|
1017
|
-
this.attr.onDelete = strategy;
|
|
1018
|
-
return this;
|
|
1019
|
-
}
|
|
1020
1190
|
/**
|
|
1021
1191
|
* Convert to a multi-relation (cardinality: "many")
|
|
1022
1192
|
*/
|
|
@@ -1026,7 +1196,6 @@ var SingleRelationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1026
1196
|
this.attr.label ?? "",
|
|
1027
1197
|
{
|
|
1028
1198
|
targets: this.attr.targets,
|
|
1029
|
-
onDelete: this.attr.onDelete,
|
|
1030
1199
|
isRequired: this.attr.required
|
|
1031
1200
|
}
|
|
1032
1201
|
);
|
|
@@ -1047,9 +1216,6 @@ var MultiRelationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1047
1216
|
this.attr.cardinality = "many";
|
|
1048
1217
|
this.attr.targets = initOptions?.targets ?? [];
|
|
1049
1218
|
this.attr.defaultValue = [];
|
|
1050
|
-
if (initOptions?.onDelete) {
|
|
1051
|
-
this.attr.onDelete = initOptions.onDelete;
|
|
1052
|
-
}
|
|
1053
1219
|
if (initOptions?.isRequired) {
|
|
1054
1220
|
this.setRequired(true);
|
|
1055
1221
|
}
|
|
@@ -1067,14 +1233,6 @@ var MultiRelationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1067
1233
|
this.attr.targets?.push(target);
|
|
1068
1234
|
return this;
|
|
1069
1235
|
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Set the deletion strategy when target record is deleted
|
|
1072
|
-
* @param strategy - "restrict" (default), "cascade", or "set_null"
|
|
1073
|
-
*/
|
|
1074
|
-
onDelete(strategy) {
|
|
1075
|
-
this.attr.onDelete = strategy;
|
|
1076
|
-
return this;
|
|
1077
|
-
}
|
|
1078
1236
|
/**
|
|
1079
1237
|
* Set minimum number of relations required
|
|
1080
1238
|
*/
|
|
@@ -2051,7 +2209,6 @@ var userConfigSchema = baseConfigSchema.extend({
|
|
|
2051
2209
|
});
|
|
2052
2210
|
var relationConfigSchema = baseConfigSchema.extend({
|
|
2053
2211
|
targets: import_zod3.z.array(relationTargetSchema).min(1),
|
|
2054
|
-
onDelete: import_zod3.z.enum(["restrict", "cascade", "set_null"]).optional(),
|
|
2055
2212
|
cardinality: import_zod3.z.enum(["one", "many"]),
|
|
2056
2213
|
minItems: import_zod3.z.number().int().min(0).optional(),
|
|
2057
2214
|
maxItems: import_zod3.z.number().int().min(1).optional()
|
|
@@ -2701,6 +2858,12 @@ function createMockUserProfilesRepository(stores) {
|
|
|
2701
2858
|
}
|
|
2702
2859
|
return Promise.resolve(results);
|
|
2703
2860
|
},
|
|
2861
|
+
countByRole(tenantId, role) {
|
|
2862
|
+
const count = Array.from(stores.userProfiles.values()).filter(
|
|
2863
|
+
(profile) => profile.tenantId === tenantId && profile.role === role
|
|
2864
|
+
).length;
|
|
2865
|
+
return Promise.resolve(count);
|
|
2866
|
+
},
|
|
2704
2867
|
updateLastLogin(id) {
|
|
2705
2868
|
const profile = stores.userProfiles.get(id);
|
|
2706
2869
|
if (profile) {
|
|
@@ -2794,12 +2957,36 @@ function createMockFilesRepository(stores) {
|
|
|
2794
2957
|
}
|
|
2795
2958
|
function createMockObjectRecordsRepository(stores) {
|
|
2796
2959
|
return {
|
|
2960
|
+
hardDelete(id) {
|
|
2961
|
+
stores.objectRecords.delete(id);
|
|
2962
|
+
return Promise.resolve();
|
|
2963
|
+
},
|
|
2964
|
+
restore(id) {
|
|
2965
|
+
const existing = stores.objectRecords.get(id);
|
|
2966
|
+
if (!existing) {
|
|
2967
|
+
return Promise.reject(new Error(`ObjectRecord ${id} not found`));
|
|
2968
|
+
}
|
|
2969
|
+
existing.deletedAt = null;
|
|
2970
|
+
stores.objectRecords.set(id, existing);
|
|
2971
|
+
return Promise.resolve(existing);
|
|
2972
|
+
},
|
|
2797
2973
|
findById(id) {
|
|
2798
2974
|
const internal = stores.objectRecords.get(id);
|
|
2799
2975
|
if (!internal) return Promise.resolve(null);
|
|
2800
2976
|
const { tenantId: _, ...record } = internal;
|
|
2801
2977
|
return Promise.resolve(record);
|
|
2802
2978
|
},
|
|
2979
|
+
findByIds(ids) {
|
|
2980
|
+
const results = [];
|
|
2981
|
+
for (const id of ids) {
|
|
2982
|
+
const internal = stores.objectRecords.get(id);
|
|
2983
|
+
if (internal) {
|
|
2984
|
+
const { tenantId: _, ...record } = internal;
|
|
2985
|
+
results.push(record);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return Promise.resolve(results);
|
|
2989
|
+
},
|
|
2803
2990
|
create(data) {
|
|
2804
2991
|
const internal = {
|
|
2805
2992
|
id: generateId(),
|
|
@@ -2912,6 +3099,85 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
2912
3099
|
};
|
|
2913
3100
|
});
|
|
2914
3101
|
return Promise.resolve({ results, total });
|
|
3102
|
+
},
|
|
3103
|
+
// -------------------------------------------------------------------------
|
|
3104
|
+
// Schema Integrity Methods
|
|
3105
|
+
// -------------------------------------------------------------------------
|
|
3106
|
+
countRecordsReferencingId(targetId) {
|
|
3107
|
+
const relationAttrs = [];
|
|
3108
|
+
for (const attr of stores.attributes.values()) {
|
|
3109
|
+
if (attr.type === "relation") {
|
|
3110
|
+
relationAttrs.push({ objectId: attr.objectId, attrName: attr.name });
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
const countsByObjectId = /* @__PURE__ */ new Map();
|
|
3114
|
+
for (const record of stores.objectRecords.values()) {
|
|
3115
|
+
for (const { objectId, attrName } of relationAttrs) {
|
|
3116
|
+
if (record.objectId !== objectId) continue;
|
|
3117
|
+
const value = record.values[attrName];
|
|
3118
|
+
let isReferencing = false;
|
|
3119
|
+
if (typeof value === "string" && value === targetId) {
|
|
3120
|
+
isReferencing = true;
|
|
3121
|
+
} else if (Array.isArray(value) && value.includes(targetId)) {
|
|
3122
|
+
isReferencing = true;
|
|
3123
|
+
}
|
|
3124
|
+
if (isReferencing) {
|
|
3125
|
+
const current = countsByObjectId.get(objectId) ?? 0;
|
|
3126
|
+
countsByObjectId.set(objectId, current + 1);
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
const results = [];
|
|
3131
|
+
for (const [objectId, count] of countsByObjectId) {
|
|
3132
|
+
const obj = stores.objects.get(objectId);
|
|
3133
|
+
if (obj) {
|
|
3134
|
+
results.push({
|
|
3135
|
+
objectName: obj.name,
|
|
3136
|
+
objectLabel: obj.label,
|
|
3137
|
+
count
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
return Promise.resolve(results);
|
|
3142
|
+
},
|
|
3143
|
+
removeAttributeData(objectId, attributeName) {
|
|
3144
|
+
let updated = 0;
|
|
3145
|
+
for (const record of stores.objectRecords.values()) {
|
|
3146
|
+
if (record.objectId === objectId && attributeName in record.values) {
|
|
3147
|
+
delete record.values[attributeName];
|
|
3148
|
+
record.updatedAt = /* @__PURE__ */ new Date();
|
|
3149
|
+
updated++;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
return Promise.resolve(updated);
|
|
3153
|
+
},
|
|
3154
|
+
batchRefreshLabels(objectId, computeLabel) {
|
|
3155
|
+
let updated = 0;
|
|
3156
|
+
for (const record of stores.objectRecords.values()) {
|
|
3157
|
+
if (record.objectId === objectId) {
|
|
3158
|
+
const newLabel = computeLabel(record.values);
|
|
3159
|
+
if (newLabel !== record.label) {
|
|
3160
|
+
record.label = newLabel;
|
|
3161
|
+
record.updatedAt = /* @__PURE__ */ new Date();
|
|
3162
|
+
updated++;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
return Promise.resolve(updated);
|
|
3167
|
+
},
|
|
3168
|
+
batchRefreshStatus(objectId, computeStatus) {
|
|
3169
|
+
let updated = 0;
|
|
3170
|
+
for (const record of stores.objectRecords.values()) {
|
|
3171
|
+
if (record.objectId === objectId) {
|
|
3172
|
+
const newStatus = computeStatus(record.values);
|
|
3173
|
+
if (newStatus !== record.completionStatus) {
|
|
3174
|
+
record.completionStatus = newStatus;
|
|
3175
|
+
record.updatedAt = /* @__PURE__ */ new Date();
|
|
3176
|
+
updated++;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
return Promise.resolve(updated);
|
|
2915
3181
|
}
|
|
2916
3182
|
};
|
|
2917
3183
|
}
|
|
@@ -3015,6 +3281,157 @@ function createMockViewsRepository(stores) {
|
|
|
3015
3281
|
}
|
|
3016
3282
|
};
|
|
3017
3283
|
}
|
|
3284
|
+
function createMockPermissionsRepository(stores) {
|
|
3285
|
+
return {
|
|
3286
|
+
getRolesByTenant(tenantId) {
|
|
3287
|
+
const results = Array.from(stores.roles.values()).filter((r) => r.tenantId === tenantId);
|
|
3288
|
+
return Promise.resolve(results);
|
|
3289
|
+
},
|
|
3290
|
+
getRoleById(roleId) {
|
|
3291
|
+
return Promise.resolve(stores.roles.get(roleId) ?? null);
|
|
3292
|
+
},
|
|
3293
|
+
getRoleByName(tenantId, name) {
|
|
3294
|
+
for (const role of stores.roles.values()) {
|
|
3295
|
+
if (role.tenantId === tenantId && role.name === name) {
|
|
3296
|
+
return Promise.resolve(role);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
return Promise.resolve(null);
|
|
3300
|
+
},
|
|
3301
|
+
createRole(input) {
|
|
3302
|
+
const role = {
|
|
3303
|
+
id: generateId(),
|
|
3304
|
+
tenantId: input.tenantId,
|
|
3305
|
+
name: input.name,
|
|
3306
|
+
label: input.label,
|
|
3307
|
+
description: input.description,
|
|
3308
|
+
system: false,
|
|
3309
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
3310
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3311
|
+
};
|
|
3312
|
+
stores.roles.set(role.id, role);
|
|
3313
|
+
return Promise.resolve(role);
|
|
3314
|
+
},
|
|
3315
|
+
updateRole(roleId, updates) {
|
|
3316
|
+
const existing = stores.roles.get(roleId);
|
|
3317
|
+
if (!existing) {
|
|
3318
|
+
return Promise.reject(new Error(`Role ${roleId} not found`));
|
|
3319
|
+
}
|
|
3320
|
+
const updated = {
|
|
3321
|
+
...existing,
|
|
3322
|
+
...updates,
|
|
3323
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3324
|
+
};
|
|
3325
|
+
stores.roles.set(roleId, updated);
|
|
3326
|
+
return Promise.resolve(updated);
|
|
3327
|
+
},
|
|
3328
|
+
deleteRole(roleId) {
|
|
3329
|
+
const role = stores.roles.get(roleId);
|
|
3330
|
+
if (role?.system) {
|
|
3331
|
+
return Promise.reject(new Error(`Cannot delete system role ${roleId}`));
|
|
3332
|
+
}
|
|
3333
|
+
stores.roles.delete(roleId);
|
|
3334
|
+
for (const [id, perm] of stores.permissions.entries()) {
|
|
3335
|
+
if (perm.roleId === roleId) {
|
|
3336
|
+
stores.permissions.delete(id);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
for (const [id, ur] of stores.userRoles.entries()) {
|
|
3340
|
+
if (ur.roleId === roleId) {
|
|
3341
|
+
stores.userRoles.delete(id);
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
return Promise.resolve();
|
|
3345
|
+
},
|
|
3346
|
+
getPermissionsByRole(roleId) {
|
|
3347
|
+
const results = Array.from(stores.permissions.values()).filter((p) => p.roleId === roleId);
|
|
3348
|
+
return Promise.resolve(results);
|
|
3349
|
+
},
|
|
3350
|
+
setPermissions(roleId, permissions) {
|
|
3351
|
+
for (const [id, perm] of stores.permissions.entries()) {
|
|
3352
|
+
if (perm.roleId === roleId) {
|
|
3353
|
+
stores.permissions.delete(id);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
for (const input of permissions) {
|
|
3357
|
+
const perm = {
|
|
3358
|
+
id: generateId(),
|
|
3359
|
+
roleId,
|
|
3360
|
+
scope: input.scope,
|
|
3361
|
+
target: input.target,
|
|
3362
|
+
actions: input.actions,
|
|
3363
|
+
filter: input.filter,
|
|
3364
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
3365
|
+
};
|
|
3366
|
+
stores.permissions.set(perm.id, perm);
|
|
3367
|
+
}
|
|
3368
|
+
return Promise.resolve();
|
|
3369
|
+
},
|
|
3370
|
+
getUserRoles(userProfileId, tenantId) {
|
|
3371
|
+
const roleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === userProfileId && ur.tenantId === tenantId).map((ur) => ur.roleId);
|
|
3372
|
+
const roles = Array.from(stores.roles.values()).filter((r) => roleIds.includes(r.id));
|
|
3373
|
+
return Promise.resolve(roles);
|
|
3374
|
+
},
|
|
3375
|
+
assignRole(input) {
|
|
3376
|
+
for (const ur of stores.userRoles.values()) {
|
|
3377
|
+
if (ur.userProfileId === input.userProfileId && ur.roleId === input.roleId && ur.tenantId === input.tenantId) {
|
|
3378
|
+
return Promise.resolve(ur);
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
const userRole = {
|
|
3382
|
+
id: generateId(),
|
|
3383
|
+
userProfileId: input.userProfileId,
|
|
3384
|
+
roleId: input.roleId,
|
|
3385
|
+
tenantId: input.tenantId,
|
|
3386
|
+
assignedAt: /* @__PURE__ */ new Date(),
|
|
3387
|
+
assignedBy: input.assignedBy
|
|
3388
|
+
};
|
|
3389
|
+
stores.userRoles.set(userRole.id, userRole);
|
|
3390
|
+
return Promise.resolve(userRole);
|
|
3391
|
+
},
|
|
3392
|
+
revokeRole(userProfileId, roleId, tenantId) {
|
|
3393
|
+
for (const [id, ur] of stores.userRoles.entries()) {
|
|
3394
|
+
if (ur.userProfileId === userProfileId && ur.roleId === roleId && ur.tenantId === tenantId) {
|
|
3395
|
+
stores.userRoles.delete(id);
|
|
3396
|
+
break;
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
return Promise.resolve();
|
|
3400
|
+
},
|
|
3401
|
+
getEffectivePermissions(userProfileId, tenantId) {
|
|
3402
|
+
const userRoleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === userProfileId && ur.tenantId === tenantId).map((ur) => ur.roleId);
|
|
3403
|
+
const userRoles = Array.from(stores.roles.values()).filter((r) => userRoleIds.includes(r.id));
|
|
3404
|
+
const isAdmin = userRoles.some((r) => r.name === "admin");
|
|
3405
|
+
const userPermissions = Array.from(stores.permissions.values()).filter(
|
|
3406
|
+
(p) => userRoleIds.includes(p.roleId)
|
|
3407
|
+
);
|
|
3408
|
+
const objectPermissions = {};
|
|
3409
|
+
const systemPermissions = {};
|
|
3410
|
+
for (const perm of userPermissions) {
|
|
3411
|
+
if (perm.scope === "object") {
|
|
3412
|
+
if (!objectPermissions[perm.target]) {
|
|
3413
|
+
objectPermissions[perm.target] = [];
|
|
3414
|
+
}
|
|
3415
|
+
for (const action of perm.actions) {
|
|
3416
|
+
if (!objectPermissions[perm.target].includes(action)) {
|
|
3417
|
+
objectPermissions[perm.target].push(action);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
} else if (perm.scope === "system") {
|
|
3421
|
+
if (!systemPermissions[perm.target]) {
|
|
3422
|
+
systemPermissions[perm.target] = [];
|
|
3423
|
+
}
|
|
3424
|
+
for (const action of perm.actions) {
|
|
3425
|
+
if (!systemPermissions[perm.target].includes(action)) {
|
|
3426
|
+
systemPermissions[perm.target].push(action);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
return Promise.resolve({ isAdmin, objectPermissions, systemPermissions });
|
|
3432
|
+
}
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3018
3435
|
function createMockAdapter() {
|
|
3019
3436
|
const stores = {
|
|
3020
3437
|
objects: /* @__PURE__ */ new Map(),
|
|
@@ -3022,7 +3439,10 @@ function createMockAdapter() {
|
|
|
3022
3439
|
userProfiles: /* @__PURE__ */ new Map(),
|
|
3023
3440
|
files: /* @__PURE__ */ new Map(),
|
|
3024
3441
|
objectRecords: /* @__PURE__ */ new Map(),
|
|
3025
|
-
views: /* @__PURE__ */ new Map()
|
|
3442
|
+
views: /* @__PURE__ */ new Map(),
|
|
3443
|
+
roles: /* @__PURE__ */ new Map(),
|
|
3444
|
+
permissions: /* @__PURE__ */ new Map(),
|
|
3445
|
+
userRoles: /* @__PURE__ */ new Map()
|
|
3026
3446
|
};
|
|
3027
3447
|
const adapter = {
|
|
3028
3448
|
objects: createMockObjectsRepository(stores),
|
|
@@ -3031,6 +3451,7 @@ function createMockAdapter() {
|
|
|
3031
3451
|
files: createMockFilesRepository(stores),
|
|
3032
3452
|
objectRecords: createMockObjectRecordsRepository(stores),
|
|
3033
3453
|
views: createMockViewsRepository(stores),
|
|
3454
|
+
permissions: createMockPermissionsRepository(stores),
|
|
3034
3455
|
async transaction(callback) {
|
|
3035
3456
|
return await callback(adapter);
|
|
3036
3457
|
},
|
|
@@ -3044,11 +3465,266 @@ function createMockAdapter() {
|
|
|
3044
3465
|
stores.files.clear();
|
|
3045
3466
|
stores.objectRecords.clear();
|
|
3046
3467
|
stores.views.clear();
|
|
3468
|
+
stores.roles.clear();
|
|
3469
|
+
stores.permissions.clear();
|
|
3470
|
+
stores.userRoles.clear();
|
|
3047
3471
|
}
|
|
3048
3472
|
};
|
|
3049
3473
|
return adapter;
|
|
3050
3474
|
}
|
|
3051
3475
|
|
|
3476
|
+
// src/runtime/services/audit.helpers.ts
|
|
3477
|
+
function buildAuditChanges(oldValues, newValues, fieldsToCheck) {
|
|
3478
|
+
const changes = [];
|
|
3479
|
+
for (const field of fieldsToCheck) {
|
|
3480
|
+
if (newValues[field] !== void 0 && newValues[field] !== oldValues[field]) {
|
|
3481
|
+
changes.push({
|
|
3482
|
+
field: String(field),
|
|
3483
|
+
oldValue: oldValues[field],
|
|
3484
|
+
newValue: newValues[field]
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
return changes;
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
// src/runtime/services/audit.service.ts
|
|
3492
|
+
var SENSITIVE_PATTERNS = [
|
|
3493
|
+
/password/i,
|
|
3494
|
+
/token/i,
|
|
3495
|
+
/secret/i,
|
|
3496
|
+
/apiKey/i,
|
|
3497
|
+
/api_key/i,
|
|
3498
|
+
/creditCard/i,
|
|
3499
|
+
/credit_card/i,
|
|
3500
|
+
/ssn/i,
|
|
3501
|
+
/cvv/i
|
|
3502
|
+
];
|
|
3503
|
+
var AuditService = class {
|
|
3504
|
+
constructor(adapter, tenantId, options) {
|
|
3505
|
+
this.adapter = adapter;
|
|
3506
|
+
this.tenantId = tenantId;
|
|
3507
|
+
this.options = options;
|
|
3508
|
+
this.buffer = [];
|
|
3509
|
+
this.flushTimer = null;
|
|
3510
|
+
if (options?.async && options.flushIntervalMs) {
|
|
3511
|
+
this.startFlushTimer();
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
// ============================================================================
|
|
3515
|
+
// PUBLIC LOGGING METHODS
|
|
3516
|
+
// ============================================================================
|
|
3517
|
+
/**
|
|
3518
|
+
* Log a record action (create, update, delete, restore)
|
|
3519
|
+
*/
|
|
3520
|
+
async logRecordAction(params) {
|
|
3521
|
+
const entry = {
|
|
3522
|
+
tenantId: this.tenantId,
|
|
3523
|
+
actorId: params.actorId,
|
|
3524
|
+
actorEmail: params.actorEmail,
|
|
3525
|
+
actorType: "user",
|
|
3526
|
+
action: params.action,
|
|
3527
|
+
resourceType: "record",
|
|
3528
|
+
resourceId: params.recordId,
|
|
3529
|
+
resourceLabel: params.recordLabel,
|
|
3530
|
+
objectName: params.objectName,
|
|
3531
|
+
objectId: params.objectId,
|
|
3532
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
3533
|
+
metadata: params.metadata
|
|
3534
|
+
};
|
|
3535
|
+
await this.log(entry);
|
|
3536
|
+
}
|
|
3537
|
+
/**
|
|
3538
|
+
* Log a schema action (object/attribute changes)
|
|
3539
|
+
*/
|
|
3540
|
+
async logSchemaAction(params) {
|
|
3541
|
+
const entry = {
|
|
3542
|
+
tenantId: this.tenantId,
|
|
3543
|
+
actorId: params.actorId,
|
|
3544
|
+
actorEmail: params.actorEmail,
|
|
3545
|
+
actorType: "user",
|
|
3546
|
+
action: params.action,
|
|
3547
|
+
resourceType: params.resourceType,
|
|
3548
|
+
resourceId: params.resourceId,
|
|
3549
|
+
resourceLabel: params.resourceLabel,
|
|
3550
|
+
objectName: params.objectName,
|
|
3551
|
+
objectId: params.objectId,
|
|
3552
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
3553
|
+
metadata: params.metadata
|
|
3554
|
+
};
|
|
3555
|
+
await this.log(entry);
|
|
3556
|
+
}
|
|
3557
|
+
/**
|
|
3558
|
+
* Log a user action
|
|
3559
|
+
*/
|
|
3560
|
+
async logUserAction(params) {
|
|
3561
|
+
const entry = {
|
|
3562
|
+
tenantId: this.tenantId,
|
|
3563
|
+
actorId: params.actorId,
|
|
3564
|
+
actorEmail: params.actorEmail,
|
|
3565
|
+
actorType: "user",
|
|
3566
|
+
action: params.action,
|
|
3567
|
+
resourceType: "user",
|
|
3568
|
+
resourceId: params.targetUserId,
|
|
3569
|
+
resourceLabel: params.targetUserEmail,
|
|
3570
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
3571
|
+
metadata: params.metadata
|
|
3572
|
+
};
|
|
3573
|
+
await this.log(entry);
|
|
3574
|
+
}
|
|
3575
|
+
/**
|
|
3576
|
+
* Log a role action
|
|
3577
|
+
*/
|
|
3578
|
+
async logRoleAction(params) {
|
|
3579
|
+
const entry = {
|
|
3580
|
+
tenantId: this.tenantId,
|
|
3581
|
+
actorId: params.actorId,
|
|
3582
|
+
actorEmail: params.actorEmail,
|
|
3583
|
+
actorType: "user",
|
|
3584
|
+
action: params.action,
|
|
3585
|
+
resourceType: "role",
|
|
3586
|
+
resourceId: params.roleId,
|
|
3587
|
+
resourceLabel: params.roleLabel,
|
|
3588
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
3589
|
+
metadata: {
|
|
3590
|
+
...params.metadata,
|
|
3591
|
+
targetUserId: params.targetUserId
|
|
3592
|
+
}
|
|
3593
|
+
};
|
|
3594
|
+
await this.log(entry);
|
|
3595
|
+
}
|
|
3596
|
+
/**
|
|
3597
|
+
* Log a system action (no actor)
|
|
3598
|
+
*/
|
|
3599
|
+
async logSystemAction(params) {
|
|
3600
|
+
const entry = {
|
|
3601
|
+
tenantId: this.tenantId,
|
|
3602
|
+
actorType: "system",
|
|
3603
|
+
action: params.action,
|
|
3604
|
+
resourceType: params.resourceType,
|
|
3605
|
+
resourceId: params.resourceId,
|
|
3606
|
+
resourceLabel: params.resourceLabel,
|
|
3607
|
+
objectName: params.objectName,
|
|
3608
|
+
objectId: params.objectId,
|
|
3609
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
3610
|
+
metadata: params.metadata
|
|
3611
|
+
};
|
|
3612
|
+
await this.log(entry);
|
|
3613
|
+
}
|
|
3614
|
+
// ============================================================================
|
|
3615
|
+
// QUERY METHODS
|
|
3616
|
+
// ============================================================================
|
|
3617
|
+
/**
|
|
3618
|
+
* List audit logs with filtering and pagination
|
|
3619
|
+
*/
|
|
3620
|
+
async list(options) {
|
|
3621
|
+
if (!this.adapter.audit) {
|
|
3622
|
+
return { entries: [], total: 0 };
|
|
3623
|
+
}
|
|
3624
|
+
return await this.adapter.audit.list(this.tenantId, options);
|
|
3625
|
+
}
|
|
3626
|
+
/**
|
|
3627
|
+
* Get audit history for a specific record
|
|
3628
|
+
*/
|
|
3629
|
+
async getRecordHistory(recordId) {
|
|
3630
|
+
if (!this.adapter.audit) {
|
|
3631
|
+
return [];
|
|
3632
|
+
}
|
|
3633
|
+
return await this.adapter.audit.getByResource(this.tenantId, "record", recordId, {
|
|
3634
|
+
orderDirection: "desc"
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
/**
|
|
3638
|
+
* Get activity for a specific user
|
|
3639
|
+
*/
|
|
3640
|
+
async getUserActivity(userId, options) {
|
|
3641
|
+
if (!this.adapter.audit) {
|
|
3642
|
+
return [];
|
|
3643
|
+
}
|
|
3644
|
+
return await this.adapter.audit.getByActor(this.tenantId, userId, options);
|
|
3645
|
+
}
|
|
3646
|
+
/**
|
|
3647
|
+
* Get audit logs for a specific resource
|
|
3648
|
+
*/
|
|
3649
|
+
async getResourceHistory(resourceType, resourceId) {
|
|
3650
|
+
if (!this.adapter.audit) {
|
|
3651
|
+
return [];
|
|
3652
|
+
}
|
|
3653
|
+
return await this.adapter.audit.getByResource(this.tenantId, resourceType, resourceId, {
|
|
3654
|
+
orderDirection: "desc"
|
|
3655
|
+
});
|
|
3656
|
+
}
|
|
3657
|
+
// ============================================================================
|
|
3658
|
+
// BUFFER MANAGEMENT (ASYNC MODE)
|
|
3659
|
+
// ============================================================================
|
|
3660
|
+
/**
|
|
3661
|
+
* Flush buffered logs to the database
|
|
3662
|
+
*/
|
|
3663
|
+
async flush() {
|
|
3664
|
+
if (this.buffer.length === 0 || !this.adapter.audit) {
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
const entries = [...this.buffer];
|
|
3668
|
+
this.buffer = [];
|
|
3669
|
+
await this.adapter.audit.createMany(entries);
|
|
3670
|
+
}
|
|
3671
|
+
/**
|
|
3672
|
+
* Clean up resources (stop timer, flush remaining logs)
|
|
3673
|
+
*/
|
|
3674
|
+
async dispose() {
|
|
3675
|
+
if (this.flushTimer) {
|
|
3676
|
+
clearInterval(this.flushTimer);
|
|
3677
|
+
this.flushTimer = null;
|
|
3678
|
+
}
|
|
3679
|
+
await this.flush();
|
|
3680
|
+
}
|
|
3681
|
+
// ============================================================================
|
|
3682
|
+
// PRIVATE METHODS
|
|
3683
|
+
// ============================================================================
|
|
3684
|
+
/**
|
|
3685
|
+
* Log an entry (sync or async based on options)
|
|
3686
|
+
*/
|
|
3687
|
+
async log(entry) {
|
|
3688
|
+
if (!this.adapter.audit) {
|
|
3689
|
+
return;
|
|
3690
|
+
}
|
|
3691
|
+
if (this.options?.async) {
|
|
3692
|
+
this.buffer.push(entry);
|
|
3693
|
+
const batchSize = this.options.batchSize ?? 10;
|
|
3694
|
+
if (this.buffer.length >= batchSize) {
|
|
3695
|
+
await this.flush();
|
|
3696
|
+
}
|
|
3697
|
+
} else {
|
|
3698
|
+
await this.adapter.audit.create(entry);
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
/**
|
|
3702
|
+
* Start the flush timer for async mode
|
|
3703
|
+
*/
|
|
3704
|
+
startFlushTimer() {
|
|
3705
|
+
const intervalMs = this.options?.flushIntervalMs ?? 1e3;
|
|
3706
|
+
this.flushTimer = setInterval(() => {
|
|
3707
|
+
this.flush().catch(console.error);
|
|
3708
|
+
}, intervalMs);
|
|
3709
|
+
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Redact sensitive fields from changes
|
|
3712
|
+
*/
|
|
3713
|
+
redactSensitiveFields(changes) {
|
|
3714
|
+
return changes.map((change) => {
|
|
3715
|
+
const isSensitive = SENSITIVE_PATTERNS.some((pattern) => pattern.test(change.field));
|
|
3716
|
+
if (isSensitive) {
|
|
3717
|
+
return {
|
|
3718
|
+
...change,
|
|
3719
|
+
oldValue: "[REDACTED]",
|
|
3720
|
+
newValue: "[REDACTED]"
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
return change;
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
};
|
|
3727
|
+
|
|
3052
3728
|
// src/runtime/services/file.service.ts
|
|
3053
3729
|
var FileService = class {
|
|
3054
3730
|
constructor(adapter, tenantId) {
|
|
@@ -3366,9 +4042,12 @@ var GlobalSearchService = class {
|
|
|
3366
4042
|
|
|
3367
4043
|
// src/runtime/services/object-schema.service.ts
|
|
3368
4044
|
var ObjectSchemaService = class {
|
|
3369
|
-
constructor(adapter, nativeRegistry) {
|
|
4045
|
+
constructor(adapter, nativeRegistry, options) {
|
|
3370
4046
|
this.adapter = adapter;
|
|
3371
4047
|
this.nativeRegistry = nativeRegistry;
|
|
4048
|
+
this.auditService = options?.auditService;
|
|
4049
|
+
this.userId = options?.userId;
|
|
4050
|
+
this.userEmail = options?.userEmail;
|
|
3372
4051
|
}
|
|
3373
4052
|
/**
|
|
3374
4053
|
* Create a new custom object
|
|
@@ -3410,41 +4089,57 @@ var ObjectSchemaService = class {
|
|
|
3410
4089
|
...objectDef.metadata,
|
|
3411
4090
|
icon: objectDef.icon
|
|
3412
4091
|
};
|
|
3413
|
-
const dbObject = await this.adapter.
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
4092
|
+
const { dbObject, createdAttributes } = await this.adapter.transaction(async (tx) => {
|
|
4093
|
+
const dbObj = await tx.objects.create({
|
|
4094
|
+
tenantId,
|
|
4095
|
+
name: objectDef.name,
|
|
4096
|
+
label: objectDef.label,
|
|
4097
|
+
pluralLabel: objectDef.pluralLabel,
|
|
4098
|
+
description: objectDef.description,
|
|
4099
|
+
labelExpression: objectDef.labelExpression,
|
|
4100
|
+
system: false,
|
|
4101
|
+
// Always false for custom objects
|
|
4102
|
+
metadata
|
|
4103
|
+
});
|
|
4104
|
+
const attrs = [];
|
|
4105
|
+
if (objectDef.attributes.length > 0) {
|
|
4106
|
+
for (let i = 0; i < objectDef.attributes.length; i++) {
|
|
4107
|
+
const attr = objectDef.attributes[i];
|
|
4108
|
+
const config = validatedConfigs[i];
|
|
4109
|
+
const dbAttr = await tx.attributes.create({
|
|
4110
|
+
objectId: dbObj.id,
|
|
4111
|
+
name: attr.name,
|
|
4112
|
+
label: attr.label,
|
|
4113
|
+
type: attr.type,
|
|
4114
|
+
required: attr.required ?? false,
|
|
4115
|
+
unique: false,
|
|
4116
|
+
system: false,
|
|
4117
|
+
config
|
|
4118
|
+
});
|
|
4119
|
+
attrs.push(this.convertDBAttributeToAttribute(dbAttr));
|
|
4120
|
+
}
|
|
3440
4121
|
}
|
|
3441
|
-
|
|
3442
|
-
|
|
4122
|
+
return { dbObject: dbObj, createdAttributes: attrs };
|
|
4123
|
+
});
|
|
4124
|
+
const result = {
|
|
3443
4125
|
...objectDef,
|
|
3444
4126
|
id: dbObject.id,
|
|
3445
4127
|
attributes: createdAttributes,
|
|
3446
4128
|
system: false
|
|
3447
4129
|
};
|
|
4130
|
+
if (this.auditService && this.userId) {
|
|
4131
|
+
await this.auditService.logSchemaAction({
|
|
4132
|
+
action: "object.created",
|
|
4133
|
+
actorId: this.userId,
|
|
4134
|
+
actorEmail: this.userEmail,
|
|
4135
|
+
resourceType: "object",
|
|
4136
|
+
resourceId: dbObject.id,
|
|
4137
|
+
resourceLabel: dbObject.label,
|
|
4138
|
+
objectName: dbObject.name,
|
|
4139
|
+
objectId: dbObject.id
|
|
4140
|
+
});
|
|
4141
|
+
}
|
|
4142
|
+
return result;
|
|
3448
4143
|
}
|
|
3449
4144
|
/**
|
|
3450
4145
|
* Add a custom attribute to an existing object
|
|
@@ -3480,6 +4175,18 @@ var ObjectSchemaService = class {
|
|
|
3480
4175
|
// Always false for attributes added via API
|
|
3481
4176
|
config
|
|
3482
4177
|
});
|
|
4178
|
+
if (this.auditService && this.userId) {
|
|
4179
|
+
await this.auditService.logSchemaAction({
|
|
4180
|
+
action: "attribute.created",
|
|
4181
|
+
actorId: this.userId,
|
|
4182
|
+
actorEmail: this.userEmail,
|
|
4183
|
+
resourceType: "attribute",
|
|
4184
|
+
resourceId: dbAttr.id,
|
|
4185
|
+
resourceLabel: dbAttr.label,
|
|
4186
|
+
objectName: dbObject.name,
|
|
4187
|
+
objectId: dbObject.id
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
3483
4190
|
return this.convertDBAttributeToAttribute(dbAttr);
|
|
3484
4191
|
}
|
|
3485
4192
|
/**
|
|
@@ -3523,11 +4230,39 @@ var ObjectSchemaService = class {
|
|
|
3523
4230
|
if (dbAttr.type === "relation" && updates.targets) {
|
|
3524
4231
|
await this.validateRelationTargets(mergedInput, tenantId);
|
|
3525
4232
|
}
|
|
3526
|
-
const
|
|
4233
|
+
const oldValues = {
|
|
4234
|
+
label: dbAttr.label,
|
|
4235
|
+
required: dbAttr.required
|
|
4236
|
+
};
|
|
4237
|
+
const updatedDbAttr = await this.adapter.attributes.update(attributeId, {
|
|
3527
4238
|
label: updates.label,
|
|
3528
4239
|
required: updates.required,
|
|
3529
4240
|
config
|
|
3530
4241
|
});
|
|
4242
|
+
if (this.auditService && this.userId) {
|
|
4243
|
+
const changes = buildAuditChanges(oldValues, updates, ["label", "required"]);
|
|
4244
|
+
if (changes.length > 0) {
|
|
4245
|
+
const dbObject = await this.adapter.objects.findById(dbAttr.objectId);
|
|
4246
|
+
await this.auditService.logSchemaAction({
|
|
4247
|
+
action: "attribute.updated",
|
|
4248
|
+
actorId: this.userId,
|
|
4249
|
+
actorEmail: this.userEmail,
|
|
4250
|
+
resourceType: "attribute",
|
|
4251
|
+
resourceId: attributeId,
|
|
4252
|
+
resourceLabel: updatedDbAttr.label,
|
|
4253
|
+
objectName: dbObject?.name,
|
|
4254
|
+
objectId: dbAttr.objectId,
|
|
4255
|
+
changes
|
|
4256
|
+
});
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
if (updates.required !== void 0 && updates.required !== oldValues.required) {
|
|
4260
|
+
const schema = await this.getObjectSchema(dbAttr.objectId);
|
|
4261
|
+
await this.adapter.objectRecords.batchRefreshStatus(
|
|
4262
|
+
dbAttr.objectId,
|
|
4263
|
+
(values) => computeRecordStatus(schema, values)
|
|
4264
|
+
);
|
|
4265
|
+
}
|
|
3531
4266
|
return this.convertDBAttributeToAttribute(updatedDbAttr);
|
|
3532
4267
|
}
|
|
3533
4268
|
/**
|
|
@@ -3546,7 +4281,27 @@ var ObjectSchemaService = class {
|
|
|
3546
4281
|
`Cannot delete system attribute "${dbAttr.name}". System attributes are protected.`
|
|
3547
4282
|
);
|
|
3548
4283
|
}
|
|
4284
|
+
const dbObject = await this.adapter.objects.findById(dbAttr.objectId);
|
|
4285
|
+
if (dbObject?.labelExpression) {
|
|
4286
|
+
const usedAttributes = extractAttributeNames(dbObject.labelExpression);
|
|
4287
|
+
if (usedAttributes.includes(dbAttr.name)) {
|
|
4288
|
+
throw new AttributeInUseError(dbAttr.name, "labelExpression");
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
3549
4291
|
await this.adapter.attributes.delete(attributeId);
|
|
4292
|
+
await this.adapter.objectRecords.removeAttributeData(dbAttr.objectId, dbAttr.name);
|
|
4293
|
+
if (this.auditService && this.userId) {
|
|
4294
|
+
await this.auditService.logSchemaAction({
|
|
4295
|
+
action: "attribute.deleted",
|
|
4296
|
+
actorId: this.userId,
|
|
4297
|
+
actorEmail: this.userEmail,
|
|
4298
|
+
resourceType: "attribute",
|
|
4299
|
+
resourceId: attributeId,
|
|
4300
|
+
resourceLabel: dbAttr.label,
|
|
4301
|
+
objectName: dbObject?.name,
|
|
4302
|
+
objectId: dbAttr.objectId
|
|
4303
|
+
});
|
|
4304
|
+
}
|
|
3550
4305
|
}
|
|
3551
4306
|
/**
|
|
3552
4307
|
* List attributes for an object
|
|
@@ -3591,6 +4346,13 @@ var ObjectSchemaService = class {
|
|
|
3591
4346
|
`Cannot modify system object "${dbObject.name}". System objects are protected.`
|
|
3592
4347
|
);
|
|
3593
4348
|
}
|
|
4349
|
+
const oldValues = {
|
|
4350
|
+
label: dbObject.label,
|
|
4351
|
+
pluralLabel: dbObject.pluralLabel,
|
|
4352
|
+
description: dbObject.description,
|
|
4353
|
+
labelExpression: dbObject.labelExpression,
|
|
4354
|
+
icon: dbObject.metadata?.icon
|
|
4355
|
+
};
|
|
3594
4356
|
let metadata = dbObject.metadata;
|
|
3595
4357
|
if (updates.icon !== void 0 || updates.metadata !== void 0) {
|
|
3596
4358
|
metadata = {
|
|
@@ -3606,6 +4368,35 @@ var ObjectSchemaService = class {
|
|
|
3606
4368
|
labelExpression: updates.labelExpression,
|
|
3607
4369
|
metadata
|
|
3608
4370
|
});
|
|
4371
|
+
if (this.auditService && this.userId) {
|
|
4372
|
+
const changes = buildAuditChanges(oldValues, { ...updates, icon: updates.icon }, [
|
|
4373
|
+
"label",
|
|
4374
|
+
"pluralLabel",
|
|
4375
|
+
"description",
|
|
4376
|
+
"labelExpression",
|
|
4377
|
+
"icon"
|
|
4378
|
+
]);
|
|
4379
|
+
if (changes.length > 0) {
|
|
4380
|
+
await this.auditService.logSchemaAction({
|
|
4381
|
+
action: "object.updated",
|
|
4382
|
+
actorId: this.userId,
|
|
4383
|
+
actorEmail: this.userEmail,
|
|
4384
|
+
resourceType: "object",
|
|
4385
|
+
resourceId: objectId,
|
|
4386
|
+
resourceLabel: updatedDbObject.label,
|
|
4387
|
+
objectName: updatedDbObject.name,
|
|
4388
|
+
objectId,
|
|
4389
|
+
changes
|
|
4390
|
+
});
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
if (updates.labelExpression !== void 0 && updates.labelExpression !== oldValues.labelExpression) {
|
|
4394
|
+
const newExpression = updates.labelExpression;
|
|
4395
|
+
await this.adapter.objectRecords.batchRefreshLabels(
|
|
4396
|
+
objectId,
|
|
4397
|
+
(values) => renderLabelExpression(newExpression, values)
|
|
4398
|
+
);
|
|
4399
|
+
}
|
|
3609
4400
|
const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
3610
4401
|
return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
|
|
3611
4402
|
}
|
|
@@ -3621,7 +4412,7 @@ var ObjectSchemaService = class {
|
|
|
3621
4412
|
* await service.deleteObject("obj-123");
|
|
3622
4413
|
* ```
|
|
3623
4414
|
*/
|
|
3624
|
-
async deleteObject(objectId) {
|
|
4415
|
+
async deleteObject(objectId, tenantId) {
|
|
3625
4416
|
const dbObject = await this.adapter.objects.findById(objectId);
|
|
3626
4417
|
if (!dbObject) {
|
|
3627
4418
|
throw new Error(`Object with id "${objectId}" not found`);
|
|
@@ -3631,7 +4422,29 @@ var ObjectSchemaService = class {
|
|
|
3631
4422
|
`Cannot delete system object "${dbObject.name}". System objects are protected.`
|
|
3632
4423
|
);
|
|
3633
4424
|
}
|
|
4425
|
+
const effectiveTenantId = tenantId ?? dbObject.tenantId;
|
|
4426
|
+
if (effectiveTenantId) {
|
|
4427
|
+
const referencingObjects = await this.findObjectsWithRelationTo(
|
|
4428
|
+
dbObject.name,
|
|
4429
|
+
effectiveTenantId
|
|
4430
|
+
);
|
|
4431
|
+
if (referencingObjects.length > 0) {
|
|
4432
|
+
throw new ObjectReferencedError(dbObject.name, referencingObjects);
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
3634
4435
|
await this.adapter.objects.delete(objectId);
|
|
4436
|
+
if (this.auditService && this.userId) {
|
|
4437
|
+
await this.auditService.logSchemaAction({
|
|
4438
|
+
action: "object.deleted",
|
|
4439
|
+
actorId: this.userId,
|
|
4440
|
+
actorEmail: this.userEmail,
|
|
4441
|
+
resourceType: "object",
|
|
4442
|
+
resourceId: objectId,
|
|
4443
|
+
resourceLabel: dbObject.label,
|
|
4444
|
+
objectName: dbObject.name,
|
|
4445
|
+
objectId
|
|
4446
|
+
});
|
|
4447
|
+
}
|
|
3635
4448
|
}
|
|
3636
4449
|
/**
|
|
3637
4450
|
* Validate attribute name format (variable identifier)
|
|
@@ -3801,14 +4614,13 @@ var ObjectSchemaService = class {
|
|
|
3801
4614
|
* @internal
|
|
3802
4615
|
*/
|
|
3803
4616
|
buildAttributeConfig(attribute) {
|
|
3804
|
-
const
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
} = attribute;
|
|
4617
|
+
const DB_COLUMN_FIELDS = ["name", "label", "type", "required", "unique"];
|
|
4618
|
+
const configInput = {};
|
|
4619
|
+
for (const [key, value] of Object.entries(attribute)) {
|
|
4620
|
+
if (!DB_COLUMN_FIELDS.includes(key)) {
|
|
4621
|
+
configInput[key] = value;
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
3812
4624
|
try {
|
|
3813
4625
|
return parseAttributeConfig(attribute.type, configInput);
|
|
3814
4626
|
} catch (error) {
|
|
@@ -3818,6 +4630,19 @@ var ObjectSchemaService = class {
|
|
|
3818
4630
|
throw error;
|
|
3819
4631
|
}
|
|
3820
4632
|
}
|
|
4633
|
+
/**
|
|
4634
|
+
* Check if an object exists (in tenant, system, or native registry)
|
|
4635
|
+
* @internal
|
|
4636
|
+
*/
|
|
4637
|
+
async objectExists(name, tenantId) {
|
|
4638
|
+
if (tenantId) {
|
|
4639
|
+
const obj = await this.adapter.objects.findByName(tenantId, name);
|
|
4640
|
+
if (obj) return true;
|
|
4641
|
+
}
|
|
4642
|
+
const systemObj = await this.adapter.objects.findSystemByName(name);
|
|
4643
|
+
if (systemObj) return true;
|
|
4644
|
+
return this.nativeRegistry.getByName(name) !== void 0;
|
|
4645
|
+
}
|
|
3821
4646
|
/**
|
|
3822
4647
|
* Validate that relation target objects exist
|
|
3823
4648
|
* @internal
|
|
@@ -3828,26 +4653,43 @@ var ObjectSchemaService = class {
|
|
|
3828
4653
|
throw new Error("Relation attribute must have at least one target object");
|
|
3829
4654
|
}
|
|
3830
4655
|
const cardinality = attribute.cardinality;
|
|
3831
|
-
|
|
4656
|
+
const isValidCardinality = cardinality === "one" || cardinality === "many";
|
|
4657
|
+
if (!isValidCardinality) {
|
|
3832
4658
|
throw new Error("Relation attribute must specify cardinality: 'one' or 'many'");
|
|
3833
4659
|
}
|
|
3834
4660
|
for (const target of targets) {
|
|
3835
4661
|
if (!target.object) {
|
|
3836
4662
|
throw new Error("Each relation target must specify an object name");
|
|
3837
4663
|
}
|
|
3838
|
-
|
|
3839
|
-
if (!
|
|
3840
|
-
|
|
4664
|
+
const exists = await this.objectExists(target.object, tenantId);
|
|
4665
|
+
if (!exists) {
|
|
4666
|
+
throw new Error(
|
|
4667
|
+
`Relation target object "${target.object}" does not exist. Make sure the object is created before adding a relation to it.`
|
|
4668
|
+
);
|
|
3841
4669
|
}
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
/**
|
|
4673
|
+
* Find objects that have a relation attribute targeting the given object.
|
|
4674
|
+
* Used to prevent deletion of objects that are relation targets.
|
|
4675
|
+
* @internal
|
|
4676
|
+
*/
|
|
4677
|
+
async findObjectsWithRelationTo(targetObjectName, tenantId) {
|
|
4678
|
+
const allObjects = await this.adapter.objects.list(tenantId);
|
|
4679
|
+
const referencing = [];
|
|
4680
|
+
for (const obj of allObjects) {
|
|
4681
|
+
if (obj.name === targetObjectName) continue;
|
|
4682
|
+
const attrs = await this.adapter.attributes.findByObjectId(obj.id);
|
|
4683
|
+
const hasRelationToTarget = attrs.some((attr) => {
|
|
4684
|
+
if (attr.type !== "relation") return false;
|
|
4685
|
+
const config = attr.config;
|
|
4686
|
+
return config?.targets?.some((t) => t.object === targetObjectName) ?? false;
|
|
4687
|
+
});
|
|
4688
|
+
if (hasRelationToTarget) {
|
|
4689
|
+
referencing.push(obj.name);
|
|
3849
4690
|
}
|
|
3850
4691
|
}
|
|
4692
|
+
return referencing;
|
|
3851
4693
|
}
|
|
3852
4694
|
/**
|
|
3853
4695
|
* Convert DB attribute to Attribute type
|
|
@@ -3868,6 +4710,424 @@ var ObjectSchemaService = class {
|
|
|
3868
4710
|
}
|
|
3869
4711
|
};
|
|
3870
4712
|
|
|
4713
|
+
// src/runtime/services/permission.service.ts
|
|
4714
|
+
var PermissionService = class {
|
|
4715
|
+
constructor(adapter, tenantId, options) {
|
|
4716
|
+
this.adapter = adapter;
|
|
4717
|
+
this.tenantId = tenantId;
|
|
4718
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
4719
|
+
if (!adapter.permissions) {
|
|
4720
|
+
throw new Error(
|
|
4721
|
+
"PermissionService requires a DatabaseAdapter with permissions repository. Make sure your adapter implements the permissions property."
|
|
4722
|
+
);
|
|
4723
|
+
}
|
|
4724
|
+
this.permissionsRepo = adapter.permissions;
|
|
4725
|
+
this.cacheTtlMs = options?.cacheTtlMs ?? 6e4;
|
|
4726
|
+
this.maxCacheSize = options?.maxCacheSize ?? 1e4;
|
|
4727
|
+
this.auditService = options?.auditService;
|
|
4728
|
+
this.userId = options?.userId;
|
|
4729
|
+
this.userEmail = options?.userEmail;
|
|
4730
|
+
}
|
|
4731
|
+
// ============================================================================
|
|
4732
|
+
// PERMISSION CHECKS
|
|
4733
|
+
// ============================================================================
|
|
4734
|
+
/**
|
|
4735
|
+
* Check if user can perform an action on an object.
|
|
4736
|
+
*
|
|
4737
|
+
* @param userProfileId - User profile ID to check
|
|
4738
|
+
* @param objectName - Object name to check access for
|
|
4739
|
+
* @param action - Action to check (read, create, update, delete)
|
|
4740
|
+
* @returns true if user has permission, false otherwise
|
|
4741
|
+
*/
|
|
4742
|
+
async canAccessObject(userProfileId, objectName, action) {
|
|
4743
|
+
const permissions = await this.getEffectivePermissions(userProfileId);
|
|
4744
|
+
if (permissions.isAdmin) {
|
|
4745
|
+
return true;
|
|
4746
|
+
}
|
|
4747
|
+
const wildcardPerms = permissions.objectPermissions["*"];
|
|
4748
|
+
if (wildcardPerms?.includes(action)) {
|
|
4749
|
+
return true;
|
|
4750
|
+
}
|
|
4751
|
+
const objectPerms = permissions.objectPermissions[objectName];
|
|
4752
|
+
return objectPerms?.includes(action) ?? false;
|
|
4753
|
+
}
|
|
4754
|
+
/**
|
|
4755
|
+
* Check if user can access an object, throw ForbiddenError if not.
|
|
4756
|
+
*
|
|
4757
|
+
* @param userProfileId - User profile ID to check
|
|
4758
|
+
* @param objectName - Object name to check access for
|
|
4759
|
+
* @param action - Action to check
|
|
4760
|
+
* @throws ForbiddenError if user lacks permission
|
|
4761
|
+
*/
|
|
4762
|
+
async checkObjectAccess(userProfileId, objectName, action) {
|
|
4763
|
+
const canAccess = await this.canAccessObject(userProfileId, objectName, action);
|
|
4764
|
+
if (!canAccess) {
|
|
4765
|
+
throw new ForbiddenError(objectName, action, userProfileId);
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
/**
|
|
4769
|
+
* Get permissions for a specific object as boolean flags.
|
|
4770
|
+
* Optimized to fetch permissions once and compute all flags.
|
|
4771
|
+
*
|
|
4772
|
+
* @param userProfileId - User profile ID to check
|
|
4773
|
+
* @param objectName - Object name to check
|
|
4774
|
+
* @returns Object with canRead, canCreate, canUpdate, canDelete flags
|
|
4775
|
+
*/
|
|
4776
|
+
async getObjectPermissions(userProfileId, objectName) {
|
|
4777
|
+
const permissions = await this.getEffectivePermissions(userProfileId);
|
|
4778
|
+
if (permissions.isAdmin) {
|
|
4779
|
+
return { canRead: true, canCreate: true, canUpdate: true, canDelete: true };
|
|
4780
|
+
}
|
|
4781
|
+
const wildcardPerms = permissions.objectPermissions["*"] ?? [];
|
|
4782
|
+
const objectPerms = permissions.objectPermissions[objectName] ?? [];
|
|
4783
|
+
const allPerms = /* @__PURE__ */ new Set([...wildcardPerms, ...objectPerms]);
|
|
4784
|
+
return {
|
|
4785
|
+
canRead: allPerms.has("read"),
|
|
4786
|
+
canCreate: allPerms.has("create"),
|
|
4787
|
+
canUpdate: allPerms.has("update"),
|
|
4788
|
+
canDelete: allPerms.has("delete")
|
|
4789
|
+
};
|
|
4790
|
+
}
|
|
4791
|
+
// ============================================================================
|
|
4792
|
+
// SYSTEM PERMISSION CHECKS
|
|
4793
|
+
// ============================================================================
|
|
4794
|
+
/**
|
|
4795
|
+
* Check if user can perform an action on a system resource.
|
|
4796
|
+
*
|
|
4797
|
+
* @param userProfileId - User profile ID to check
|
|
4798
|
+
* @param resource - System resource (users, schema, roles, settings)
|
|
4799
|
+
* @param action - Action to check (read, create, update, delete)
|
|
4800
|
+
* @returns true if user has permission, false otherwise
|
|
4801
|
+
*/
|
|
4802
|
+
async canAccessSystem(userProfileId, resource, action) {
|
|
4803
|
+
const permissions = await this.getEffectivePermissions(userProfileId);
|
|
4804
|
+
if (permissions.isAdmin) {
|
|
4805
|
+
return true;
|
|
4806
|
+
}
|
|
4807
|
+
const wildcardPerms = permissions.systemPermissions?.["*"];
|
|
4808
|
+
if (wildcardPerms?.includes(action)) {
|
|
4809
|
+
return true;
|
|
4810
|
+
}
|
|
4811
|
+
const resourcePerms = permissions.systemPermissions?.[resource];
|
|
4812
|
+
return resourcePerms?.includes(action) ?? false;
|
|
4813
|
+
}
|
|
4814
|
+
/**
|
|
4815
|
+
* Check if user can access a system resource, throw ForbiddenError if not.
|
|
4816
|
+
*
|
|
4817
|
+
* @param userProfileId - User profile ID to check
|
|
4818
|
+
* @param resource - System resource to check access for
|
|
4819
|
+
* @param action - Action to check
|
|
4820
|
+
* @throws ForbiddenError if user lacks permission
|
|
4821
|
+
*/
|
|
4822
|
+
async checkSystemAccess(userProfileId, resource, action) {
|
|
4823
|
+
const canAccess = await this.canAccessSystem(userProfileId, resource, action);
|
|
4824
|
+
if (!canAccess) {
|
|
4825
|
+
throw new ForbiddenError(`_${resource}`, action, userProfileId);
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
/**
|
|
4829
|
+
* Get permissions for a specific system resource as boolean flags.
|
|
4830
|
+
* Optimized to fetch permissions once and compute all flags.
|
|
4831
|
+
*
|
|
4832
|
+
* @param userProfileId - User profile ID to check
|
|
4833
|
+
* @param resource - System resource to check
|
|
4834
|
+
* @returns Object with canRead, canCreate, canUpdate, canDelete flags
|
|
4835
|
+
*/
|
|
4836
|
+
async getSystemPermissions(userProfileId, resource) {
|
|
4837
|
+
const permissions = await this.getEffectivePermissions(userProfileId);
|
|
4838
|
+
if (permissions.isAdmin) {
|
|
4839
|
+
return { canRead: true, canCreate: true, canUpdate: true, canDelete: true };
|
|
4840
|
+
}
|
|
4841
|
+
const wildcardPerms = permissions.systemPermissions?.["*"] ?? [];
|
|
4842
|
+
const resourcePerms = permissions.systemPermissions?.[resource] ?? [];
|
|
4843
|
+
const allPerms = /* @__PURE__ */ new Set([...wildcardPerms, ...resourcePerms]);
|
|
4844
|
+
return {
|
|
4845
|
+
canRead: allPerms.has("read"),
|
|
4846
|
+
canCreate: allPerms.has("create"),
|
|
4847
|
+
canUpdate: allPerms.has("update"),
|
|
4848
|
+
canDelete: allPerms.has("delete")
|
|
4849
|
+
};
|
|
4850
|
+
}
|
|
4851
|
+
// ============================================================================
|
|
4852
|
+
// EFFECTIVE PERMISSIONS
|
|
4853
|
+
// ============================================================================
|
|
4854
|
+
/**
|
|
4855
|
+
* Get effective permissions for a user.
|
|
4856
|
+
* Results are cached with TTL and size limit.
|
|
4857
|
+
*
|
|
4858
|
+
* @param userProfileId - User profile ID
|
|
4859
|
+
* @returns Merged permissions from all user's roles
|
|
4860
|
+
*/
|
|
4861
|
+
async getEffectivePermissions(userProfileId) {
|
|
4862
|
+
const cacheKey = this.getCacheKey(userProfileId);
|
|
4863
|
+
const cached = this.cache.get(cacheKey);
|
|
4864
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
4865
|
+
return cached.permissions;
|
|
4866
|
+
}
|
|
4867
|
+
const permissions = await this.permissionsRepo.getEffectivePermissions(
|
|
4868
|
+
userProfileId,
|
|
4869
|
+
this.tenantId
|
|
4870
|
+
);
|
|
4871
|
+
if (this.cache.size >= this.maxCacheSize) {
|
|
4872
|
+
const firstKey = this.cache.keys().next().value;
|
|
4873
|
+
if (firstKey) {
|
|
4874
|
+
this.cache.delete(firstKey);
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
this.cache.set(cacheKey, {
|
|
4878
|
+
permissions,
|
|
4879
|
+
expiresAt: Date.now() + this.cacheTtlMs
|
|
4880
|
+
});
|
|
4881
|
+
return permissions;
|
|
4882
|
+
}
|
|
4883
|
+
// ============================================================================
|
|
4884
|
+
// CACHE MANAGEMENT
|
|
4885
|
+
// ============================================================================
|
|
4886
|
+
/**
|
|
4887
|
+
* Invalidate cached permissions for a specific user.
|
|
4888
|
+
* Call this after role/permission changes.
|
|
4889
|
+
*/
|
|
4890
|
+
invalidateCache(userProfileId) {
|
|
4891
|
+
this.cache.delete(this.getCacheKey(userProfileId));
|
|
4892
|
+
}
|
|
4893
|
+
/**
|
|
4894
|
+
* Invalidate all cached permissions.
|
|
4895
|
+
* Call this after bulk role/permission changes.
|
|
4896
|
+
*/
|
|
4897
|
+
invalidateAllCache() {
|
|
4898
|
+
this.cache.clear();
|
|
4899
|
+
}
|
|
4900
|
+
/**
|
|
4901
|
+
* Get current cache size (for monitoring).
|
|
4902
|
+
*/
|
|
4903
|
+
getCacheSize() {
|
|
4904
|
+
return this.cache.size;
|
|
4905
|
+
}
|
|
4906
|
+
getCacheKey(userProfileId) {
|
|
4907
|
+
return `${this.tenantId}:${userProfileId}`;
|
|
4908
|
+
}
|
|
4909
|
+
// ============================================================================
|
|
4910
|
+
// ROLE MANAGEMENT
|
|
4911
|
+
// ============================================================================
|
|
4912
|
+
/**
|
|
4913
|
+
* Get all roles for the current tenant.
|
|
4914
|
+
*/
|
|
4915
|
+
async getRoles() {
|
|
4916
|
+
return await this.permissionsRepo.getRolesByTenant(this.tenantId);
|
|
4917
|
+
}
|
|
4918
|
+
/**
|
|
4919
|
+
* Get a role by ID.
|
|
4920
|
+
*/
|
|
4921
|
+
async getRoleById(roleId) {
|
|
4922
|
+
return await this.permissionsRepo.getRoleById(roleId);
|
|
4923
|
+
}
|
|
4924
|
+
/**
|
|
4925
|
+
* Create a new role.
|
|
4926
|
+
*/
|
|
4927
|
+
async createRole(input) {
|
|
4928
|
+
const role = await this.permissionsRepo.createRole({
|
|
4929
|
+
...input,
|
|
4930
|
+
tenantId: this.tenantId
|
|
4931
|
+
});
|
|
4932
|
+
if (this.auditService && this.userId) {
|
|
4933
|
+
await this.auditService.logRoleAction({
|
|
4934
|
+
action: "role.created",
|
|
4935
|
+
actorId: this.userId,
|
|
4936
|
+
actorEmail: this.userEmail,
|
|
4937
|
+
roleId: role.id,
|
|
4938
|
+
roleLabel: role.label
|
|
4939
|
+
});
|
|
4940
|
+
}
|
|
4941
|
+
return role;
|
|
4942
|
+
}
|
|
4943
|
+
/**
|
|
4944
|
+
* Update a role.
|
|
4945
|
+
*/
|
|
4946
|
+
async updateRole(roleId, updates) {
|
|
4947
|
+
const oldRole = await this.permissionsRepo.getRoleById(roleId);
|
|
4948
|
+
const role = await this.permissionsRepo.updateRole(roleId, updates);
|
|
4949
|
+
if (this.auditService && this.userId && oldRole) {
|
|
4950
|
+
const changes = [];
|
|
4951
|
+
if (updates.label !== void 0 && updates.label !== oldRole.label) {
|
|
4952
|
+
changes.push({ field: "label", oldValue: oldRole.label, newValue: updates.label });
|
|
4953
|
+
}
|
|
4954
|
+
if (updates.description !== void 0 && updates.description !== oldRole.description) {
|
|
4955
|
+
changes.push({
|
|
4956
|
+
field: "description",
|
|
4957
|
+
oldValue: oldRole.description,
|
|
4958
|
+
newValue: updates.description
|
|
4959
|
+
});
|
|
4960
|
+
}
|
|
4961
|
+
if (changes.length > 0) {
|
|
4962
|
+
await this.auditService.logRoleAction({
|
|
4963
|
+
action: "role.updated",
|
|
4964
|
+
actorId: this.userId,
|
|
4965
|
+
actorEmail: this.userEmail,
|
|
4966
|
+
roleId: role.id,
|
|
4967
|
+
roleLabel: role.label,
|
|
4968
|
+
changes
|
|
4969
|
+
});
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
return role;
|
|
4973
|
+
}
|
|
4974
|
+
/**
|
|
4975
|
+
* Delete a role.
|
|
4976
|
+
*/
|
|
4977
|
+
async deleteRole(roleId) {
|
|
4978
|
+
const role = await this.permissionsRepo.getRoleById(roleId);
|
|
4979
|
+
await this.permissionsRepo.deleteRole(roleId);
|
|
4980
|
+
this.invalidateAllCache();
|
|
4981
|
+
if (this.auditService && this.userId && role) {
|
|
4982
|
+
await this.auditService.logRoleAction({
|
|
4983
|
+
action: "role.deleted",
|
|
4984
|
+
actorId: this.userId,
|
|
4985
|
+
actorEmail: this.userEmail,
|
|
4986
|
+
roleId,
|
|
4987
|
+
roleLabel: role.label
|
|
4988
|
+
});
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
// ============================================================================
|
|
4992
|
+
// PERMISSION MANAGEMENT
|
|
4993
|
+
// ============================================================================
|
|
4994
|
+
/**
|
|
4995
|
+
* Get all permissions for a role.
|
|
4996
|
+
*/
|
|
4997
|
+
async getPermissionsByRole(roleId) {
|
|
4998
|
+
return await this.permissionsRepo.getPermissionsByRole(roleId);
|
|
4999
|
+
}
|
|
5000
|
+
/**
|
|
5001
|
+
* Set permissions for a role (replaces existing).
|
|
5002
|
+
*/
|
|
5003
|
+
async setPermissions(roleId, permissions) {
|
|
5004
|
+
await this.permissionsRepo.setPermissions(roleId, permissions);
|
|
5005
|
+
this.invalidateAllCache();
|
|
5006
|
+
if (this.auditService && this.userId) {
|
|
5007
|
+
const role = await this.permissionsRepo.getRoleById(roleId);
|
|
5008
|
+
await this.auditService.logRoleAction({
|
|
5009
|
+
action: "role.updated",
|
|
5010
|
+
actorId: this.userId,
|
|
5011
|
+
actorEmail: this.userEmail,
|
|
5012
|
+
roleId,
|
|
5013
|
+
roleLabel: role?.label ?? roleId,
|
|
5014
|
+
metadata: { permissionsUpdated: true, permissionCount: permissions.length }
|
|
5015
|
+
});
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
5018
|
+
// ============================================================================
|
|
5019
|
+
// USER ROLE MANAGEMENT
|
|
5020
|
+
// ============================================================================
|
|
5021
|
+
/**
|
|
5022
|
+
* Get all roles assigned to a user.
|
|
5023
|
+
*/
|
|
5024
|
+
async getUserRoles(userProfileId) {
|
|
5025
|
+
return await this.permissionsRepo.getUserRoles(userProfileId, this.tenantId);
|
|
5026
|
+
}
|
|
5027
|
+
/**
|
|
5028
|
+
* Assign a role to a user.
|
|
5029
|
+
*/
|
|
5030
|
+
async assignRole(userProfileId, roleId, assignedBy) {
|
|
5031
|
+
const result = await this.permissionsRepo.assignRole({
|
|
5032
|
+
userProfileId,
|
|
5033
|
+
roleId,
|
|
5034
|
+
tenantId: this.tenantId,
|
|
5035
|
+
assignedBy
|
|
5036
|
+
});
|
|
5037
|
+
this.invalidateCache(userProfileId);
|
|
5038
|
+
if (this.auditService && this.userId) {
|
|
5039
|
+
const role = await this.permissionsRepo.getRoleById(roleId);
|
|
5040
|
+
await this.auditService.logRoleAction({
|
|
5041
|
+
action: "role.assigned",
|
|
5042
|
+
actorId: this.userId,
|
|
5043
|
+
actorEmail: this.userEmail,
|
|
5044
|
+
roleId,
|
|
5045
|
+
roleLabel: role?.label ?? roleId,
|
|
5046
|
+
targetUserId: userProfileId
|
|
5047
|
+
});
|
|
5048
|
+
}
|
|
5049
|
+
return result;
|
|
5050
|
+
}
|
|
5051
|
+
/**
|
|
5052
|
+
* Revoke a role from a user.
|
|
5053
|
+
*/
|
|
5054
|
+
async revokeRole(userProfileId, roleId) {
|
|
5055
|
+
const role = await this.permissionsRepo.getRoleById(roleId);
|
|
5056
|
+
await this.permissionsRepo.revokeRole(userProfileId, roleId, this.tenantId);
|
|
5057
|
+
this.invalidateCache(userProfileId);
|
|
5058
|
+
if (this.auditService && this.userId) {
|
|
5059
|
+
await this.auditService.logRoleAction({
|
|
5060
|
+
action: "role.revoked",
|
|
5061
|
+
actorId: this.userId,
|
|
5062
|
+
actorEmail: this.userEmail,
|
|
5063
|
+
roleId,
|
|
5064
|
+
roleLabel: role?.label ?? roleId,
|
|
5065
|
+
targetUserId: userProfileId
|
|
5066
|
+
});
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
// ============================================================================
|
|
5070
|
+
// INITIALIZATION
|
|
5071
|
+
// ============================================================================
|
|
5072
|
+
/**
|
|
5073
|
+
* Initialize default roles for the tenant if they don't exist.
|
|
5074
|
+
*
|
|
5075
|
+
* Creates the following roles with their default permissions:
|
|
5076
|
+
* - **admin**: Full access to all system resources and objects
|
|
5077
|
+
* - **member**: No system access, can read/create/update objects (no delete)
|
|
5078
|
+
* - **guest**: No system access, read-only access to objects
|
|
5079
|
+
*
|
|
5080
|
+
* This method is idempotent - it only creates roles that don't already exist.
|
|
5081
|
+
* Should be called during tenant bootstrap or on first admin login.
|
|
5082
|
+
*
|
|
5083
|
+
* @example
|
|
5084
|
+
* ```typescript
|
|
5085
|
+
* const service = new PermissionService(adapter, tenantId);
|
|
5086
|
+
* await service.initializeDefaultRoles();
|
|
5087
|
+
* ```
|
|
5088
|
+
*/
|
|
5089
|
+
async initializeDefaultRoles() {
|
|
5090
|
+
const {
|
|
5091
|
+
DEFAULT_ROLES: DEFAULT_ROLES2,
|
|
5092
|
+
DEFAULT_ROLE_LABELS: DEFAULT_ROLE_LABELS2,
|
|
5093
|
+
DEFAULT_ROLE_DESCRIPTIONS: DEFAULT_ROLE_DESCRIPTIONS2,
|
|
5094
|
+
DEFAULT_ROLE_PERMISSIONS: DEFAULT_ROLE_PERMISSIONS2
|
|
5095
|
+
} = await Promise.resolve().then(() => (init_default_roles(), default_roles_exports));
|
|
5096
|
+
const existingRoles = await this.getRoles();
|
|
5097
|
+
const existingRoleNames = new Set(existingRoles.map((r) => r.name));
|
|
5098
|
+
for (const roleName of Object.values(DEFAULT_ROLES2)) {
|
|
5099
|
+
if (existingRoleNames.has(roleName)) {
|
|
5100
|
+
continue;
|
|
5101
|
+
}
|
|
5102
|
+
const role = await this.permissionsRepo.createRole({
|
|
5103
|
+
tenantId: this.tenantId,
|
|
5104
|
+
name: roleName,
|
|
5105
|
+
label: DEFAULT_ROLE_LABELS2[roleName],
|
|
5106
|
+
description: DEFAULT_ROLE_DESCRIPTIONS2[roleName]
|
|
5107
|
+
});
|
|
5108
|
+
const permConfig = DEFAULT_ROLE_PERMISSIONS2[roleName];
|
|
5109
|
+
const permissionInputs = [];
|
|
5110
|
+
for (const perm of permConfig.system) {
|
|
5111
|
+
permissionInputs.push({
|
|
5112
|
+
scope: "system",
|
|
5113
|
+
target: perm.target,
|
|
5114
|
+
actions: perm.actions
|
|
5115
|
+
});
|
|
5116
|
+
}
|
|
5117
|
+
for (const perm of permConfig.object) {
|
|
5118
|
+
permissionInputs.push({
|
|
5119
|
+
scope: "object",
|
|
5120
|
+
target: perm.target,
|
|
5121
|
+
actions: perm.actions
|
|
5122
|
+
});
|
|
5123
|
+
}
|
|
5124
|
+
if (permissionInputs.length > 0) {
|
|
5125
|
+
await this.setPermissions(role.id, permissionInputs);
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
};
|
|
5130
|
+
|
|
3871
5131
|
// src/runtime/services/relation.service.ts
|
|
3872
5132
|
var RelationService = class {
|
|
3873
5133
|
constructor(adapter, nativeRegistry) {
|
|
@@ -4042,8 +5302,7 @@ var RelationService = class {
|
|
|
4042
5302
|
objectName: objectSchema.name,
|
|
4043
5303
|
objectLabel: objectSchema.label,
|
|
4044
5304
|
objectIcon: objectSchema.icon,
|
|
4045
|
-
label
|
|
4046
|
-
data: record.values
|
|
5305
|
+
label
|
|
4047
5306
|
});
|
|
4048
5307
|
}
|
|
4049
5308
|
}
|
|
@@ -4058,40 +5317,58 @@ var RelationService = class {
|
|
|
4058
5317
|
* Resolve record IDs to their display labels
|
|
4059
5318
|
* Useful for displaying current values in the UI
|
|
4060
5319
|
*
|
|
5320
|
+
* Uses batch fetching for performance - fetches all records in one query,
|
|
5321
|
+
* then groups by objectId to minimize schema lookups.
|
|
5322
|
+
*
|
|
4061
5323
|
* @param ids - Record IDs to resolve
|
|
4062
|
-
* @param
|
|
5324
|
+
* @param _tenantId - Tenant ID for multi-tenant isolation (unused but kept for API compatibility)
|
|
5325
|
+
* @param attributeId - Relation attribute ID to use its displayTemplate for label rendering
|
|
4063
5326
|
*
|
|
4064
5327
|
* @example
|
|
4065
5328
|
* ```typescript
|
|
4066
|
-
* const resolved = await relationService.resolveIds(["rec-1", "rec-2"], "tenant-1");
|
|
5329
|
+
* const resolved = await relationService.resolveIds(["rec-1", "rec-2"], "tenant-1", "attr-123");
|
|
4067
5330
|
* // [{ id: "rec-1", label: "Nike Air Max", objectName: "products", ... }]
|
|
4068
5331
|
* ```
|
|
4069
5332
|
*/
|
|
4070
|
-
async resolveIds(ids,
|
|
5333
|
+
async resolveIds(ids, _tenantId, attributeId) {
|
|
4071
5334
|
if (!ids || ids.length === 0) {
|
|
4072
5335
|
return [];
|
|
4073
5336
|
}
|
|
4074
|
-
const
|
|
5337
|
+
const attribute = await this.findAttributeById(attributeId);
|
|
5338
|
+
const records = await this.adapter.objectRecords.findByIds(ids);
|
|
5339
|
+
if (records.length === 0) {
|
|
5340
|
+
return [];
|
|
5341
|
+
}
|
|
5342
|
+
const recordsByObjectId = /* @__PURE__ */ new Map();
|
|
5343
|
+
for (const record of records) {
|
|
5344
|
+
const existing = recordsByObjectId.get(record.objectId) ?? [];
|
|
5345
|
+
existing.push(record);
|
|
5346
|
+
recordsByObjectId.set(record.objectId, existing);
|
|
5347
|
+
}
|
|
4075
5348
|
const resolved = [];
|
|
4076
|
-
for (const
|
|
4077
|
-
const
|
|
4078
|
-
if (!record) {
|
|
4079
|
-
continue;
|
|
4080
|
-
}
|
|
4081
|
-
const objectSchema = await this.schemaService.getObjectSchema(record.objectId);
|
|
5349
|
+
for (const [objectId, objectRecords] of recordsByObjectId) {
|
|
5350
|
+
const objectSchema = await this.schemaService.getObjectSchema(objectId);
|
|
4082
5351
|
if (!objectSchema) {
|
|
4083
5352
|
continue;
|
|
4084
5353
|
}
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
5354
|
+
let template = objectSchema.labelExpression;
|
|
5355
|
+
if (attribute?.targets) {
|
|
5356
|
+
const targetConfig = attribute.targets.find((t) => t.object === objectSchema.name);
|
|
5357
|
+
if (targetConfig?.displayTemplate) {
|
|
5358
|
+
template = targetConfig.displayTemplate;
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5361
|
+
for (const record of objectRecords) {
|
|
5362
|
+
const label = renderLabelExpression(template, record.values);
|
|
5363
|
+
resolved.push({
|
|
5364
|
+
id: record.id,
|
|
5365
|
+
objectId: record.objectId,
|
|
5366
|
+
objectName: objectSchema.name,
|
|
5367
|
+
objectLabel: objectSchema.label,
|
|
5368
|
+
objectIcon: objectSchema.icon,
|
|
5369
|
+
label
|
|
5370
|
+
});
|
|
5371
|
+
}
|
|
4095
5372
|
}
|
|
4096
5373
|
return resolved;
|
|
4097
5374
|
}
|
|
@@ -4118,6 +5395,70 @@ var RecordService = class {
|
|
|
4118
5395
|
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
4119
5396
|
this.relationService = new RelationService(adapter, registry);
|
|
4120
5397
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
5398
|
+
this.permissionService = options?.permissionService;
|
|
5399
|
+
this.auditService = options?.auditService;
|
|
5400
|
+
this.userId = options?.userId;
|
|
5401
|
+
this.userEmail = options?.userEmail;
|
|
5402
|
+
}
|
|
5403
|
+
/**
|
|
5404
|
+
* Check permission for an action on an object.
|
|
5405
|
+
* Only checks if permissionService and userId are configured.
|
|
5406
|
+
* @internal
|
|
5407
|
+
*/
|
|
5408
|
+
async checkPermission(objectName, action) {
|
|
5409
|
+
if (this.permissionService && this.userId) {
|
|
5410
|
+
await this.permissionService.checkObjectAccess(this.userId, objectName, action);
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
/**
|
|
5414
|
+
* Resolve relation IDs to their display labels
|
|
5415
|
+
* @internal
|
|
5416
|
+
*/
|
|
5417
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complexity is ok here
|
|
5418
|
+
async resolveRelationLabels(relationAttrs, values) {
|
|
5419
|
+
const resolvedMap = /* @__PURE__ */ new Map();
|
|
5420
|
+
const idsWithAttrId = [];
|
|
5421
|
+
const idsWithoutAttrId = [];
|
|
5422
|
+
for (const attr of relationAttrs) {
|
|
5423
|
+
const val = values[attr.name];
|
|
5424
|
+
const ids = this.extractRelationIds(val);
|
|
5425
|
+
if (ids.length > 0) {
|
|
5426
|
+
if (attr.id) {
|
|
5427
|
+
idsWithAttrId.push({ attrId: attr.id, ids });
|
|
5428
|
+
} else {
|
|
5429
|
+
idsWithoutAttrId.push(...ids);
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
await Promise.all(
|
|
5434
|
+
idsWithAttrId.map(async ({ attrId, ids }) => {
|
|
5435
|
+
const resolved = await this.relationService.resolveIds(ids, this.tenantId, attrId);
|
|
5436
|
+
for (const r of resolved) {
|
|
5437
|
+
resolvedMap.set(r.id, r.label);
|
|
5438
|
+
}
|
|
5439
|
+
})
|
|
5440
|
+
);
|
|
5441
|
+
if (idsWithoutAttrId.length > 0) {
|
|
5442
|
+
const uniqueIds = [...new Set(idsWithoutAttrId)].filter((id) => !resolvedMap.has(id));
|
|
5443
|
+
if (uniqueIds.length > 0) {
|
|
5444
|
+
const records = await this.adapter.objectRecords.findByIds(uniqueIds);
|
|
5445
|
+
for (const record of records) {
|
|
5446
|
+
if (record.label) {
|
|
5447
|
+
resolvedMap.set(record.id, record.label);
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
return resolvedMap;
|
|
5453
|
+
}
|
|
5454
|
+
/**
|
|
5455
|
+
* Extract relation IDs from a value (string or array)
|
|
5456
|
+
* @internal
|
|
5457
|
+
*/
|
|
5458
|
+
extractRelationIds(val) {
|
|
5459
|
+
if (typeof val === "string") return [val];
|
|
5460
|
+
if (Array.isArray(val) && typeof val[0] === "string") return [val[0]];
|
|
5461
|
+
return [];
|
|
4121
5462
|
}
|
|
4122
5463
|
/**
|
|
4123
5464
|
* Compute display label from schema expression
|
|
@@ -4132,27 +5473,16 @@ var RecordService = class {
|
|
|
4132
5473
|
if (relationAttrs.length === 0) {
|
|
4133
5474
|
return renderLabelExpression(schema.labelExpression, values);
|
|
4134
5475
|
}
|
|
4135
|
-
const
|
|
4136
|
-
|
|
4137
|
-
const val = values[attr.name];
|
|
4138
|
-
if (typeof val === "string") {
|
|
4139
|
-
idsToResolve.push(val);
|
|
4140
|
-
} else if (Array.isArray(val) && typeof val[0] === "string") {
|
|
4141
|
-
idsToResolve.push(val[0]);
|
|
4142
|
-
}
|
|
4143
|
-
}
|
|
4144
|
-
if (idsToResolve.length === 0) {
|
|
5476
|
+
const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
|
|
5477
|
+
if (resolvedMap.size === 0) {
|
|
4145
5478
|
return renderLabelExpression(schema.labelExpression, values);
|
|
4146
5479
|
}
|
|
4147
|
-
const resolved = await this.relationService.resolveIds(idsToResolve, this.tenantId);
|
|
4148
|
-
const resolvedMap = new Map(resolved.map((r) => [r.id, r.label]));
|
|
4149
5480
|
const enrichedValues = { ...values };
|
|
4150
5481
|
for (const attr of relationAttrs) {
|
|
4151
5482
|
const val = values[attr.name];
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
enrichedValues[attr.name] = resolvedMap.get(val[0]);
|
|
5483
|
+
const ids = this.extractRelationIds(val);
|
|
5484
|
+
if (ids.length > 0 && resolvedMap.has(ids[0])) {
|
|
5485
|
+
enrichedValues[attr.name] = resolvedMap.get(ids[0]);
|
|
4156
5486
|
}
|
|
4157
5487
|
}
|
|
4158
5488
|
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
@@ -4188,6 +5518,7 @@ var RecordService = class {
|
|
|
4188
5518
|
*/
|
|
4189
5519
|
async createRecord(objectId, data, options) {
|
|
4190
5520
|
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
5521
|
+
await this.checkPermission(schema.name, "create");
|
|
4191
5522
|
const hookCtx = this.buildCreateHookContext(schema, data, options?.hookMetadata);
|
|
4192
5523
|
if (!options?.skipHooks) {
|
|
4193
5524
|
await this.hookRegistry.execute("beforeCreate", schema.name, hookCtx);
|
|
@@ -4219,6 +5550,18 @@ var RecordService = class {
|
|
|
4219
5550
|
};
|
|
4220
5551
|
await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
|
|
4221
5552
|
}
|
|
5553
|
+
if (this.auditService && this.userId) {
|
|
5554
|
+
await this.auditService.logRecordAction({
|
|
5555
|
+
action: "record.created",
|
|
5556
|
+
actorId: this.userId,
|
|
5557
|
+
actorEmail: this.userEmail,
|
|
5558
|
+
objectName: schema.name,
|
|
5559
|
+
objectId: schema.id,
|
|
5560
|
+
recordId: record.id,
|
|
5561
|
+
recordLabel: record.label,
|
|
5562
|
+
metadata: options?.hookMetadata
|
|
5563
|
+
});
|
|
5564
|
+
}
|
|
4222
5565
|
return record;
|
|
4223
5566
|
}
|
|
4224
5567
|
/**
|
|
@@ -4277,6 +5620,7 @@ var RecordService = class {
|
|
|
4277
5620
|
async updateRecord(recordId, data, options) {
|
|
4278
5621
|
const existing = await this.getRecordOrThrow(recordId);
|
|
4279
5622
|
const schema = await this.schemaService.getObjectSchema(existing.objectId);
|
|
5623
|
+
await this.checkPermission(schema.name, "update");
|
|
4280
5624
|
const mergedData = { ...existing.values, ...data };
|
|
4281
5625
|
const changedAttributes = Object.keys(data).filter((key) => existing.values[key] !== data[key]);
|
|
4282
5626
|
const hookCtx = this.buildHookContext(
|
|
@@ -4314,6 +5658,24 @@ var RecordService = class {
|
|
|
4314
5658
|
};
|
|
4315
5659
|
await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
|
|
4316
5660
|
}
|
|
5661
|
+
if (this.auditService && this.userId && changedAttributes.length > 0) {
|
|
5662
|
+
const changes = changedAttributes.map((attr) => ({
|
|
5663
|
+
field: attr,
|
|
5664
|
+
oldValue: hookCtx.oldValues?.[attr],
|
|
5665
|
+
newValue: data[attr]
|
|
5666
|
+
}));
|
|
5667
|
+
await this.auditService.logRecordAction({
|
|
5668
|
+
action: "record.updated",
|
|
5669
|
+
actorId: this.userId,
|
|
5670
|
+
actorEmail: this.userEmail,
|
|
5671
|
+
objectName: schema.name,
|
|
5672
|
+
objectId: schema.id,
|
|
5673
|
+
recordId: updated.id,
|
|
5674
|
+
recordLabel: updated.label,
|
|
5675
|
+
changes,
|
|
5676
|
+
metadata: options?.hookMetadata
|
|
5677
|
+
});
|
|
5678
|
+
}
|
|
4317
5679
|
return updated;
|
|
4318
5680
|
}
|
|
4319
5681
|
/**
|
|
@@ -4403,11 +5765,18 @@ var RecordService = class {
|
|
|
4403
5765
|
async deleteRecord(recordId, options) {
|
|
4404
5766
|
const record = await this.getRecordOrThrow(recordId);
|
|
4405
5767
|
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
5768
|
+
await this.checkPermission(schema.name, "delete");
|
|
4406
5769
|
if (options?.checkSystem) {
|
|
4407
5770
|
if (schema.system) {
|
|
4408
5771
|
throw new Error(`Cannot delete record of system object "${schema.label}"`);
|
|
4409
5772
|
}
|
|
4410
5773
|
}
|
|
5774
|
+
if (!options?.skipReferenceCheck) {
|
|
5775
|
+
const references = await this.adapter.objectRecords.countRecordsReferencingId(recordId);
|
|
5776
|
+
if (references.length > 0) {
|
|
5777
|
+
throw new RecordReferencedError(recordId, references);
|
|
5778
|
+
}
|
|
5779
|
+
}
|
|
4411
5780
|
const hookCtx = this.buildDeleteHookContext(schema, record, options?.hookMetadata);
|
|
4412
5781
|
if (!options?.skipHooks) {
|
|
4413
5782
|
await this.hookRegistry.execute("beforeDelete", schema.name, hookCtx);
|
|
@@ -4416,6 +5785,110 @@ var RecordService = class {
|
|
|
4416
5785
|
if (!options?.skipHooks) {
|
|
4417
5786
|
await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
|
|
4418
5787
|
}
|
|
5788
|
+
if (this.auditService && this.userId) {
|
|
5789
|
+
await this.auditService.logRecordAction({
|
|
5790
|
+
action: "record.deleted",
|
|
5791
|
+
actorId: this.userId,
|
|
5792
|
+
actorEmail: this.userEmail,
|
|
5793
|
+
objectName: schema.name,
|
|
5794
|
+
objectId: schema.id,
|
|
5795
|
+
recordId: record.id,
|
|
5796
|
+
recordLabel: record.label,
|
|
5797
|
+
metadata: options?.hookMetadata
|
|
5798
|
+
});
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5801
|
+
/**
|
|
5802
|
+
* Restore a soft-deleted record
|
|
5803
|
+
*
|
|
5804
|
+
* Triggers beforeRestore and afterRestore hooks if a HookRegistry is configured.
|
|
5805
|
+
*
|
|
5806
|
+
* @param recordId - Record UUID
|
|
5807
|
+
* @param options - Restore options
|
|
5808
|
+
* @returns Restored record
|
|
5809
|
+
*
|
|
5810
|
+
* @example
|
|
5811
|
+
* ```typescript
|
|
5812
|
+
* // Restore a deleted record
|
|
5813
|
+
* const restored = await service.restoreRecord("rec-123");
|
|
5814
|
+
* console.log(restored.deletedAt); // null
|
|
5815
|
+
* ```
|
|
5816
|
+
*/
|
|
5817
|
+
async restoreRecord(recordId, options) {
|
|
5818
|
+
const record = await this.getRecordOrThrow(recordId);
|
|
5819
|
+
if (!record.deletedAt) {
|
|
5820
|
+
throw new Error(`Record "${recordId}" is not deleted`);
|
|
5821
|
+
}
|
|
5822
|
+
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
5823
|
+
await this.checkPermission(schema.name, "update");
|
|
5824
|
+
const hookCtx = this.buildRestoreHookContext(schema, record, options?.hookMetadata);
|
|
5825
|
+
if (!options?.skipHooks) {
|
|
5826
|
+
await this.hookRegistry.execute("beforeRestore", schema.name, hookCtx);
|
|
5827
|
+
}
|
|
5828
|
+
const restored = await this.adapter.objectRecords.restore(recordId);
|
|
5829
|
+
if (!options?.skipHooks) {
|
|
5830
|
+
const afterCtx = {
|
|
5831
|
+
...hookCtx,
|
|
5832
|
+
record: restored
|
|
5833
|
+
};
|
|
5834
|
+
await this.hookRegistry.execute("afterRestore", schema.name, afterCtx);
|
|
5835
|
+
}
|
|
5836
|
+
if (this.auditService && this.userId) {
|
|
5837
|
+
await this.auditService.logRecordAction({
|
|
5838
|
+
action: "record.restored",
|
|
5839
|
+
actorId: this.userId,
|
|
5840
|
+
actorEmail: this.userEmail,
|
|
5841
|
+
objectName: schema.name,
|
|
5842
|
+
objectId: schema.id,
|
|
5843
|
+
recordId: restored.id,
|
|
5844
|
+
recordLabel: restored.label,
|
|
5845
|
+
metadata: options?.hookMetadata
|
|
5846
|
+
});
|
|
5847
|
+
}
|
|
5848
|
+
return restored;
|
|
5849
|
+
}
|
|
5850
|
+
/**
|
|
5851
|
+
* Build hook context for restore operations
|
|
5852
|
+
* @internal
|
|
5853
|
+
*/
|
|
5854
|
+
buildRestoreHookContext(schema, record, metadata) {
|
|
5855
|
+
return {
|
|
5856
|
+
objectId: schema.id,
|
|
5857
|
+
objectName: schema.name,
|
|
5858
|
+
recordId: record.id,
|
|
5859
|
+
tenantId: this.tenantId,
|
|
5860
|
+
record,
|
|
5861
|
+
oldValues: record.values,
|
|
5862
|
+
newValues: record.values,
|
|
5863
|
+
changedAttributes: [],
|
|
5864
|
+
getChange: (attr) => ({
|
|
5865
|
+
oldValue: record.values[attr],
|
|
5866
|
+
newValue: record.values[attr],
|
|
5867
|
+
changed: false
|
|
5868
|
+
}),
|
|
5869
|
+
metadata: metadata ?? {},
|
|
5870
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
5871
|
+
};
|
|
5872
|
+
}
|
|
5873
|
+
/**
|
|
5874
|
+
* Permanently delete a record (hard delete)
|
|
5875
|
+
*
|
|
5876
|
+
* This cannot be undone. Use with caution - prefer soft delete for data safety.
|
|
5877
|
+
* Does NOT trigger delete hooks (already triggered on soft delete).
|
|
5878
|
+
*
|
|
5879
|
+
* @param recordId - Record UUID
|
|
5880
|
+
*
|
|
5881
|
+
* @example
|
|
5882
|
+
* ```typescript
|
|
5883
|
+
* // Permanently delete a record
|
|
5884
|
+
* await service.hardDeleteRecord("rec-123");
|
|
5885
|
+
* ```
|
|
5886
|
+
*/
|
|
5887
|
+
async hardDeleteRecord(recordId) {
|
|
5888
|
+
const record = await this.getRecordOrThrow(recordId);
|
|
5889
|
+
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
5890
|
+
await this.checkPermission(schema.name, "delete");
|
|
5891
|
+
await this.adapter.objectRecords.hardDelete(recordId);
|
|
4419
5892
|
}
|
|
4420
5893
|
/**
|
|
4421
5894
|
* List records for an object with pagination
|
|
@@ -4425,6 +5898,10 @@ var RecordService = class {
|
|
|
4425
5898
|
* @returns Records and total count
|
|
4426
5899
|
*/
|
|
4427
5900
|
async listRecords(objectId, options) {
|
|
5901
|
+
if (this.permissionService && this.userId) {
|
|
5902
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
5903
|
+
await this.checkPermission(schema.name, "read");
|
|
5904
|
+
}
|
|
4428
5905
|
return await this.adapter.objectRecords.list(this.tenantId, objectId, options);
|
|
4429
5906
|
}
|
|
4430
5907
|
/**
|
|
@@ -4436,6 +5913,10 @@ var RecordService = class {
|
|
|
4436
5913
|
* @returns Matching records and total count
|
|
4437
5914
|
*/
|
|
4438
5915
|
async searchRecords(objectId, query, options) {
|
|
5916
|
+
if (this.permissionService && this.userId) {
|
|
5917
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
5918
|
+
await this.checkPermission(schema.name, "read");
|
|
5919
|
+
}
|
|
4439
5920
|
return await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
|
|
4440
5921
|
}
|
|
4441
5922
|
/**
|
|
@@ -4483,9 +5964,12 @@ var RecordService = class {
|
|
|
4483
5964
|
|
|
4484
5965
|
// src/runtime/services/user-profile.service.ts
|
|
4485
5966
|
var UserProfileService = class {
|
|
4486
|
-
constructor(adapter, tenantId) {
|
|
5967
|
+
constructor(adapter, tenantId, options) {
|
|
4487
5968
|
this.adapter = adapter;
|
|
4488
5969
|
this.tenantId = tenantId;
|
|
5970
|
+
this.auditService = options?.auditService;
|
|
5971
|
+
this.userId = options?.userId;
|
|
5972
|
+
this.userEmail = options?.userEmail;
|
|
4489
5973
|
}
|
|
4490
5974
|
/**
|
|
4491
5975
|
* Create a new user profile (typically after first auth)
|
|
@@ -4523,7 +6007,17 @@ var UserProfileService = class {
|
|
|
4523
6007
|
if (existingEmail) {
|
|
4524
6008
|
throw new Error(`User profile with email "${data.email}" already exists in this tenant`);
|
|
4525
6009
|
}
|
|
4526
|
-
|
|
6010
|
+
const profile = await this.adapter.userProfiles.create(data);
|
|
6011
|
+
if (this.auditService && this.userId) {
|
|
6012
|
+
await this.auditService.logUserAction({
|
|
6013
|
+
action: "user.created",
|
|
6014
|
+
actorId: this.userId,
|
|
6015
|
+
actorEmail: this.userEmail,
|
|
6016
|
+
targetUserId: profile.id,
|
|
6017
|
+
targetUserEmail: profile.email
|
|
6018
|
+
});
|
|
6019
|
+
}
|
|
6020
|
+
return profile;
|
|
4527
6021
|
}
|
|
4528
6022
|
/**
|
|
4529
6023
|
* Get user profile by ID
|
|
@@ -4584,8 +6078,27 @@ var UserProfileService = class {
|
|
|
4584
6078
|
* Update user profile
|
|
4585
6079
|
*/
|
|
4586
6080
|
async updateProfile(profileId, data) {
|
|
4587
|
-
await this.getProfileOrThrow(profileId);
|
|
4588
|
-
|
|
6081
|
+
const existing = await this.getProfileOrThrow(profileId);
|
|
6082
|
+
const updated = await this.adapter.userProfiles.update(profileId, data);
|
|
6083
|
+
if (this.auditService && this.userId) {
|
|
6084
|
+
const changes = buildAuditChanges(existing, data, [
|
|
6085
|
+
"firstName",
|
|
6086
|
+
"lastName",
|
|
6087
|
+
"role",
|
|
6088
|
+
"status"
|
|
6089
|
+
]);
|
|
6090
|
+
if (changes.length > 0) {
|
|
6091
|
+
await this.auditService.logUserAction({
|
|
6092
|
+
action: "user.updated",
|
|
6093
|
+
actorId: this.userId,
|
|
6094
|
+
actorEmail: this.userEmail,
|
|
6095
|
+
targetUserId: profileId,
|
|
6096
|
+
targetUserEmail: updated.email,
|
|
6097
|
+
changes
|
|
6098
|
+
});
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
return updated;
|
|
4589
6102
|
}
|
|
4590
6103
|
/**
|
|
4591
6104
|
* Delete user profile
|
|
@@ -4594,17 +6107,25 @@ var UserProfileService = class {
|
|
|
4594
6107
|
* @param options - Delete options
|
|
4595
6108
|
*/
|
|
4596
6109
|
async deleteProfile(profileId, options) {
|
|
6110
|
+
const profile = await this.getProfileOrThrow(profileId);
|
|
4597
6111
|
if (options?.checkAdmin) {
|
|
4598
|
-
const profile = await this.getProfileOrThrow(profileId);
|
|
4599
6112
|
if (profile.role === "admin") {
|
|
4600
|
-
const
|
|
4601
|
-
const adminCount = allProfiles.filter((p) => p.role === "admin").length;
|
|
6113
|
+
const adminCount = await this.adapter.userProfiles.countByRole(this.tenantId, "admin");
|
|
4602
6114
|
if (adminCount <= 1) {
|
|
4603
6115
|
throw new Error("Cannot delete the last admin user");
|
|
4604
6116
|
}
|
|
4605
6117
|
}
|
|
4606
6118
|
}
|
|
4607
6119
|
await this.adapter.userProfiles.delete(profileId);
|
|
6120
|
+
if (this.auditService && this.userId) {
|
|
6121
|
+
await this.auditService.logUserAction({
|
|
6122
|
+
action: "user.deleted",
|
|
6123
|
+
actorId: this.userId,
|
|
6124
|
+
actorEmail: this.userEmail,
|
|
6125
|
+
targetUserId: profileId,
|
|
6126
|
+
targetUserEmail: profile.email
|
|
6127
|
+
});
|
|
6128
|
+
}
|
|
4608
6129
|
}
|
|
4609
6130
|
/**
|
|
4610
6131
|
* List all user profiles for the tenant
|
|
@@ -5191,12 +6712,20 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
5191
6712
|
}
|
|
5192
6713
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5193
6714
|
0 && (module.exports = {
|
|
6715
|
+
ALL_SYSTEM_RESOURCES,
|
|
6716
|
+
AttributeInUseError,
|
|
5194
6717
|
AttributeNotFoundError,
|
|
6718
|
+
AuditService,
|
|
5195
6719
|
CustomTabConfig,
|
|
5196
6720
|
DEFAULT_LABEL_FALLBACK,
|
|
6721
|
+
DEFAULT_ROLES,
|
|
6722
|
+
DEFAULT_ROLE_DESCRIPTIONS,
|
|
6723
|
+
DEFAULT_ROLE_LABELS,
|
|
6724
|
+
DEFAULT_ROLE_PERMISSIONS,
|
|
5197
6725
|
DuplicateError,
|
|
5198
6726
|
FileNotFoundError,
|
|
5199
6727
|
FileService,
|
|
6728
|
+
ForbiddenError,
|
|
5200
6729
|
GeocodingService,
|
|
5201
6730
|
GlobalSearchService,
|
|
5202
6731
|
GroupBuilder,
|
|
@@ -5208,11 +6737,18 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
5208
6737
|
OPERATORS_BY_TYPE,
|
|
5209
6738
|
ObjectBuilder,
|
|
5210
6739
|
ObjectNotFoundError,
|
|
6740
|
+
ObjectReferencedError,
|
|
5211
6741
|
ObjectSchemaService,
|
|
6742
|
+
PermissionService,
|
|
5212
6743
|
ProtectedResourceError,
|
|
6744
|
+
ProtectedRoleError,
|
|
5213
6745
|
RecordNotFoundError,
|
|
6746
|
+
RecordReferencedError,
|
|
5214
6747
|
RecordService,
|
|
5215
6748
|
RelationService,
|
|
6749
|
+
RoleNotFoundError,
|
|
6750
|
+
SYSTEM_RESOURCES,
|
|
6751
|
+
SYSTEM_RESOURCE_LABELS,
|
|
5216
6752
|
SchemaError,
|
|
5217
6753
|
SchemaErrorCode,
|
|
5218
6754
|
SyncError,
|
|
@@ -5224,6 +6760,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
5224
6760
|
ViewBuilder,
|
|
5225
6761
|
ViewService,
|
|
5226
6762
|
attributeConfigSchemas,
|
|
6763
|
+
buildAuditChanges,
|
|
5227
6764
|
checkbox,
|
|
5228
6765
|
checkboxConfigSchema,
|
|
5229
6766
|
computeRecordStatus,
|
|
@@ -5264,6 +6801,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
5264
6801
|
group,
|
|
5265
6802
|
isAdvancedFilterState,
|
|
5266
6803
|
isCustomTab,
|
|
6804
|
+
isDefaultRole,
|
|
6805
|
+
isForbiddenError,
|
|
5267
6806
|
isFormTab,
|
|
5268
6807
|
isLabelExpression,
|
|
5269
6808
|
isNoValueOperator,
|