@stndrds/schema 0.1.0-alpha.14 → 0.1.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2ASXJITY.mjs +5360 -0
- package/dist/chunk-5AWYVQKR.mjs +5310 -0
- package/dist/chunk-65HUQZHK.mjs +5374 -0
- package/dist/chunk-73P4F2OU.mjs +5809 -0
- package/dist/chunk-BHKKVBPP.mjs +5527 -0
- package/dist/chunk-BTVPMGYW.mjs +6327 -0
- package/dist/chunk-BUWVNOSK.mjs +5457 -0
- package/dist/chunk-C7HZ55GJ.mjs +6882 -0
- package/dist/chunk-CCEHW4E7.mjs +6071 -0
- package/dist/chunk-CNR3BZLU.mjs +5163 -0
- package/dist/chunk-DLMVX6DN.mjs +6074 -0
- package/dist/chunk-DUUORFHI.mjs +5379 -0
- package/dist/chunk-DWTD7YPJ.mjs +74 -0
- package/dist/chunk-FAR5ENGR.mjs +78 -0
- package/dist/chunk-ITWOVZOW.mjs +5809 -0
- package/dist/chunk-JDUTFH3J.mjs +6940 -0
- package/dist/chunk-KJLKY5X7.mjs +6314 -0
- package/dist/chunk-KTJXMGBR.mjs +5397 -0
- package/dist/chunk-NOVHQ7B2.mjs +6327 -0
- package/dist/chunk-PMFQ4ZL4.mjs +6230 -0
- package/dist/chunk-QAGN53HE.mjs +6327 -0
- package/dist/chunk-QC47SMCZ.mjs +5467 -0
- package/dist/chunk-UUOAVXAI.mjs +6327 -0
- package/dist/chunk-VYZ3BOSB.mjs +6946 -0
- package/dist/chunk-XAX75NSP.mjs +6321 -0
- package/dist/chunk-Z6LIJ5PO.mjs +5362 -0
- package/dist/chunk-ZG3PT3RL.mjs +6944 -0
- package/dist/default-roles-G4TOWUVM.mjs +20 -0
- package/dist/default-roles-QEZPRSUL.mjs +20 -0
- package/dist/index.d.mts +419 -16
- package/dist/index.d.ts +419 -16
- package/dist/index.js +2580 -406
- package/dist/index.mjs +80 -163
- package/dist/runtime-B1_syHVj.d.mts +4787 -0
- package/dist/runtime-B1_syHVj.d.ts +4787 -0
- package/dist/runtime-B7N-oycb.d.mts +4854 -0
- package/dist/runtime-B7N-oycb.d.ts +4854 -0
- package/dist/runtime-BDoAIuTe.d.mts +4872 -0
- package/dist/runtime-BDoAIuTe.d.ts +4872 -0
- package/dist/runtime-BPYJWZfC.d.mts +5130 -0
- package/dist/runtime-BPYJWZfC.d.ts +5130 -0
- package/dist/runtime-BVRmzg3d.d.mts +4718 -0
- package/dist/runtime-BVRmzg3d.d.ts +4718 -0
- package/dist/runtime-BrXwIidf.d.mts +4794 -0
- package/dist/runtime-BrXwIidf.d.ts +4794 -0
- package/dist/runtime-CC_HZhVN.d.mts +5224 -0
- package/dist/runtime-CC_HZhVN.d.ts +5224 -0
- package/dist/runtime-CEx57sBZ.d.mts +4791 -0
- package/dist/runtime-CEx57sBZ.d.ts +4791 -0
- package/dist/runtime-COGvuXIq.d.mts +4790 -0
- package/dist/runtime-COGvuXIq.d.ts +4790 -0
- package/dist/runtime-CSGWjztI.d.mts +5166 -0
- package/dist/runtime-CSGWjztI.d.ts +5166 -0
- package/dist/runtime-DRc_OhnM.d.mts +5142 -0
- package/dist/runtime-DRc_OhnM.d.ts +5142 -0
- package/dist/runtime-DUxWyRcq.d.mts +5399 -0
- package/dist/runtime-DUxWyRcq.d.ts +5399 -0
- package/dist/runtime-D_S3m9jf.d.mts +5142 -0
- package/dist/runtime-D_S3m9jf.d.ts +5142 -0
- package/dist/runtime-Db3rRxbJ.d.mts +4821 -0
- package/dist/runtime-Db3rRxbJ.d.ts +4821 -0
- package/dist/runtime-DlCiIIfr.d.mts +5130 -0
- package/dist/runtime-DlCiIIfr.d.ts +5130 -0
- package/dist/runtime-LO17NDk1.d.mts +5292 -0
- package/dist/runtime-LO17NDk1.d.ts +5292 -0
- package/dist/runtime-WqYxjhjJ.d.mts +5125 -0
- package/dist/runtime-WqYxjhjJ.d.ts +5125 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +2178 -370
- package/dist/runtime.mjs +9 -1
- package/package.json +3 -3
package/dist/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,112 @@ 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
|
+
FlowBuilder: () => FlowBuilder,
|
|
135
|
+
FlowPageBuilder: () => FlowPageBuilder,
|
|
136
|
+
FlowRowBuilder: () => FlowRowBuilder,
|
|
137
|
+
FlowService: () => FlowService,
|
|
138
|
+
ForbiddenError: () => ForbiddenError,
|
|
39
139
|
GeocodingService: () => GeocodingService,
|
|
40
140
|
GlobalSearchService: () => GlobalSearchService,
|
|
41
141
|
GroupBuilder: () => GroupBuilder,
|
|
@@ -47,11 +147,18 @@ __export(index_exports, {
|
|
|
47
147
|
OPERATORS_BY_TYPE: () => OPERATORS_BY_TYPE,
|
|
48
148
|
ObjectBuilder: () => ObjectBuilder,
|
|
49
149
|
ObjectNotFoundError: () => ObjectNotFoundError,
|
|
150
|
+
ObjectReferencedError: () => ObjectReferencedError,
|
|
50
151
|
ObjectSchemaService: () => ObjectSchemaService,
|
|
152
|
+
PermissionService: () => PermissionService,
|
|
51
153
|
ProtectedResourceError: () => ProtectedResourceError,
|
|
154
|
+
ProtectedRoleError: () => ProtectedRoleError,
|
|
52
155
|
RecordNotFoundError: () => RecordNotFoundError,
|
|
156
|
+
RecordReferencedError: () => RecordReferencedError,
|
|
53
157
|
RecordService: () => RecordService,
|
|
54
158
|
RelationService: () => RelationService,
|
|
159
|
+
RoleNotFoundError: () => RoleNotFoundError,
|
|
160
|
+
SYSTEM_RESOURCES: () => SYSTEM_RESOURCES,
|
|
161
|
+
SYSTEM_RESOURCE_LABELS: () => SYSTEM_RESOURCE_LABELS,
|
|
55
162
|
SchemaError: () => SchemaError,
|
|
56
163
|
SchemaErrorCode: () => SchemaErrorCode,
|
|
57
164
|
SyncError: () => SyncError,
|
|
@@ -63,6 +170,7 @@ __export(index_exports, {
|
|
|
63
170
|
ViewBuilder: () => ViewBuilder,
|
|
64
171
|
ViewService: () => ViewService,
|
|
65
172
|
attributeConfigSchemas: () => attributeConfigSchemas,
|
|
173
|
+
buildAuditChanges: () => buildAuditChanges,
|
|
66
174
|
checkbox: () => checkbox,
|
|
67
175
|
checkboxConfigSchema: () => checkboxConfigSchema,
|
|
68
176
|
computeRecordStatus: () => computeRecordStatus,
|
|
@@ -94,6 +202,7 @@ __export(index_exports, {
|
|
|
94
202
|
extractAttributeNames: () => extractAttributeNames,
|
|
95
203
|
file: () => file,
|
|
96
204
|
fileConfigSchema: () => fileConfigSchema,
|
|
205
|
+
flow: () => flow,
|
|
97
206
|
generateId: () => generateId,
|
|
98
207
|
generatePrefixedId: () => generatePrefixedId,
|
|
99
208
|
getAttributeConfigSchema: () => getAttributeConfigSchema,
|
|
@@ -103,6 +212,10 @@ __export(index_exports, {
|
|
|
103
212
|
group: () => group,
|
|
104
213
|
isAdvancedFilterState: () => isAdvancedFilterState,
|
|
105
214
|
isCustomTab: () => isCustomTab,
|
|
215
|
+
isDefaultRole: () => isDefaultRole,
|
|
216
|
+
isFlowDefinition: () => isFlowDefinition,
|
|
217
|
+
isFlowPublished: () => isFlowPublished,
|
|
218
|
+
isForbiddenError: () => isForbiddenError,
|
|
106
219
|
isFormTab: () => isFormTab,
|
|
107
220
|
isLabelExpression: () => isLabelExpression,
|
|
108
221
|
isNoValueOperator: () => isNoValueOperator,
|
|
@@ -110,6 +223,7 @@ __export(index_exports, {
|
|
|
110
223
|
isProtectedResourceError: () => isProtectedResourceError,
|
|
111
224
|
isRecordComplete: () => isRecordComplete,
|
|
112
225
|
isSchemaError: () => isSchemaError,
|
|
226
|
+
isSystemFlow: () => isSystemFlow,
|
|
113
227
|
isTableTab: () => isTableTab,
|
|
114
228
|
isValidationError: () => isValidationError,
|
|
115
229
|
location: () => location,
|
|
@@ -159,6 +273,38 @@ __export(index_exports, {
|
|
|
159
273
|
});
|
|
160
274
|
module.exports = __toCommonJS(index_exports);
|
|
161
275
|
|
|
276
|
+
// src/types/errors.ts
|
|
277
|
+
var RecordReferencedError = class extends Error {
|
|
278
|
+
constructor(recordId, references) {
|
|
279
|
+
const total = references.reduce((sum, r) => sum + r.count, 0);
|
|
280
|
+
super(`Cannot delete record: referenced by ${total} record${total > 1 ? "s" : ""}`);
|
|
281
|
+
this.recordId = recordId;
|
|
282
|
+
this.references = references;
|
|
283
|
+
this.code = "RECORD_REFERENCED";
|
|
284
|
+
this.name = "RecordReferencedError";
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
var AttributeInUseError = class extends Error {
|
|
288
|
+
constructor(attributeName, usage) {
|
|
289
|
+
super(`Cannot delete attribute "${attributeName}": used in ${usage}`);
|
|
290
|
+
this.attributeName = attributeName;
|
|
291
|
+
this.usage = usage;
|
|
292
|
+
this.code = "ATTRIBUTE_IN_USE";
|
|
293
|
+
this.name = "AttributeInUseError";
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
var ObjectReferencedError = class extends Error {
|
|
297
|
+
constructor(objectName, referencingObjects) {
|
|
298
|
+
super(
|
|
299
|
+
`Cannot delete object "${objectName}": target of relations in ${referencingObjects.join(", ")}`
|
|
300
|
+
);
|
|
301
|
+
this.objectName = objectName;
|
|
302
|
+
this.referencingObjects = referencingObjects;
|
|
303
|
+
this.code = "OBJECT_REFERENCED";
|
|
304
|
+
this.name = "ObjectReferencedError";
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
162
308
|
// src/types/filters.ts
|
|
163
309
|
function isAdvancedFilterState(state) {
|
|
164
310
|
return "groups" in state;
|
|
@@ -254,6 +400,17 @@ function isNoValueOperator(operator) {
|
|
|
254
400
|
return NO_VALUE_OPERATORS.includes(operator);
|
|
255
401
|
}
|
|
256
402
|
|
|
403
|
+
// src/types/flows.ts
|
|
404
|
+
function isFlowDefinition(obj) {
|
|
405
|
+
return typeof obj === "object" && obj !== null && "slots" in obj && "pages" in obj && "relations" in obj && "status" in obj;
|
|
406
|
+
}
|
|
407
|
+
function isFlowPublished(flow2) {
|
|
408
|
+
return flow2.status === "published";
|
|
409
|
+
}
|
|
410
|
+
function isSystemFlow(flow2) {
|
|
411
|
+
return flow2.system === true;
|
|
412
|
+
}
|
|
413
|
+
|
|
257
414
|
// src/types/geocoding.ts
|
|
258
415
|
var NoopGeocodingAdapter = class {
|
|
259
416
|
async autocomplete() {
|
|
@@ -293,6 +450,9 @@ function generatePrefixedId(prefix) {
|
|
|
293
450
|
return `${prefix}_${generateId()}`;
|
|
294
451
|
}
|
|
295
452
|
|
|
453
|
+
// src/constants/index.ts
|
|
454
|
+
init_default_roles();
|
|
455
|
+
|
|
296
456
|
// src/exceptions.ts
|
|
297
457
|
var SchemaErrorCode = {
|
|
298
458
|
// Generic
|
|
@@ -303,6 +463,7 @@ var SchemaErrorCode = {
|
|
|
303
463
|
RECORD_NOT_FOUND: "SCHEMA_RECORD_NOT_FOUND",
|
|
304
464
|
USER_PROFILE_NOT_FOUND: "SCHEMA_USER_PROFILE_NOT_FOUND",
|
|
305
465
|
FILE_NOT_FOUND: "SCHEMA_FILE_NOT_FOUND",
|
|
466
|
+
ROLE_NOT_FOUND: "SCHEMA_ROLE_NOT_FOUND",
|
|
306
467
|
// Validation
|
|
307
468
|
VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED",
|
|
308
469
|
INVALID_ATTRIBUTE_NAME: "SCHEMA_INVALID_ATTRIBUTE_NAME",
|
|
@@ -310,6 +471,9 @@ var SchemaErrorCode = {
|
|
|
310
471
|
// Protected Resources
|
|
311
472
|
PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
|
|
312
473
|
PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
|
|
474
|
+
PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
|
|
475
|
+
// Permissions
|
|
476
|
+
FORBIDDEN: "SCHEMA_FORBIDDEN",
|
|
313
477
|
// Sync
|
|
314
478
|
SYNC_FAILED: "SCHEMA_SYNC_FAILED",
|
|
315
479
|
NOT_SYSTEM_OBJECT: "SCHEMA_NOT_SYSTEM_OBJECT",
|
|
@@ -454,6 +618,39 @@ function isValidationError(error) {
|
|
|
454
618
|
function isProtectedResourceError(error) {
|
|
455
619
|
return error instanceof ProtectedResourceError;
|
|
456
620
|
}
|
|
621
|
+
var ForbiddenError = class extends SchemaError {
|
|
622
|
+
constructor(objectName, action, userId) {
|
|
623
|
+
super(`No ${action} permission on object "${objectName}"`, SchemaErrorCode.FORBIDDEN, {
|
|
624
|
+
objectName,
|
|
625
|
+
action,
|
|
626
|
+
userId
|
|
627
|
+
});
|
|
628
|
+
this.name = "ForbiddenError";
|
|
629
|
+
this.objectName = objectName;
|
|
630
|
+
this.action = action;
|
|
631
|
+
this.userId = userId;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
var ProtectedRoleError = class extends SchemaError {
|
|
635
|
+
constructor(roleName, operation) {
|
|
636
|
+
super(`Cannot ${operation} system role "${roleName}"`, SchemaErrorCode.PROTECTED_ROLE, {
|
|
637
|
+
roleName,
|
|
638
|
+
operation
|
|
639
|
+
});
|
|
640
|
+
this.name = "ProtectedRoleError";
|
|
641
|
+
this.roleName = roleName;
|
|
642
|
+
this.operation = operation;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
var RoleNotFoundError = class extends NotFoundError {
|
|
646
|
+
constructor(roleId) {
|
|
647
|
+
super("Role", roleId, SchemaErrorCode.ROLE_NOT_FOUND);
|
|
648
|
+
this.name = "RoleNotFoundError";
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
function isForbiddenError(error) {
|
|
652
|
+
return error instanceof ForbiddenError;
|
|
653
|
+
}
|
|
457
654
|
|
|
458
655
|
// src/builders/attribute-validators.ts
|
|
459
656
|
var import_constants = require("@stndrds/constants");
|
|
@@ -1009,14 +1206,6 @@ var SingleRelationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1009
1206
|
this.attr.targets?.push(target);
|
|
1010
1207
|
return this;
|
|
1011
1208
|
}
|
|
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
1209
|
/**
|
|
1021
1210
|
* Convert to a multi-relation (cardinality: "many")
|
|
1022
1211
|
*/
|
|
@@ -1026,7 +1215,6 @@ var SingleRelationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1026
1215
|
this.attr.label ?? "",
|
|
1027
1216
|
{
|
|
1028
1217
|
targets: this.attr.targets,
|
|
1029
|
-
onDelete: this.attr.onDelete,
|
|
1030
1218
|
isRequired: this.attr.required
|
|
1031
1219
|
}
|
|
1032
1220
|
);
|
|
@@ -1047,9 +1235,6 @@ var MultiRelationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1047
1235
|
this.attr.cardinality = "many";
|
|
1048
1236
|
this.attr.targets = initOptions?.targets ?? [];
|
|
1049
1237
|
this.attr.defaultValue = [];
|
|
1050
|
-
if (initOptions?.onDelete) {
|
|
1051
|
-
this.attr.onDelete = initOptions.onDelete;
|
|
1052
|
-
}
|
|
1053
1238
|
if (initOptions?.isRequired) {
|
|
1054
1239
|
this.setRequired(true);
|
|
1055
1240
|
}
|
|
@@ -1067,14 +1252,6 @@ var MultiRelationAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1067
1252
|
this.attr.targets?.push(target);
|
|
1068
1253
|
return this;
|
|
1069
1254
|
}
|
|
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
1255
|
/**
|
|
1079
1256
|
* Set minimum number of relations required
|
|
1080
1257
|
*/
|
|
@@ -1126,8 +1303,361 @@ function rating(config) {
|
|
|
1126
1303
|
return new RatingAttributeBuilder(config.name, config.label);
|
|
1127
1304
|
}
|
|
1128
1305
|
|
|
1306
|
+
// src/builders/flow-builder.ts
|
|
1307
|
+
var import_zod = require("zod");
|
|
1308
|
+
var FlowRowBuilder = class {
|
|
1309
|
+
/** @internal */
|
|
1310
|
+
constructor(pageBuilder, id, order) {
|
|
1311
|
+
this.pageBuilder = pageBuilder;
|
|
1312
|
+
this.rowData = {
|
|
1313
|
+
id,
|
|
1314
|
+
order,
|
|
1315
|
+
fields: []
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Add a field to the current row
|
|
1320
|
+
*
|
|
1321
|
+
* @param slotId - Reference to a slot defined with .slot()
|
|
1322
|
+
* @param attribute - Attribute name on the object
|
|
1323
|
+
* @param options - Optional field configuration
|
|
1324
|
+
*
|
|
1325
|
+
* @example
|
|
1326
|
+
* ```typescript
|
|
1327
|
+
* .row("row-1")
|
|
1328
|
+
* .field("mr", "firstName")
|
|
1329
|
+
* .field("mr", "lastName")
|
|
1330
|
+
* ```
|
|
1331
|
+
*/
|
|
1332
|
+
field(slotId, attribute, options) {
|
|
1333
|
+
const field = {
|
|
1334
|
+
id: `${this.rowData.id}-${slotId}-${attribute}`,
|
|
1335
|
+
slotId,
|
|
1336
|
+
attribute,
|
|
1337
|
+
label: options?.label,
|
|
1338
|
+
required: options?.required
|
|
1339
|
+
};
|
|
1340
|
+
this.rowData.fields.push(field);
|
|
1341
|
+
return this;
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Start a new row on the same page
|
|
1345
|
+
*/
|
|
1346
|
+
row(id) {
|
|
1347
|
+
return this.pageBuilder._finalizeRow(this.rowData).row(id);
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Start a new page
|
|
1351
|
+
*/
|
|
1352
|
+
page(id, label, description) {
|
|
1353
|
+
return this.pageBuilder._finalizeRow(this.rowData)._getFlow().page(id, label, description);
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Build the final flow definition
|
|
1357
|
+
*/
|
|
1358
|
+
build() {
|
|
1359
|
+
return this.pageBuilder._finalizeRow(this.rowData)._getFlow().build();
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
var FlowPageBuilder = class {
|
|
1363
|
+
/** @internal */
|
|
1364
|
+
constructor(flow2, id, label, description) {
|
|
1365
|
+
this.rowOrder = 0;
|
|
1366
|
+
this.flow = flow2;
|
|
1367
|
+
this.pageData = {
|
|
1368
|
+
id,
|
|
1369
|
+
label,
|
|
1370
|
+
description,
|
|
1371
|
+
order: 0,
|
|
1372
|
+
// Will be set when finalized
|
|
1373
|
+
rows: []
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Add a row to the current page
|
|
1378
|
+
*
|
|
1379
|
+
* @param id - Unique row identifier
|
|
1380
|
+
*
|
|
1381
|
+
* @example
|
|
1382
|
+
* ```typescript
|
|
1383
|
+
* .page("identity", "Identité")
|
|
1384
|
+
* .row("name-row")
|
|
1385
|
+
* .field("mr", "firstName")
|
|
1386
|
+
* .field("mr", "lastName")
|
|
1387
|
+
* .row("email-row")
|
|
1388
|
+
* .field("mr", "email")
|
|
1389
|
+
* ```
|
|
1390
|
+
*/
|
|
1391
|
+
row(id) {
|
|
1392
|
+
this.rowOrder++;
|
|
1393
|
+
return new FlowRowBuilder(this, id, this.rowOrder);
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Start a new page
|
|
1397
|
+
*/
|
|
1398
|
+
page(id, label, description) {
|
|
1399
|
+
return this.flow._finalizePage(this.pageData).page(id, label, description);
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Build the final flow definition
|
|
1403
|
+
*/
|
|
1404
|
+
build() {
|
|
1405
|
+
return this.flow._finalizePage(this.pageData).build();
|
|
1406
|
+
}
|
|
1407
|
+
/** @internal */
|
|
1408
|
+
_finalizeRow(row) {
|
|
1409
|
+
if (row.id && row.fields.length > 0) {
|
|
1410
|
+
this.pageData.rows?.push(row);
|
|
1411
|
+
}
|
|
1412
|
+
return this;
|
|
1413
|
+
}
|
|
1414
|
+
/** @internal */
|
|
1415
|
+
_getFlow() {
|
|
1416
|
+
return this.flow._finalizePage(this.pageData);
|
|
1417
|
+
}
|
|
1418
|
+
/** @internal */
|
|
1419
|
+
_getPageData() {
|
|
1420
|
+
return this.pageData;
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
var FlowBuilder = class {
|
|
1424
|
+
constructor(name, label) {
|
|
1425
|
+
this.data = {
|
|
1426
|
+
slots: [],
|
|
1427
|
+
pages: [],
|
|
1428
|
+
relations: [],
|
|
1429
|
+
status: "published",
|
|
1430
|
+
// System flows are always published
|
|
1431
|
+
version: 1,
|
|
1432
|
+
system: true
|
|
1433
|
+
};
|
|
1434
|
+
this.validateName(name);
|
|
1435
|
+
this.data.name = name;
|
|
1436
|
+
this.data.label = label;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Set flow description
|
|
1440
|
+
*/
|
|
1441
|
+
description(value) {
|
|
1442
|
+
this.data.description = value;
|
|
1443
|
+
return this;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Set flow icon
|
|
1447
|
+
*/
|
|
1448
|
+
icon(value) {
|
|
1449
|
+
this.data.icon = value;
|
|
1450
|
+
return this;
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Set extensible metadata
|
|
1454
|
+
*/
|
|
1455
|
+
metadata(value) {
|
|
1456
|
+
this.data.metadata = value;
|
|
1457
|
+
return this;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Add a slot (object to create in the flow)
|
|
1461
|
+
*
|
|
1462
|
+
* @param id - Unique slot identifier (used to reference in fields and relations)
|
|
1463
|
+
* @param objectName - Name of the object to create
|
|
1464
|
+
* @param label - Display label for the slot
|
|
1465
|
+
* @param options - Optional slot configuration
|
|
1466
|
+
*
|
|
1467
|
+
* @example
|
|
1468
|
+
* ```typescript
|
|
1469
|
+
* .slot("mr", "contacts", "Monsieur", { color: "blue", icon: "user" })
|
|
1470
|
+
* .slot("company", "companies", "Entreprise", { color: "green" })
|
|
1471
|
+
* ```
|
|
1472
|
+
*/
|
|
1473
|
+
slot(id, objectName, label, options) {
|
|
1474
|
+
if (this.data.slots?.some((s) => s.id === id)) {
|
|
1475
|
+
throw new Error(`[FlowBuilder] Duplicate slot id: "${id}"`);
|
|
1476
|
+
}
|
|
1477
|
+
this.data.slots?.push({
|
|
1478
|
+
id,
|
|
1479
|
+
objectName,
|
|
1480
|
+
label,
|
|
1481
|
+
color: options?.color,
|
|
1482
|
+
icon: options?.icon
|
|
1483
|
+
});
|
|
1484
|
+
return this;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Define a relation between two slots
|
|
1488
|
+
* The source slot will be linked to the target slot via the specified attribute
|
|
1489
|
+
*
|
|
1490
|
+
* @param sourceSlotId - Slot that has the relation attribute
|
|
1491
|
+
* @param sourceAttribute - Relation attribute name on the source
|
|
1492
|
+
* @param targetSlotId - Target slot to link to
|
|
1493
|
+
*
|
|
1494
|
+
* @example
|
|
1495
|
+
* ```typescript
|
|
1496
|
+
* // mr.company → company (the contact's company field will be set to the created company)
|
|
1497
|
+
* .relation("mr", "company", "company")
|
|
1498
|
+
* ```
|
|
1499
|
+
*/
|
|
1500
|
+
relation(sourceSlotId, sourceAttribute, targetSlotId) {
|
|
1501
|
+
this.data.relations?.push({
|
|
1502
|
+
id: `${sourceSlotId}-${sourceAttribute}-${targetSlotId}`,
|
|
1503
|
+
sourceSlotId,
|
|
1504
|
+
sourceAttribute,
|
|
1505
|
+
targetSlotId
|
|
1506
|
+
});
|
|
1507
|
+
return this;
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Start a new page/step in the flow
|
|
1511
|
+
*
|
|
1512
|
+
* @param id - Unique page identifier
|
|
1513
|
+
* @param label - Display label
|
|
1514
|
+
* @param description - Optional description
|
|
1515
|
+
*
|
|
1516
|
+
* @example
|
|
1517
|
+
* ```typescript
|
|
1518
|
+
* .page("identity", "Identité", "Informations personnelles")
|
|
1519
|
+
* .row("name")
|
|
1520
|
+
* .field("mr", "firstName")
|
|
1521
|
+
* .field("mr", "lastName")
|
|
1522
|
+
* ```
|
|
1523
|
+
*/
|
|
1524
|
+
page(id, label, description) {
|
|
1525
|
+
return new FlowPageBuilder(this, id, label, description);
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Build and validate the flow definition
|
|
1529
|
+
*
|
|
1530
|
+
* @throws Error if validation fails
|
|
1531
|
+
*/
|
|
1532
|
+
build() {
|
|
1533
|
+
this.validateRequiredData();
|
|
1534
|
+
const slotIds = new Set(this.data.slots?.map((s) => s.id) ?? []);
|
|
1535
|
+
this.validateFieldReferences(slotIds);
|
|
1536
|
+
this.validateRelationReferences(slotIds);
|
|
1537
|
+
this.validateNoCircularDependencies();
|
|
1538
|
+
return this.data;
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Validate that required data is present
|
|
1542
|
+
*/
|
|
1543
|
+
validateRequiredData() {
|
|
1544
|
+
if (!this.data.slots || this.data.slots.length === 0) {
|
|
1545
|
+
throw new Error("[FlowBuilder] At least one slot is required. Use .slot() to add slots.");
|
|
1546
|
+
}
|
|
1547
|
+
if (!this.data.pages || this.data.pages.length === 0) {
|
|
1548
|
+
throw new Error("[FlowBuilder] At least one page is required. Use .page() to add pages.");
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Validate that all field slotId references exist
|
|
1553
|
+
*/
|
|
1554
|
+
validateFieldReferences(slotIds) {
|
|
1555
|
+
for (const page of this.data.pages ?? []) {
|
|
1556
|
+
for (const row of page.rows) {
|
|
1557
|
+
for (const field of row.fields) {
|
|
1558
|
+
if (!slotIds.has(field.slotId)) {
|
|
1559
|
+
throw new Error(
|
|
1560
|
+
`[FlowBuilder] Field "${field.id}" references unknown slot "${field.slotId}" in page "${page.id}". Available slots: ${[...slotIds].join(", ")}`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Validate that all relation references exist and are valid
|
|
1569
|
+
*/
|
|
1570
|
+
validateRelationReferences(slotIds) {
|
|
1571
|
+
for (const relation2 of this.data.relations ?? []) {
|
|
1572
|
+
if (!slotIds.has(relation2.sourceSlotId)) {
|
|
1573
|
+
throw new Error(
|
|
1574
|
+
`[FlowBuilder] Relation references unknown source slot "${relation2.sourceSlotId}". Available slots: ${[...slotIds].join(", ")}`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
if (!slotIds.has(relation2.targetSlotId)) {
|
|
1578
|
+
throw new Error(
|
|
1579
|
+
`[FlowBuilder] Relation references unknown target slot "${relation2.targetSlotId}". Available slots: ${[...slotIds].join(", ")}`
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
if (relation2.sourceSlotId === relation2.targetSlotId) {
|
|
1583
|
+
throw new Error(
|
|
1584
|
+
`[FlowBuilder] Relation cannot link a slot to itself: "${relation2.sourceSlotId}"`
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* @internal Used by FlowPageBuilder to finalize a page
|
|
1591
|
+
*/
|
|
1592
|
+
_finalizePage(page) {
|
|
1593
|
+
if (page.id) {
|
|
1594
|
+
page.order = (this.data.pages?.length ?? 0) + 1;
|
|
1595
|
+
this.data.pages?.push(page);
|
|
1596
|
+
}
|
|
1597
|
+
return this;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Validate flow name format (kebab-case)
|
|
1601
|
+
*/
|
|
1602
|
+
validateName(name) {
|
|
1603
|
+
const flowNameSchema = import_zod.z.string().min(1, "Flow name cannot be empty").max(63, "Flow name is too long (max 63 characters)").regex(/^[a-z][a-z0-9-]*$/, {
|
|
1604
|
+
message: "Invalid flow name format.\nName must be in kebab-case:\n \u2705 Valid: 'couple-creation', 'new-contact', 'onboarding-flow'\n \u274C Invalid: 'CoupleCreation', 'new_contact', 'Onboarding Flow'"
|
|
1605
|
+
});
|
|
1606
|
+
try {
|
|
1607
|
+
flowNameSchema.parse(name);
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
if (error instanceof import_zod.z.ZodError) {
|
|
1610
|
+
throw new Error(`[FlowBuilder] ${error.issues[0].message}`);
|
|
1611
|
+
}
|
|
1612
|
+
throw error;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Validate that there are no circular dependencies in relations
|
|
1617
|
+
* Uses DFS to detect cycles in the dependency graph
|
|
1618
|
+
*/
|
|
1619
|
+
validateNoCircularDependencies() {
|
|
1620
|
+
const relations = this.data.relations ?? [];
|
|
1621
|
+
if (relations.length === 0) return;
|
|
1622
|
+
const dependsOn = /* @__PURE__ */ new Map();
|
|
1623
|
+
for (const slot of this.data.slots ?? []) {
|
|
1624
|
+
dependsOn.set(slot.id, /* @__PURE__ */ new Set());
|
|
1625
|
+
}
|
|
1626
|
+
for (const rel of relations) {
|
|
1627
|
+
dependsOn.get(rel.sourceSlotId)?.add(rel.targetSlotId);
|
|
1628
|
+
}
|
|
1629
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1630
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
1631
|
+
const hasCycle = (slotId) => {
|
|
1632
|
+
visited.add(slotId);
|
|
1633
|
+
recursionStack.add(slotId);
|
|
1634
|
+
for (const dependency of dependsOn.get(slotId) ?? []) {
|
|
1635
|
+
if (!visited.has(dependency)) {
|
|
1636
|
+
if (hasCycle(dependency)) return true;
|
|
1637
|
+
} else if (recursionStack.has(dependency)) {
|
|
1638
|
+
return true;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
recursionStack.delete(slotId);
|
|
1642
|
+
return false;
|
|
1643
|
+
};
|
|
1644
|
+
for (const slot of this.data.slots ?? []) {
|
|
1645
|
+
if (!visited.has(slot.id)) {
|
|
1646
|
+
if (hasCycle(slot.id)) {
|
|
1647
|
+
throw new Error(
|
|
1648
|
+
"[FlowBuilder] Circular dependency detected in relations. Slots cannot have circular dependencies."
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
function flow(name, label) {
|
|
1656
|
+
return new FlowBuilder(name, label);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1129
1659
|
// src/builders/object-builder.ts
|
|
1130
|
-
var
|
|
1660
|
+
var import_zod2 = __toESM(require("zod"));
|
|
1131
1661
|
var ObjectBuilder = class {
|
|
1132
1662
|
constructor(config) {
|
|
1133
1663
|
this.validateName(config.name);
|
|
@@ -1260,14 +1790,14 @@ The labelExpression defines how records are displayed in lists and relations.
|
|
|
1260
1790
|
if (name === void 0) {
|
|
1261
1791
|
return;
|
|
1262
1792
|
}
|
|
1263
|
-
const objectNameSchema =
|
|
1793
|
+
const objectNameSchema = import_zod2.default.string().min(1, "Object name cannot be empty").max(63, "Object name is too long (max 63 characters)").regex(/^[a-z][a-z0-9-]*$/, {
|
|
1264
1794
|
message: "Invalid object name format.\nName must be in kebab-case (slug format):\n \u2705 Valid: 'products', 'user-profiles', 'order-items'\n \u274C Invalid: 'Products', 'userProfiles', 'user_profiles'"
|
|
1265
1795
|
});
|
|
1266
1796
|
try {
|
|
1267
1797
|
objectNameSchema.parse(name);
|
|
1268
1798
|
} catch (error) {
|
|
1269
|
-
if (error instanceof
|
|
1270
|
-
throw new Error(`[ObjectBuilder] ${error.
|
|
1799
|
+
if (error instanceof import_zod2.default.ZodError) {
|
|
1800
|
+
throw new Error(`[ObjectBuilder] ${error.issues[0].message}`);
|
|
1271
1801
|
}
|
|
1272
1802
|
throw error;
|
|
1273
1803
|
}
|
|
@@ -1278,7 +1808,7 @@ function object(config) {
|
|
|
1278
1808
|
}
|
|
1279
1809
|
|
|
1280
1810
|
// src/builders/view-builder.ts
|
|
1281
|
-
var
|
|
1811
|
+
var import_zod3 = require("zod");
|
|
1282
1812
|
var GroupBuilder = class {
|
|
1283
1813
|
constructor(id, label) {
|
|
1284
1814
|
this.data = { fields: [] };
|
|
@@ -1634,14 +2164,14 @@ var ViewBuilder = class {
|
|
|
1634
2164
|
* Validate view name format (kebab-case)
|
|
1635
2165
|
*/
|
|
1636
2166
|
validateName(name) {
|
|
1637
|
-
const viewNameSchema =
|
|
2167
|
+
const viewNameSchema = import_zod3.z.string().min(1, "View name cannot be empty").max(63, "View name is too long (max 63 characters)").regex(/^[a-z][a-z0-9-]*$/, {
|
|
1638
2168
|
message: "Invalid view name format.\nName must be in kebab-case:\n \u2705 Valid: 'detail', 'list-view', 'company-detail'\n \u274C Invalid: 'Detail', 'listView', 'list_view'"
|
|
1639
2169
|
});
|
|
1640
2170
|
try {
|
|
1641
2171
|
viewNameSchema.parse(name);
|
|
1642
2172
|
} catch (error) {
|
|
1643
|
-
if (error instanceof
|
|
1644
|
-
throw new Error(`[ViewBuilder] ${error.
|
|
2173
|
+
if (error instanceof import_zod3.z.ZodError) {
|
|
2174
|
+
throw new Error(`[ViewBuilder] ${error.issues[0].message}`);
|
|
1645
2175
|
}
|
|
1646
2176
|
throw error;
|
|
1647
2177
|
}
|
|
@@ -1939,35 +2469,35 @@ Available views: ${availableNames}`
|
|
|
1939
2469
|
var viewRegistry = new ViewRegistry();
|
|
1940
2470
|
|
|
1941
2471
|
// src/validation/validators.ts
|
|
1942
|
-
var
|
|
1943
|
-
var baseConfigSchema =
|
|
1944
|
-
disabled:
|
|
1945
|
-
placeholder:
|
|
1946
|
-
description:
|
|
1947
|
-
defaultValue:
|
|
1948
|
-
icon:
|
|
1949
|
-
order:
|
|
1950
|
-
hidden:
|
|
1951
|
-
archived:
|
|
1952
|
-
deprecated:
|
|
1953
|
-
metadata:
|
|
2472
|
+
var import_zod4 = require("zod");
|
|
2473
|
+
var baseConfigSchema = import_zod4.z.object({
|
|
2474
|
+
disabled: import_zod4.z.boolean().optional(),
|
|
2475
|
+
placeholder: import_zod4.z.string().optional(),
|
|
2476
|
+
description: import_zod4.z.string().optional(),
|
|
2477
|
+
defaultValue: import_zod4.z.unknown().optional(),
|
|
2478
|
+
icon: import_zod4.z.string().optional(),
|
|
2479
|
+
order: import_zod4.z.number().int().optional(),
|
|
2480
|
+
hidden: import_zod4.z.boolean().optional(),
|
|
2481
|
+
archived: import_zod4.z.boolean().optional(),
|
|
2482
|
+
deprecated: import_zod4.z.boolean().optional(),
|
|
2483
|
+
metadata: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.unknown()).optional()
|
|
1954
2484
|
});
|
|
1955
|
-
var optionSchema =
|
|
1956
|
-
id:
|
|
1957
|
-
label:
|
|
1958
|
-
value:
|
|
1959
|
-
color:
|
|
1960
|
-
icon:
|
|
1961
|
-
description:
|
|
1962
|
-
group:
|
|
2485
|
+
var optionSchema = import_zod4.z.object({
|
|
2486
|
+
id: import_zod4.z.string().min(1),
|
|
2487
|
+
label: import_zod4.z.string().min(1),
|
|
2488
|
+
value: import_zod4.z.string().min(1),
|
|
2489
|
+
color: import_zod4.z.string().optional(),
|
|
2490
|
+
icon: import_zod4.z.string().optional(),
|
|
2491
|
+
description: import_zod4.z.string().optional(),
|
|
2492
|
+
group: import_zod4.z.enum(["idle", "in_progress", "finished"]).optional()
|
|
1963
2493
|
});
|
|
1964
|
-
var relationTargetSchema =
|
|
1965
|
-
object:
|
|
1966
|
-
displayTemplate:
|
|
1967
|
-
filter:
|
|
2494
|
+
var relationTargetSchema = import_zod4.z.object({
|
|
2495
|
+
object: import_zod4.z.string().min(1),
|
|
2496
|
+
displayTemplate: import_zod4.z.string().optional(),
|
|
2497
|
+
filter: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.unknown()).optional()
|
|
1968
2498
|
});
|
|
1969
|
-
var documentTypeConfigSchema =
|
|
1970
|
-
type:
|
|
2499
|
+
var documentTypeConfigSchema = import_zod4.z.object({
|
|
2500
|
+
type: import_zod4.z.enum([
|
|
1971
2501
|
"id_card",
|
|
1972
2502
|
"passport",
|
|
1973
2503
|
"incorporation_certificate",
|
|
@@ -1978,87 +2508,86 @@ var documentTypeConfigSchema = import_zod3.z.object({
|
|
|
1978
2508
|
"proof_of_address",
|
|
1979
2509
|
"custom"
|
|
1980
2510
|
]),
|
|
1981
|
-
label:
|
|
1982
|
-
faces:
|
|
1983
|
-
attributeMapping:
|
|
1984
|
-
|
|
1985
|
-
attributeId:
|
|
1986
|
-
extractedKey:
|
|
1987
|
-
face:
|
|
1988
|
-
required:
|
|
2511
|
+
label: import_zod4.z.string(),
|
|
2512
|
+
faces: import_zod4.z.array(import_zod4.z.enum(["front", "back", "single"])),
|
|
2513
|
+
attributeMapping: import_zod4.z.array(
|
|
2514
|
+
import_zod4.z.object({
|
|
2515
|
+
attributeId: import_zod4.z.string(),
|
|
2516
|
+
extractedKey: import_zod4.z.string(),
|
|
2517
|
+
face: import_zod4.z.enum(["front", "back", "single"]).optional(),
|
|
2518
|
+
required: import_zod4.z.boolean().optional()
|
|
1989
2519
|
})
|
|
1990
2520
|
).optional()
|
|
1991
2521
|
});
|
|
1992
|
-
var fileVerificationConfigSchema =
|
|
1993
|
-
enabled:
|
|
1994
|
-
documentTypes:
|
|
1995
|
-
autoExtract:
|
|
1996
|
-
autoValidate:
|
|
2522
|
+
var fileVerificationConfigSchema = import_zod4.z.object({
|
|
2523
|
+
enabled: import_zod4.z.boolean(),
|
|
2524
|
+
documentTypes: import_zod4.z.array(documentTypeConfigSchema),
|
|
2525
|
+
autoExtract: import_zod4.z.boolean().optional(),
|
|
2526
|
+
autoValidate: import_zod4.z.boolean().optional()
|
|
1997
2527
|
});
|
|
1998
2528
|
var textConfigSchema = baseConfigSchema.extend({
|
|
1999
|
-
minLength:
|
|
2000
|
-
maxLength:
|
|
2001
|
-
pattern:
|
|
2529
|
+
minLength: import_zod4.z.number().int().min(0).optional(),
|
|
2530
|
+
maxLength: import_zod4.z.number().int().min(1).optional(),
|
|
2531
|
+
pattern: import_zod4.z.string().optional()
|
|
2002
2532
|
});
|
|
2003
2533
|
var textareaConfigSchema = baseConfigSchema;
|
|
2004
2534
|
var numberConfigSchema = baseConfigSchema.extend({
|
|
2005
|
-
min:
|
|
2006
|
-
max:
|
|
2007
|
-
unit:
|
|
2008
|
-
decimals:
|
|
2535
|
+
min: import_zod4.z.number().optional(),
|
|
2536
|
+
max: import_zod4.z.number().optional(),
|
|
2537
|
+
unit: import_zod4.z.enum(["integer", "decimal", "percentage"]).optional(),
|
|
2538
|
+
decimals: import_zod4.z.number().int().min(0).optional()
|
|
2009
2539
|
});
|
|
2010
2540
|
var checkboxConfigSchema = baseConfigSchema;
|
|
2011
2541
|
var dateConfigSchema = baseConfigSchema.extend({
|
|
2012
|
-
dateFormat:
|
|
2013
|
-
minDate:
|
|
2014
|
-
maxDate:
|
|
2542
|
+
dateFormat: import_zod4.z.enum(["short", "long", "full", "relative"]).optional(),
|
|
2543
|
+
minDate: import_zod4.z.string().optional(),
|
|
2544
|
+
maxDate: import_zod4.z.string().optional()
|
|
2015
2545
|
});
|
|
2016
2546
|
var phoneConfigSchema = baseConfigSchema.extend({
|
|
2017
|
-
defaultCountryCode:
|
|
2547
|
+
defaultCountryCode: import_zod4.z.string().length(3).optional()
|
|
2018
2548
|
});
|
|
2019
2549
|
var currencyConfigSchema = baseConfigSchema.extend({
|
|
2020
|
-
defaultCurrency:
|
|
2021
|
-
allowedCurrencies:
|
|
2550
|
+
defaultCurrency: import_zod4.z.string().length(3).optional(),
|
|
2551
|
+
allowedCurrencies: import_zod4.z.array(import_zod4.z.string().length(3)).optional()
|
|
2022
2552
|
});
|
|
2023
2553
|
var statusConfigSchema = baseConfigSchema.extend({
|
|
2024
|
-
options:
|
|
2554
|
+
options: import_zod4.z.array(optionSchema).min(1)
|
|
2025
2555
|
});
|
|
2026
2556
|
var locationConfigSchema = baseConfigSchema.extend({
|
|
2027
|
-
granularity:
|
|
2028
|
-
enableAutocomplete:
|
|
2029
|
-
enableMap:
|
|
2030
|
-
defaultCountry:
|
|
2031
|
-
allowedCountries:
|
|
2032
|
-
displayFormat:
|
|
2557
|
+
granularity: import_zod4.z.enum(["full", "address", "city", "state", "country", "coordinates"]),
|
|
2558
|
+
enableAutocomplete: import_zod4.z.boolean().optional(),
|
|
2559
|
+
enableMap: import_zod4.z.boolean().optional(),
|
|
2560
|
+
defaultCountry: import_zod4.z.string().length(3).optional(),
|
|
2561
|
+
allowedCountries: import_zod4.z.array(import_zod4.z.string().length(3)).optional(),
|
|
2562
|
+
displayFormat: import_zod4.z.enum(["single_line", "multi_line", "compact"]).optional()
|
|
2033
2563
|
});
|
|
2034
2564
|
var timestampConfigSchema = baseConfigSchema.extend({
|
|
2035
|
-
autoUpdate:
|
|
2565
|
+
autoUpdate: import_zod4.z.boolean().optional()
|
|
2036
2566
|
});
|
|
2037
2567
|
var selectConfigSchema = baseConfigSchema.extend({
|
|
2038
|
-
options:
|
|
2568
|
+
options: import_zod4.z.array(optionSchema).min(1)
|
|
2039
2569
|
});
|
|
2040
2570
|
var multiselectConfigSchema = baseConfigSchema.extend({
|
|
2041
|
-
options:
|
|
2571
|
+
options: import_zod4.z.array(optionSchema).min(1)
|
|
2042
2572
|
});
|
|
2043
2573
|
var fileConfigSchema = baseConfigSchema.extend({
|
|
2044
|
-
maxFiles:
|
|
2045
|
-
maxSize:
|
|
2046
|
-
allowedTypes:
|
|
2574
|
+
maxFiles: import_zod4.z.number().int().min(1).optional(),
|
|
2575
|
+
maxSize: import_zod4.z.number().int().min(1).optional(),
|
|
2576
|
+
allowedTypes: import_zod4.z.array(import_zod4.z.string()).optional(),
|
|
2047
2577
|
verification: fileVerificationConfigSchema.optional()
|
|
2048
2578
|
});
|
|
2049
2579
|
var userConfigSchema = baseConfigSchema.extend({
|
|
2050
|
-
allowedRoles:
|
|
2580
|
+
allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional()
|
|
2051
2581
|
});
|
|
2052
2582
|
var relationConfigSchema = baseConfigSchema.extend({
|
|
2053
|
-
targets:
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
maxItems: import_zod3.z.number().int().min(1).optional()
|
|
2583
|
+
targets: import_zod4.z.array(relationTargetSchema).min(1),
|
|
2584
|
+
cardinality: import_zod4.z.enum(["one", "many"]),
|
|
2585
|
+
minItems: import_zod4.z.number().int().min(0).optional(),
|
|
2586
|
+
maxItems: import_zod4.z.number().int().min(1).optional()
|
|
2058
2587
|
});
|
|
2059
2588
|
var ratingConfigSchema = baseConfigSchema.extend({
|
|
2060
|
-
max:
|
|
2061
|
-
iconType:
|
|
2589
|
+
max: import_zod4.z.number().int().min(1).optional(),
|
|
2590
|
+
iconType: import_zod4.z.enum(["star", "heart", "thumbs", "number"]).optional()
|
|
2062
2591
|
});
|
|
2063
2592
|
var attributeConfigSchemas = {
|
|
2064
2593
|
text: textConfigSchema,
|
|
@@ -2089,7 +2618,7 @@ function validateAttributeConfig(type, config) {
|
|
|
2089
2618
|
}
|
|
2090
2619
|
return {
|
|
2091
2620
|
success: false,
|
|
2092
|
-
errors: result.error.
|
|
2621
|
+
errors: result.error.issues.map((err) => `${err.path.join(".")}: ${err.message}`)
|
|
2093
2622
|
};
|
|
2094
2623
|
}
|
|
2095
2624
|
function parseAttributeConfig(type, config) {
|
|
@@ -2102,7 +2631,7 @@ function safeParseAttributeConfig(type, config) {
|
|
|
2102
2631
|
return result.success ? result.data : void 0;
|
|
2103
2632
|
}
|
|
2104
2633
|
function createTextValidator(attr) {
|
|
2105
|
-
let schema =
|
|
2634
|
+
let schema = import_zod4.z.string();
|
|
2106
2635
|
if (attr.minLength !== void 0) {
|
|
2107
2636
|
schema = schema.min(
|
|
2108
2637
|
attr.minLength,
|
|
@@ -2121,7 +2650,7 @@ function createTextValidator(attr) {
|
|
|
2121
2650
|
return schema;
|
|
2122
2651
|
}
|
|
2123
2652
|
function createNumberValidator(attr) {
|
|
2124
|
-
let schema =
|
|
2653
|
+
let schema = import_zod4.z.number();
|
|
2125
2654
|
if (attr.min !== void 0) {
|
|
2126
2655
|
schema = schema.min(attr.min, `${attr.label} must be at least ${attr.min}`);
|
|
2127
2656
|
}
|
|
@@ -2134,81 +2663,75 @@ function createNumberValidator(attr) {
|
|
|
2134
2663
|
return schema;
|
|
2135
2664
|
}
|
|
2136
2665
|
function createCheckboxValidator(_attr) {
|
|
2137
|
-
return
|
|
2666
|
+
return import_zod4.z.boolean();
|
|
2138
2667
|
}
|
|
2139
2668
|
function createDateValidator(attr) {
|
|
2140
|
-
return
|
|
2669
|
+
return import_zod4.z.string().datetime({ message: `${attr.label} must be a valid ISO date` });
|
|
2141
2670
|
}
|
|
2142
2671
|
function createPhoneValidator(_attr) {
|
|
2143
|
-
return
|
|
2144
|
-
countryCode:
|
|
2145
|
-
phoneNumber:
|
|
2672
|
+
return import_zod4.z.object({
|
|
2673
|
+
countryCode: import_zod4.z.string().length(3),
|
|
2674
|
+
phoneNumber: import_zod4.z.string().min(1)
|
|
2146
2675
|
});
|
|
2147
2676
|
}
|
|
2148
2677
|
function createCurrencyValidator(_attr) {
|
|
2149
|
-
return
|
|
2150
|
-
code:
|
|
2151
|
-
value:
|
|
2678
|
+
return import_zod4.z.object({
|
|
2679
|
+
code: import_zod4.z.string().length(3),
|
|
2680
|
+
value: import_zod4.z.number().min(0)
|
|
2152
2681
|
});
|
|
2153
2682
|
}
|
|
2154
2683
|
function createStatusValidator(attr) {
|
|
2155
2684
|
const validValues = attr.options.map((opt) => opt.value);
|
|
2156
|
-
return
|
|
2157
|
-
|
|
2158
|
-
message: `${attr.label} must be one of: ${validValues.join(", ")}`
|
|
2159
|
-
})
|
|
2685
|
+
return import_zod4.z.enum(validValues, {
|
|
2686
|
+
error: `${attr.label} must be one of: ${validValues.join(", ")}`
|
|
2160
2687
|
});
|
|
2161
2688
|
}
|
|
2162
2689
|
function createSelectValidator(attr) {
|
|
2163
2690
|
const validValues = attr.options.map((opt) => opt.value);
|
|
2164
|
-
return
|
|
2165
|
-
|
|
2166
|
-
message: `${attr.label} must be one of: ${validValues.join(", ")}`
|
|
2167
|
-
})
|
|
2691
|
+
return import_zod4.z.enum(validValues, {
|
|
2692
|
+
error: `${attr.label} must be one of: ${validValues.join(", ")}`
|
|
2168
2693
|
});
|
|
2169
2694
|
}
|
|
2170
2695
|
function createMultiselectValidator(attr) {
|
|
2171
2696
|
const validValues = attr.options.map((opt) => opt.value);
|
|
2172
|
-
return
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
message: `Each value must be one of: ${validValues.join(", ")}`
|
|
2176
|
-
})
|
|
2697
|
+
return import_zod4.z.array(
|
|
2698
|
+
import_zod4.z.enum(validValues, {
|
|
2699
|
+
error: `Each value must be one of: ${validValues.join(", ")}`
|
|
2177
2700
|
})
|
|
2178
2701
|
);
|
|
2179
2702
|
}
|
|
2180
2703
|
function createLocationValidator(_attr) {
|
|
2181
|
-
return
|
|
2182
|
-
address:
|
|
2183
|
-
address2:
|
|
2184
|
-
city:
|
|
2185
|
-
state:
|
|
2186
|
-
postalCode:
|
|
2187
|
-
country:
|
|
2188
|
-
latitude:
|
|
2189
|
-
longitude:
|
|
2704
|
+
return import_zod4.z.object({
|
|
2705
|
+
address: import_zod4.z.string().optional(),
|
|
2706
|
+
address2: import_zod4.z.string().optional(),
|
|
2707
|
+
city: import_zod4.z.string().optional(),
|
|
2708
|
+
state: import_zod4.z.string().optional(),
|
|
2709
|
+
postalCode: import_zod4.z.string().optional(),
|
|
2710
|
+
country: import_zod4.z.string().length(3).optional(),
|
|
2711
|
+
latitude: import_zod4.z.number().optional(),
|
|
2712
|
+
longitude: import_zod4.z.number().optional()
|
|
2190
2713
|
});
|
|
2191
2714
|
}
|
|
2192
2715
|
function createTimestampValidator(_attr) {
|
|
2193
|
-
return
|
|
2716
|
+
return import_zod4.z.number().int().positive();
|
|
2194
2717
|
}
|
|
2195
2718
|
function createFileValidator(_attr) {
|
|
2196
|
-
return
|
|
2719
|
+
return import_zod4.z.string().uuid();
|
|
2197
2720
|
}
|
|
2198
2721
|
function createUserValidator(_attr) {
|
|
2199
|
-
return
|
|
2722
|
+
return import_zod4.z.string().uuid();
|
|
2200
2723
|
}
|
|
2201
2724
|
function createSingleRelationValidator(attr) {
|
|
2202
|
-
const uuidSchema =
|
|
2725
|
+
const uuidSchema = import_zod4.z.string().uuid({
|
|
2203
2726
|
message: `${attr.label} must be a valid record ID`
|
|
2204
2727
|
});
|
|
2205
|
-
return
|
|
2728
|
+
return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
|
|
2206
2729
|
}
|
|
2207
2730
|
function createMultiRelationValidator(attr) {
|
|
2208
|
-
const uuidSchema =
|
|
2731
|
+
const uuidSchema = import_zod4.z.string().uuid({
|
|
2209
2732
|
message: `Each ${attr.label} item must be a valid record ID`
|
|
2210
2733
|
});
|
|
2211
|
-
let arraySchema =
|
|
2734
|
+
let arraySchema = import_zod4.z.array(uuidSchema);
|
|
2212
2735
|
if (attr.minItems !== void 0) {
|
|
2213
2736
|
arraySchema = arraySchema.min(
|
|
2214
2737
|
attr.minItems,
|
|
@@ -2230,7 +2753,7 @@ function createRelationValidator(attr) {
|
|
|
2230
2753
|
return createSingleRelationValidator(attr);
|
|
2231
2754
|
}
|
|
2232
2755
|
function createRatingValidator(attr) {
|
|
2233
|
-
let schema =
|
|
2756
|
+
let schema = import_zod4.z.number().int().min(0);
|
|
2234
2757
|
if (attr.max !== void 0) {
|
|
2235
2758
|
schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
|
|
2236
2759
|
}
|
|
@@ -2269,7 +2792,7 @@ function createAttributeValidator(attr) {
|
|
|
2269
2792
|
case "rating":
|
|
2270
2793
|
return createRatingValidator(attr);
|
|
2271
2794
|
default:
|
|
2272
|
-
return
|
|
2795
|
+
return import_zod4.z.unknown();
|
|
2273
2796
|
}
|
|
2274
2797
|
}
|
|
2275
2798
|
function createObjectValidator(objectDef) {
|
|
@@ -2281,7 +2804,7 @@ function createObjectValidator(objectDef) {
|
|
|
2281
2804
|
}
|
|
2282
2805
|
shape[attr.name] = validator;
|
|
2283
2806
|
}
|
|
2284
|
-
return
|
|
2807
|
+
return import_zod4.z.object(shape);
|
|
2285
2808
|
}
|
|
2286
2809
|
function validateAttribute(attr, value) {
|
|
2287
2810
|
const validator = createAttributeValidator(attr);
|
|
@@ -2297,7 +2820,7 @@ function validateAttribute(attr, value) {
|
|
|
2297
2820
|
}
|
|
2298
2821
|
return {
|
|
2299
2822
|
success: false,
|
|
2300
|
-
errors: result.error.
|
|
2823
|
+
errors: result.error.issues.map((err) => ({
|
|
2301
2824
|
path: [attr.name, ...err.path.map(String)],
|
|
2302
2825
|
message: err.message
|
|
2303
2826
|
}))
|
|
@@ -2314,7 +2837,7 @@ function validateObject(objectDef, data) {
|
|
|
2314
2837
|
}
|
|
2315
2838
|
return {
|
|
2316
2839
|
success: false,
|
|
2317
|
-
errors: result.error.
|
|
2840
|
+
errors: result.error.issues.map((err) => ({
|
|
2318
2841
|
path: err.path.map(String),
|
|
2319
2842
|
message: err.message
|
|
2320
2843
|
}))
|
|
@@ -2335,7 +2858,7 @@ function createDraftValidator(objectDef) {
|
|
|
2335
2858
|
const validator = createAttributeValidator(attr).optional();
|
|
2336
2859
|
shape[attr.name] = validator;
|
|
2337
2860
|
}
|
|
2338
|
-
return
|
|
2861
|
+
return import_zod4.z.object(shape);
|
|
2339
2862
|
}
|
|
2340
2863
|
function validateDraft(objectDef, data) {
|
|
2341
2864
|
const validator = createDraftValidator(objectDef);
|
|
@@ -2348,7 +2871,7 @@ function validateDraft(objectDef, data) {
|
|
|
2348
2871
|
}
|
|
2349
2872
|
return {
|
|
2350
2873
|
success: false,
|
|
2351
|
-
errors: result.error.
|
|
2874
|
+
errors: result.error.issues.map((err) => ({
|
|
2352
2875
|
path: err.path.map(String),
|
|
2353
2876
|
message: err.message
|
|
2354
2877
|
}))
|
|
@@ -2701,6 +3224,12 @@ function createMockUserProfilesRepository(stores) {
|
|
|
2701
3224
|
}
|
|
2702
3225
|
return Promise.resolve(results);
|
|
2703
3226
|
},
|
|
3227
|
+
countByRole(tenantId, role) {
|
|
3228
|
+
const count = Array.from(stores.userProfiles.values()).filter(
|
|
3229
|
+
(profile) => profile.tenantId === tenantId && profile.role === role
|
|
3230
|
+
).length;
|
|
3231
|
+
return Promise.resolve(count);
|
|
3232
|
+
},
|
|
2704
3233
|
updateLastLogin(id) {
|
|
2705
3234
|
const profile = stores.userProfiles.get(id);
|
|
2706
3235
|
if (profile) {
|
|
@@ -2794,12 +3323,36 @@ function createMockFilesRepository(stores) {
|
|
|
2794
3323
|
}
|
|
2795
3324
|
function createMockObjectRecordsRepository(stores) {
|
|
2796
3325
|
return {
|
|
3326
|
+
hardDelete(id) {
|
|
3327
|
+
stores.objectRecords.delete(id);
|
|
3328
|
+
return Promise.resolve();
|
|
3329
|
+
},
|
|
3330
|
+
restore(id) {
|
|
3331
|
+
const existing = stores.objectRecords.get(id);
|
|
3332
|
+
if (!existing) {
|
|
3333
|
+
return Promise.reject(new Error(`ObjectRecord ${id} not found`));
|
|
3334
|
+
}
|
|
3335
|
+
existing.deletedAt = null;
|
|
3336
|
+
stores.objectRecords.set(id, existing);
|
|
3337
|
+
return Promise.resolve(existing);
|
|
3338
|
+
},
|
|
2797
3339
|
findById(id) {
|
|
2798
3340
|
const internal = stores.objectRecords.get(id);
|
|
2799
3341
|
if (!internal) return Promise.resolve(null);
|
|
2800
3342
|
const { tenantId: _, ...record } = internal;
|
|
2801
3343
|
return Promise.resolve(record);
|
|
2802
3344
|
},
|
|
3345
|
+
findByIds(ids) {
|
|
3346
|
+
const results = [];
|
|
3347
|
+
for (const id of ids) {
|
|
3348
|
+
const internal = stores.objectRecords.get(id);
|
|
3349
|
+
if (internal) {
|
|
3350
|
+
const { tenantId: _, ...record } = internal;
|
|
3351
|
+
results.push(record);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
return Promise.resolve(results);
|
|
3355
|
+
},
|
|
2803
3356
|
create(data) {
|
|
2804
3357
|
const internal = {
|
|
2805
3358
|
id: generateId(),
|
|
@@ -2912,26 +3465,105 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
2912
3465
|
};
|
|
2913
3466
|
});
|
|
2914
3467
|
return Promise.resolve({ results, total });
|
|
2915
|
-
}
|
|
2916
|
-
};
|
|
2917
|
-
}
|
|
2918
|
-
function createMockViewsRepository(stores) {
|
|
2919
|
-
return {
|
|
2920
|
-
findById(id) {
|
|
2921
|
-
return Promise.resolve(stores.views.get(id) ?? null);
|
|
2922
|
-
},
|
|
2923
|
-
findByName(tenantId, objectName, viewName) {
|
|
2924
|
-
return Promise.resolve(
|
|
2925
|
-
Array.from(stores.views.values()).find(
|
|
2926
|
-
(v) => v.tenantId === tenantId && v.objectName === objectName && v.name === viewName
|
|
2927
|
-
) ?? null
|
|
2928
|
-
);
|
|
2929
3468
|
},
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
3469
|
+
// -------------------------------------------------------------------------
|
|
3470
|
+
// Schema Integrity Methods
|
|
3471
|
+
// -------------------------------------------------------------------------
|
|
3472
|
+
countRecordsReferencingId(targetId) {
|
|
3473
|
+
const relationAttrs = [];
|
|
3474
|
+
for (const attr of stores.attributes.values()) {
|
|
3475
|
+
if (attr.type === "relation") {
|
|
3476
|
+
relationAttrs.push({ objectId: attr.objectId, attrName: attr.name });
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
const countsByObjectId = /* @__PURE__ */ new Map();
|
|
3480
|
+
for (const record of stores.objectRecords.values()) {
|
|
3481
|
+
for (const { objectId, attrName } of relationAttrs) {
|
|
3482
|
+
if (record.objectId !== objectId) continue;
|
|
3483
|
+
const value = record.values[attrName];
|
|
3484
|
+
let isReferencing = false;
|
|
3485
|
+
if (typeof value === "string" && value === targetId) {
|
|
3486
|
+
isReferencing = true;
|
|
3487
|
+
} else if (Array.isArray(value) && value.includes(targetId)) {
|
|
3488
|
+
isReferencing = true;
|
|
3489
|
+
}
|
|
3490
|
+
if (isReferencing) {
|
|
3491
|
+
const current = countsByObjectId.get(objectId) ?? 0;
|
|
3492
|
+
countsByObjectId.set(objectId, current + 1);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
const results = [];
|
|
3497
|
+
for (const [objectId, count] of countsByObjectId) {
|
|
3498
|
+
const obj = stores.objects.get(objectId);
|
|
3499
|
+
if (obj) {
|
|
3500
|
+
results.push({
|
|
3501
|
+
objectName: obj.name,
|
|
3502
|
+
objectLabel: obj.label,
|
|
3503
|
+
count
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
return Promise.resolve(results);
|
|
3508
|
+
},
|
|
3509
|
+
removeAttributeData(objectId, attributeName) {
|
|
3510
|
+
let updated = 0;
|
|
3511
|
+
for (const record of stores.objectRecords.values()) {
|
|
3512
|
+
if (record.objectId === objectId && attributeName in record.values) {
|
|
3513
|
+
delete record.values[attributeName];
|
|
3514
|
+
record.updatedAt = /* @__PURE__ */ new Date();
|
|
3515
|
+
updated++;
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
return Promise.resolve(updated);
|
|
3519
|
+
},
|
|
3520
|
+
batchRefreshLabels(objectId, computeLabel) {
|
|
3521
|
+
let updated = 0;
|
|
3522
|
+
for (const record of stores.objectRecords.values()) {
|
|
3523
|
+
if (record.objectId === objectId) {
|
|
3524
|
+
const newLabel = computeLabel(record.values);
|
|
3525
|
+
if (newLabel !== record.label) {
|
|
3526
|
+
record.label = newLabel;
|
|
3527
|
+
record.updatedAt = /* @__PURE__ */ new Date();
|
|
3528
|
+
updated++;
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
return Promise.resolve(updated);
|
|
3533
|
+
},
|
|
3534
|
+
batchRefreshStatus(objectId, computeStatus) {
|
|
3535
|
+
let updated = 0;
|
|
3536
|
+
for (const record of stores.objectRecords.values()) {
|
|
3537
|
+
if (record.objectId === objectId) {
|
|
3538
|
+
const newStatus = computeStatus(record.values);
|
|
3539
|
+
if (newStatus !== record.completionStatus) {
|
|
3540
|
+
record.completionStatus = newStatus;
|
|
3541
|
+
record.updatedAt = /* @__PURE__ */ new Date();
|
|
3542
|
+
updated++;
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return Promise.resolve(updated);
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
function createMockViewsRepository(stores) {
|
|
3551
|
+
return {
|
|
3552
|
+
findById(id) {
|
|
3553
|
+
return Promise.resolve(stores.views.get(id) ?? null);
|
|
3554
|
+
},
|
|
3555
|
+
findByName(tenantId, objectName, viewName) {
|
|
3556
|
+
return Promise.resolve(
|
|
3557
|
+
Array.from(stores.views.values()).find(
|
|
3558
|
+
(v) => v.tenantId === tenantId && v.objectName === objectName && v.name === viewName
|
|
3559
|
+
) ?? null
|
|
3560
|
+
);
|
|
3561
|
+
},
|
|
3562
|
+
findByObjectName(tenantId, objectName) {
|
|
3563
|
+
return Promise.resolve(
|
|
3564
|
+
Array.from(stores.views.values()).filter(
|
|
3565
|
+
(v) => v.tenantId === tenantId && v.objectName === objectName
|
|
3566
|
+
)
|
|
2935
3567
|
);
|
|
2936
3568
|
},
|
|
2937
3569
|
findAllForTenant(tenantId) {
|
|
@@ -3015,6 +3647,157 @@ function createMockViewsRepository(stores) {
|
|
|
3015
3647
|
}
|
|
3016
3648
|
};
|
|
3017
3649
|
}
|
|
3650
|
+
function createMockPermissionsRepository(stores) {
|
|
3651
|
+
return {
|
|
3652
|
+
getRolesByTenant(tenantId) {
|
|
3653
|
+
const results = Array.from(stores.roles.values()).filter((r) => r.tenantId === tenantId);
|
|
3654
|
+
return Promise.resolve(results);
|
|
3655
|
+
},
|
|
3656
|
+
getRoleById(roleId) {
|
|
3657
|
+
return Promise.resolve(stores.roles.get(roleId) ?? null);
|
|
3658
|
+
},
|
|
3659
|
+
getRoleByName(tenantId, name) {
|
|
3660
|
+
for (const role of stores.roles.values()) {
|
|
3661
|
+
if (role.tenantId === tenantId && role.name === name) {
|
|
3662
|
+
return Promise.resolve(role);
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
return Promise.resolve(null);
|
|
3666
|
+
},
|
|
3667
|
+
createRole(input) {
|
|
3668
|
+
const role = {
|
|
3669
|
+
id: generateId(),
|
|
3670
|
+
tenantId: input.tenantId,
|
|
3671
|
+
name: input.name,
|
|
3672
|
+
label: input.label,
|
|
3673
|
+
description: input.description,
|
|
3674
|
+
system: false,
|
|
3675
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
3676
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3677
|
+
};
|
|
3678
|
+
stores.roles.set(role.id, role);
|
|
3679
|
+
return Promise.resolve(role);
|
|
3680
|
+
},
|
|
3681
|
+
updateRole(roleId, updates) {
|
|
3682
|
+
const existing = stores.roles.get(roleId);
|
|
3683
|
+
if (!existing) {
|
|
3684
|
+
return Promise.reject(new Error(`Role ${roleId} not found`));
|
|
3685
|
+
}
|
|
3686
|
+
const updated = {
|
|
3687
|
+
...existing,
|
|
3688
|
+
...updates,
|
|
3689
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3690
|
+
};
|
|
3691
|
+
stores.roles.set(roleId, updated);
|
|
3692
|
+
return Promise.resolve(updated);
|
|
3693
|
+
},
|
|
3694
|
+
deleteRole(roleId) {
|
|
3695
|
+
const role = stores.roles.get(roleId);
|
|
3696
|
+
if (role?.system) {
|
|
3697
|
+
return Promise.reject(new Error(`Cannot delete system role ${roleId}`));
|
|
3698
|
+
}
|
|
3699
|
+
stores.roles.delete(roleId);
|
|
3700
|
+
for (const [id, perm] of stores.permissions.entries()) {
|
|
3701
|
+
if (perm.roleId === roleId) {
|
|
3702
|
+
stores.permissions.delete(id);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
for (const [id, ur] of stores.userRoles.entries()) {
|
|
3706
|
+
if (ur.roleId === roleId) {
|
|
3707
|
+
stores.userRoles.delete(id);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
return Promise.resolve();
|
|
3711
|
+
},
|
|
3712
|
+
getPermissionsByRole(roleId) {
|
|
3713
|
+
const results = Array.from(stores.permissions.values()).filter((p) => p.roleId === roleId);
|
|
3714
|
+
return Promise.resolve(results);
|
|
3715
|
+
},
|
|
3716
|
+
setPermissions(roleId, permissions) {
|
|
3717
|
+
for (const [id, perm] of stores.permissions.entries()) {
|
|
3718
|
+
if (perm.roleId === roleId) {
|
|
3719
|
+
stores.permissions.delete(id);
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
for (const input of permissions) {
|
|
3723
|
+
const perm = {
|
|
3724
|
+
id: generateId(),
|
|
3725
|
+
roleId,
|
|
3726
|
+
scope: input.scope,
|
|
3727
|
+
target: input.target,
|
|
3728
|
+
actions: input.actions,
|
|
3729
|
+
filter: input.filter,
|
|
3730
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
3731
|
+
};
|
|
3732
|
+
stores.permissions.set(perm.id, perm);
|
|
3733
|
+
}
|
|
3734
|
+
return Promise.resolve();
|
|
3735
|
+
},
|
|
3736
|
+
getUserRoles(userProfileId, tenantId) {
|
|
3737
|
+
const roleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === userProfileId && ur.tenantId === tenantId).map((ur) => ur.roleId);
|
|
3738
|
+
const roles = Array.from(stores.roles.values()).filter((r) => roleIds.includes(r.id));
|
|
3739
|
+
return Promise.resolve(roles);
|
|
3740
|
+
},
|
|
3741
|
+
assignRole(input) {
|
|
3742
|
+
for (const ur of stores.userRoles.values()) {
|
|
3743
|
+
if (ur.userProfileId === input.userProfileId && ur.roleId === input.roleId && ur.tenantId === input.tenantId) {
|
|
3744
|
+
return Promise.resolve(ur);
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
const userRole = {
|
|
3748
|
+
id: generateId(),
|
|
3749
|
+
userProfileId: input.userProfileId,
|
|
3750
|
+
roleId: input.roleId,
|
|
3751
|
+
tenantId: input.tenantId,
|
|
3752
|
+
assignedAt: /* @__PURE__ */ new Date(),
|
|
3753
|
+
assignedBy: input.assignedBy
|
|
3754
|
+
};
|
|
3755
|
+
stores.userRoles.set(userRole.id, userRole);
|
|
3756
|
+
return Promise.resolve(userRole);
|
|
3757
|
+
},
|
|
3758
|
+
revokeRole(userProfileId, roleId, tenantId) {
|
|
3759
|
+
for (const [id, ur] of stores.userRoles.entries()) {
|
|
3760
|
+
if (ur.userProfileId === userProfileId && ur.roleId === roleId && ur.tenantId === tenantId) {
|
|
3761
|
+
stores.userRoles.delete(id);
|
|
3762
|
+
break;
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
return Promise.resolve();
|
|
3766
|
+
},
|
|
3767
|
+
getEffectivePermissions(userProfileId, tenantId) {
|
|
3768
|
+
const userRoleIds = Array.from(stores.userRoles.values()).filter((ur) => ur.userProfileId === userProfileId && ur.tenantId === tenantId).map((ur) => ur.roleId);
|
|
3769
|
+
const userRoles = Array.from(stores.roles.values()).filter((r) => userRoleIds.includes(r.id));
|
|
3770
|
+
const isAdmin = userRoles.some((r) => r.name === "admin");
|
|
3771
|
+
const userPermissions = Array.from(stores.permissions.values()).filter(
|
|
3772
|
+
(p) => userRoleIds.includes(p.roleId)
|
|
3773
|
+
);
|
|
3774
|
+
const objectPermissions = {};
|
|
3775
|
+
const systemPermissions = {};
|
|
3776
|
+
for (const perm of userPermissions) {
|
|
3777
|
+
if (perm.scope === "object") {
|
|
3778
|
+
if (!objectPermissions[perm.target]) {
|
|
3779
|
+
objectPermissions[perm.target] = [];
|
|
3780
|
+
}
|
|
3781
|
+
for (const action of perm.actions) {
|
|
3782
|
+
if (!objectPermissions[perm.target].includes(action)) {
|
|
3783
|
+
objectPermissions[perm.target].push(action);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
} else if (perm.scope === "system") {
|
|
3787
|
+
if (!systemPermissions[perm.target]) {
|
|
3788
|
+
systemPermissions[perm.target] = [];
|
|
3789
|
+
}
|
|
3790
|
+
for (const action of perm.actions) {
|
|
3791
|
+
if (!systemPermissions[perm.target].includes(action)) {
|
|
3792
|
+
systemPermissions[perm.target].push(action);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
return Promise.resolve({ isAdmin, objectPermissions, systemPermissions });
|
|
3798
|
+
}
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3018
3801
|
function createMockAdapter() {
|
|
3019
3802
|
const stores = {
|
|
3020
3803
|
objects: /* @__PURE__ */ new Map(),
|
|
@@ -3022,7 +3805,10 @@ function createMockAdapter() {
|
|
|
3022
3805
|
userProfiles: /* @__PURE__ */ new Map(),
|
|
3023
3806
|
files: /* @__PURE__ */ new Map(),
|
|
3024
3807
|
objectRecords: /* @__PURE__ */ new Map(),
|
|
3025
|
-
views: /* @__PURE__ */ new Map()
|
|
3808
|
+
views: /* @__PURE__ */ new Map(),
|
|
3809
|
+
roles: /* @__PURE__ */ new Map(),
|
|
3810
|
+
permissions: /* @__PURE__ */ new Map(),
|
|
3811
|
+
userRoles: /* @__PURE__ */ new Map()
|
|
3026
3812
|
};
|
|
3027
3813
|
const adapter = {
|
|
3028
3814
|
objects: createMockObjectsRepository(stores),
|
|
@@ -3031,6 +3817,7 @@ function createMockAdapter() {
|
|
|
3031
3817
|
files: createMockFilesRepository(stores),
|
|
3032
3818
|
objectRecords: createMockObjectRecordsRepository(stores),
|
|
3033
3819
|
views: createMockViewsRepository(stores),
|
|
3820
|
+
permissions: createMockPermissionsRepository(stores),
|
|
3034
3821
|
async transaction(callback) {
|
|
3035
3822
|
return await callback(adapter);
|
|
3036
3823
|
},
|
|
@@ -3044,11 +3831,266 @@ function createMockAdapter() {
|
|
|
3044
3831
|
stores.files.clear();
|
|
3045
3832
|
stores.objectRecords.clear();
|
|
3046
3833
|
stores.views.clear();
|
|
3834
|
+
stores.roles.clear();
|
|
3835
|
+
stores.permissions.clear();
|
|
3836
|
+
stores.userRoles.clear();
|
|
3047
3837
|
}
|
|
3048
3838
|
};
|
|
3049
3839
|
return adapter;
|
|
3050
3840
|
}
|
|
3051
3841
|
|
|
3842
|
+
// src/runtime/services/audit.helpers.ts
|
|
3843
|
+
function buildAuditChanges(oldValues, newValues, fieldsToCheck) {
|
|
3844
|
+
const changes = [];
|
|
3845
|
+
for (const field of fieldsToCheck) {
|
|
3846
|
+
if (newValues[field] !== void 0 && newValues[field] !== oldValues[field]) {
|
|
3847
|
+
changes.push({
|
|
3848
|
+
field: String(field),
|
|
3849
|
+
oldValue: oldValues[field],
|
|
3850
|
+
newValue: newValues[field]
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
return changes;
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
// src/runtime/services/audit.service.ts
|
|
3858
|
+
var SENSITIVE_PATTERNS = [
|
|
3859
|
+
/password/i,
|
|
3860
|
+
/token/i,
|
|
3861
|
+
/secret/i,
|
|
3862
|
+
/apiKey/i,
|
|
3863
|
+
/api_key/i,
|
|
3864
|
+
/creditCard/i,
|
|
3865
|
+
/credit_card/i,
|
|
3866
|
+
/ssn/i,
|
|
3867
|
+
/cvv/i
|
|
3868
|
+
];
|
|
3869
|
+
var AuditService = class {
|
|
3870
|
+
constructor(adapter, tenantId, options) {
|
|
3871
|
+
this.adapter = adapter;
|
|
3872
|
+
this.tenantId = tenantId;
|
|
3873
|
+
this.options = options;
|
|
3874
|
+
this.buffer = [];
|
|
3875
|
+
this.flushTimer = null;
|
|
3876
|
+
if (options?.async && options.flushIntervalMs) {
|
|
3877
|
+
this.startFlushTimer();
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
// ============================================================================
|
|
3881
|
+
// PUBLIC LOGGING METHODS
|
|
3882
|
+
// ============================================================================
|
|
3883
|
+
/**
|
|
3884
|
+
* Log a record action (create, update, delete, restore)
|
|
3885
|
+
*/
|
|
3886
|
+
async logRecordAction(params) {
|
|
3887
|
+
const entry = {
|
|
3888
|
+
tenantId: this.tenantId,
|
|
3889
|
+
actorId: params.actorId,
|
|
3890
|
+
actorEmail: params.actorEmail,
|
|
3891
|
+
actorType: "user",
|
|
3892
|
+
action: params.action,
|
|
3893
|
+
resourceType: "record",
|
|
3894
|
+
resourceId: params.recordId,
|
|
3895
|
+
resourceLabel: params.recordLabel,
|
|
3896
|
+
objectName: params.objectName,
|
|
3897
|
+
objectId: params.objectId,
|
|
3898
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
3899
|
+
metadata: params.metadata
|
|
3900
|
+
};
|
|
3901
|
+
await this.log(entry);
|
|
3902
|
+
}
|
|
3903
|
+
/**
|
|
3904
|
+
* Log a schema action (object/attribute changes)
|
|
3905
|
+
*/
|
|
3906
|
+
async logSchemaAction(params) {
|
|
3907
|
+
const entry = {
|
|
3908
|
+
tenantId: this.tenantId,
|
|
3909
|
+
actorId: params.actorId,
|
|
3910
|
+
actorEmail: params.actorEmail,
|
|
3911
|
+
actorType: "user",
|
|
3912
|
+
action: params.action,
|
|
3913
|
+
resourceType: params.resourceType,
|
|
3914
|
+
resourceId: params.resourceId,
|
|
3915
|
+
resourceLabel: params.resourceLabel,
|
|
3916
|
+
objectName: params.objectName,
|
|
3917
|
+
objectId: params.objectId,
|
|
3918
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
3919
|
+
metadata: params.metadata
|
|
3920
|
+
};
|
|
3921
|
+
await this.log(entry);
|
|
3922
|
+
}
|
|
3923
|
+
/**
|
|
3924
|
+
* Log a user action
|
|
3925
|
+
*/
|
|
3926
|
+
async logUserAction(params) {
|
|
3927
|
+
const entry = {
|
|
3928
|
+
tenantId: this.tenantId,
|
|
3929
|
+
actorId: params.actorId,
|
|
3930
|
+
actorEmail: params.actorEmail,
|
|
3931
|
+
actorType: "user",
|
|
3932
|
+
action: params.action,
|
|
3933
|
+
resourceType: "user",
|
|
3934
|
+
resourceId: params.targetUserId,
|
|
3935
|
+
resourceLabel: params.targetUserEmail,
|
|
3936
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
3937
|
+
metadata: params.metadata
|
|
3938
|
+
};
|
|
3939
|
+
await this.log(entry);
|
|
3940
|
+
}
|
|
3941
|
+
/**
|
|
3942
|
+
* Log a role action
|
|
3943
|
+
*/
|
|
3944
|
+
async logRoleAction(params) {
|
|
3945
|
+
const entry = {
|
|
3946
|
+
tenantId: this.tenantId,
|
|
3947
|
+
actorId: params.actorId,
|
|
3948
|
+
actorEmail: params.actorEmail,
|
|
3949
|
+
actorType: "user",
|
|
3950
|
+
action: params.action,
|
|
3951
|
+
resourceType: "role",
|
|
3952
|
+
resourceId: params.roleId,
|
|
3953
|
+
resourceLabel: params.roleLabel,
|
|
3954
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
3955
|
+
metadata: {
|
|
3956
|
+
...params.metadata,
|
|
3957
|
+
targetUserId: params.targetUserId
|
|
3958
|
+
}
|
|
3959
|
+
};
|
|
3960
|
+
await this.log(entry);
|
|
3961
|
+
}
|
|
3962
|
+
/**
|
|
3963
|
+
* Log a system action (no actor)
|
|
3964
|
+
*/
|
|
3965
|
+
async logSystemAction(params) {
|
|
3966
|
+
const entry = {
|
|
3967
|
+
tenantId: this.tenantId,
|
|
3968
|
+
actorType: "system",
|
|
3969
|
+
action: params.action,
|
|
3970
|
+
resourceType: params.resourceType,
|
|
3971
|
+
resourceId: params.resourceId,
|
|
3972
|
+
resourceLabel: params.resourceLabel,
|
|
3973
|
+
objectName: params.objectName,
|
|
3974
|
+
objectId: params.objectId,
|
|
3975
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
3976
|
+
metadata: params.metadata
|
|
3977
|
+
};
|
|
3978
|
+
await this.log(entry);
|
|
3979
|
+
}
|
|
3980
|
+
// ============================================================================
|
|
3981
|
+
// QUERY METHODS
|
|
3982
|
+
// ============================================================================
|
|
3983
|
+
/**
|
|
3984
|
+
* List audit logs with filtering and pagination
|
|
3985
|
+
*/
|
|
3986
|
+
async list(options) {
|
|
3987
|
+
if (!this.adapter.audit) {
|
|
3988
|
+
return { entries: [], total: 0 };
|
|
3989
|
+
}
|
|
3990
|
+
return await this.adapter.audit.list(this.tenantId, options);
|
|
3991
|
+
}
|
|
3992
|
+
/**
|
|
3993
|
+
* Get audit history for a specific record
|
|
3994
|
+
*/
|
|
3995
|
+
async getRecordHistory(recordId) {
|
|
3996
|
+
if (!this.adapter.audit) {
|
|
3997
|
+
return [];
|
|
3998
|
+
}
|
|
3999
|
+
return await this.adapter.audit.getByResource(this.tenantId, "record", recordId, {
|
|
4000
|
+
orderDirection: "desc"
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
/**
|
|
4004
|
+
* Get activity for a specific user
|
|
4005
|
+
*/
|
|
4006
|
+
async getUserActivity(userId, options) {
|
|
4007
|
+
if (!this.adapter.audit) {
|
|
4008
|
+
return [];
|
|
4009
|
+
}
|
|
4010
|
+
return await this.adapter.audit.getByActor(this.tenantId, userId, options);
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* Get audit logs for a specific resource
|
|
4014
|
+
*/
|
|
4015
|
+
async getResourceHistory(resourceType, resourceId) {
|
|
4016
|
+
if (!this.adapter.audit) {
|
|
4017
|
+
return [];
|
|
4018
|
+
}
|
|
4019
|
+
return await this.adapter.audit.getByResource(this.tenantId, resourceType, resourceId, {
|
|
4020
|
+
orderDirection: "desc"
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
4023
|
+
// ============================================================================
|
|
4024
|
+
// BUFFER MANAGEMENT (ASYNC MODE)
|
|
4025
|
+
// ============================================================================
|
|
4026
|
+
/**
|
|
4027
|
+
* Flush buffered logs to the database
|
|
4028
|
+
*/
|
|
4029
|
+
async flush() {
|
|
4030
|
+
if (this.buffer.length === 0 || !this.adapter.audit) {
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
const entries = [...this.buffer];
|
|
4034
|
+
this.buffer = [];
|
|
4035
|
+
await this.adapter.audit.createMany(entries);
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Clean up resources (stop timer, flush remaining logs)
|
|
4039
|
+
*/
|
|
4040
|
+
async dispose() {
|
|
4041
|
+
if (this.flushTimer) {
|
|
4042
|
+
clearInterval(this.flushTimer);
|
|
4043
|
+
this.flushTimer = null;
|
|
4044
|
+
}
|
|
4045
|
+
await this.flush();
|
|
4046
|
+
}
|
|
4047
|
+
// ============================================================================
|
|
4048
|
+
// PRIVATE METHODS
|
|
4049
|
+
// ============================================================================
|
|
4050
|
+
/**
|
|
4051
|
+
* Log an entry (sync or async based on options)
|
|
4052
|
+
*/
|
|
4053
|
+
async log(entry) {
|
|
4054
|
+
if (!this.adapter.audit) {
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
if (this.options?.async) {
|
|
4058
|
+
this.buffer.push(entry);
|
|
4059
|
+
const batchSize = this.options.batchSize ?? 10;
|
|
4060
|
+
if (this.buffer.length >= batchSize) {
|
|
4061
|
+
await this.flush();
|
|
4062
|
+
}
|
|
4063
|
+
} else {
|
|
4064
|
+
await this.adapter.audit.create(entry);
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
/**
|
|
4068
|
+
* Start the flush timer for async mode
|
|
4069
|
+
*/
|
|
4070
|
+
startFlushTimer() {
|
|
4071
|
+
const intervalMs = this.options?.flushIntervalMs ?? 1e3;
|
|
4072
|
+
this.flushTimer = setInterval(() => {
|
|
4073
|
+
this.flush().catch(console.error);
|
|
4074
|
+
}, intervalMs);
|
|
4075
|
+
}
|
|
4076
|
+
/**
|
|
4077
|
+
* Redact sensitive fields from changes
|
|
4078
|
+
*/
|
|
4079
|
+
redactSensitiveFields(changes) {
|
|
4080
|
+
return changes.map((change) => {
|
|
4081
|
+
const isSensitive = SENSITIVE_PATTERNS.some((pattern) => pattern.test(change.field));
|
|
4082
|
+
if (isSensitive) {
|
|
4083
|
+
return {
|
|
4084
|
+
...change,
|
|
4085
|
+
oldValue: "[REDACTED]",
|
|
4086
|
+
newValue: "[REDACTED]"
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
4089
|
+
return change;
|
|
4090
|
+
});
|
|
4091
|
+
}
|
|
4092
|
+
};
|
|
4093
|
+
|
|
3052
4094
|
// src/runtime/services/file.service.ts
|
|
3053
4095
|
var FileService = class {
|
|
3054
4096
|
constructor(adapter, tenantId) {
|
|
@@ -3249,56 +4291,323 @@ var FileService = class {
|
|
|
3249
4291
|
}
|
|
3250
4292
|
};
|
|
3251
4293
|
|
|
3252
|
-
// src/runtime/services/
|
|
3253
|
-
var
|
|
3254
|
-
constructor(adapter) {
|
|
4294
|
+
// src/runtime/services/flow.service.ts
|
|
4295
|
+
var FlowService = class {
|
|
4296
|
+
constructor(adapter, systemFlows = []) {
|
|
3255
4297
|
this.adapter = adapter;
|
|
4298
|
+
this.systemFlows = new Map(systemFlows.map((f) => [f.name, f]));
|
|
3256
4299
|
}
|
|
3257
4300
|
/**
|
|
3258
|
-
*
|
|
4301
|
+
* Get all flows for a tenant (system + custom)
|
|
3259
4302
|
*/
|
|
3260
|
-
async
|
|
3261
|
-
|
|
3262
|
-
|
|
4303
|
+
async getAllFlows(tenantId) {
|
|
4304
|
+
const systemFlowsList = Array.from(this.systemFlows.values());
|
|
4305
|
+
if (!this.adapter.flows) {
|
|
4306
|
+
return systemFlowsList;
|
|
3263
4307
|
}
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
limit: params.limit ?? 5
|
|
3268
|
-
});
|
|
4308
|
+
const dbFlows = await this.adapter.flows.findAllForTenant(tenantId);
|
|
4309
|
+
const customFlows = dbFlows.filter((f) => !f.system).map(this.convertDBFlowToDefinition);
|
|
4310
|
+
return [...systemFlowsList, ...customFlows];
|
|
3269
4311
|
}
|
|
3270
4312
|
/**
|
|
3271
|
-
*
|
|
4313
|
+
* Get published flows only
|
|
3272
4314
|
*/
|
|
3273
|
-
async
|
|
3274
|
-
|
|
3275
|
-
|
|
4315
|
+
async getPublishedFlows(tenantId) {
|
|
4316
|
+
const allFlows = await this.getAllFlows(tenantId);
|
|
4317
|
+
return allFlows.filter((f) => f.status === "published");
|
|
4318
|
+
}
|
|
4319
|
+
/**
|
|
4320
|
+
* Get a specific flow by name
|
|
4321
|
+
*/
|
|
4322
|
+
async getFlow(name, tenantId) {
|
|
4323
|
+
const systemFlow = this.systemFlows.get(name);
|
|
4324
|
+
if (systemFlow) {
|
|
4325
|
+
return systemFlow;
|
|
3276
4326
|
}
|
|
3277
|
-
|
|
4327
|
+
if (!this.adapter.flows) {
|
|
4328
|
+
return null;
|
|
4329
|
+
}
|
|
4330
|
+
const dbFlow = await this.adapter.flows.findByName(tenantId, name);
|
|
4331
|
+
if (dbFlow) {
|
|
4332
|
+
return this.convertDBFlowToDefinition(dbFlow);
|
|
4333
|
+
}
|
|
4334
|
+
return null;
|
|
3278
4335
|
}
|
|
3279
4336
|
/**
|
|
3280
|
-
*
|
|
4337
|
+
* Get a flow by ID
|
|
3281
4338
|
*/
|
|
3282
|
-
async
|
|
3283
|
-
if (!this.adapter.
|
|
3284
|
-
|
|
4339
|
+
async getFlowById(flowId) {
|
|
4340
|
+
if (!this.adapter.flows) {
|
|
4341
|
+
return null;
|
|
3285
4342
|
}
|
|
3286
|
-
|
|
4343
|
+
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
4344
|
+
if (dbFlow) {
|
|
4345
|
+
return this.convertDBFlowToDefinition(dbFlow);
|
|
4346
|
+
}
|
|
4347
|
+
return null;
|
|
3287
4348
|
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
4349
|
+
/**
|
|
4350
|
+
* Create a new custom flow (as draft)
|
|
4351
|
+
*/
|
|
4352
|
+
async createFlow(input, tenantId) {
|
|
4353
|
+
if (!this.adapter.flows) {
|
|
4354
|
+
throw new Error("Flows feature is not enabled. Database adapter does not support flows.");
|
|
4355
|
+
}
|
|
4356
|
+
this.validateFlowName(input.name);
|
|
4357
|
+
const existing = await this.adapter.flows.findByName(tenantId, input.name);
|
|
4358
|
+
if (existing) {
|
|
4359
|
+
throw new Error(`Flow "${input.name}" already exists`);
|
|
4360
|
+
}
|
|
4361
|
+
if (this.systemFlows.has(input.name)) {
|
|
4362
|
+
throw new Error(
|
|
4363
|
+
`Cannot create flow "${input.name}": a system flow with this name already exists`
|
|
4364
|
+
);
|
|
4365
|
+
}
|
|
4366
|
+
this.validateFlowStructure(input);
|
|
4367
|
+
const dbFlow = await this.adapter.flows.create({
|
|
4368
|
+
tenantId,
|
|
4369
|
+
name: input.name,
|
|
4370
|
+
label: input.label,
|
|
4371
|
+
description: input.description,
|
|
4372
|
+
icon: input.icon,
|
|
4373
|
+
status: "draft",
|
|
4374
|
+
version: 1,
|
|
4375
|
+
slots: input.slots,
|
|
4376
|
+
pages: input.pages,
|
|
4377
|
+
relations: input.relations,
|
|
4378
|
+
system: false,
|
|
4379
|
+
metadata: input.metadata
|
|
4380
|
+
});
|
|
4381
|
+
return this.convertDBFlowToDefinition(dbFlow);
|
|
3295
4382
|
}
|
|
3296
4383
|
/**
|
|
3297
|
-
*
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
4384
|
+
* Update a custom flow
|
|
4385
|
+
*/
|
|
4386
|
+
async updateFlow(flowId, input) {
|
|
4387
|
+
if (!this.adapter.flows) {
|
|
4388
|
+
throw new Error("Flows feature is not enabled.");
|
|
4389
|
+
}
|
|
4390
|
+
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
4391
|
+
if (!dbFlow) {
|
|
4392
|
+
throw new Error(`Flow with id "${flowId}" not found`);
|
|
4393
|
+
}
|
|
4394
|
+
if (dbFlow.system) {
|
|
4395
|
+
throw new Error("Cannot modify system flows. System flows are protected.");
|
|
4396
|
+
}
|
|
4397
|
+
if (input.slots || input.pages || input.relations) {
|
|
4398
|
+
this.validateFlowStructure({
|
|
4399
|
+
name: dbFlow.name,
|
|
4400
|
+
label: input.label ?? dbFlow.label,
|
|
4401
|
+
slots: input.slots ?? dbFlow.slots,
|
|
4402
|
+
pages: input.pages ?? dbFlow.pages,
|
|
4403
|
+
relations: input.relations ?? dbFlow.relations
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4406
|
+
const updated = await this.adapter.flows.update(flowId, {
|
|
4407
|
+
label: input.label,
|
|
4408
|
+
description: input.description,
|
|
4409
|
+
icon: input.icon,
|
|
4410
|
+
slots: input.slots,
|
|
4411
|
+
pages: input.pages,
|
|
4412
|
+
relations: input.relations,
|
|
4413
|
+
metadata: input.metadata
|
|
4414
|
+
});
|
|
4415
|
+
return this.convertDBFlowToDefinition(updated);
|
|
4416
|
+
}
|
|
4417
|
+
/**
|
|
4418
|
+
* Publish a flow
|
|
4419
|
+
*/
|
|
4420
|
+
async publishFlow(flowId) {
|
|
4421
|
+
if (!this.adapter.flows) {
|
|
4422
|
+
throw new Error("Flows feature is not enabled.");
|
|
4423
|
+
}
|
|
4424
|
+
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
4425
|
+
if (!dbFlow) {
|
|
4426
|
+
throw new Error(`Flow with id "${flowId}" not found`);
|
|
4427
|
+
}
|
|
4428
|
+
if (dbFlow.system) {
|
|
4429
|
+
throw new Error("Cannot publish system flows. They are always published.");
|
|
4430
|
+
}
|
|
4431
|
+
if (dbFlow.status === "published") {
|
|
4432
|
+
return this.convertDBFlowToDefinition(dbFlow);
|
|
4433
|
+
}
|
|
4434
|
+
this.validateFlowStructure({
|
|
4435
|
+
name: dbFlow.name,
|
|
4436
|
+
label: dbFlow.label,
|
|
4437
|
+
slots: dbFlow.slots,
|
|
4438
|
+
pages: dbFlow.pages,
|
|
4439
|
+
relations: dbFlow.relations
|
|
4440
|
+
});
|
|
4441
|
+
const updated = await this.adapter.flows.update(flowId, {
|
|
4442
|
+
status: "published",
|
|
4443
|
+
version: dbFlow.version + 1
|
|
4444
|
+
});
|
|
4445
|
+
return this.convertDBFlowToDefinition(updated);
|
|
4446
|
+
}
|
|
4447
|
+
/**
|
|
4448
|
+
* Archive a flow
|
|
4449
|
+
*/
|
|
4450
|
+
async archiveFlow(flowId) {
|
|
4451
|
+
if (!this.adapter.flows) {
|
|
4452
|
+
throw new Error("Flows feature is not enabled.");
|
|
4453
|
+
}
|
|
4454
|
+
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
4455
|
+
if (!dbFlow) {
|
|
4456
|
+
throw new Error(`Flow with id "${flowId}" not found`);
|
|
4457
|
+
}
|
|
4458
|
+
if (dbFlow.system) {
|
|
4459
|
+
throw new Error("Cannot archive system flows.");
|
|
4460
|
+
}
|
|
4461
|
+
const updated = await this.adapter.flows.update(flowId, {
|
|
4462
|
+
status: "archived"
|
|
4463
|
+
});
|
|
4464
|
+
return this.convertDBFlowToDefinition(updated);
|
|
4465
|
+
}
|
|
4466
|
+
/**
|
|
4467
|
+
* Delete a custom flow
|
|
4468
|
+
*/
|
|
4469
|
+
async deleteFlow(flowId) {
|
|
4470
|
+
if (!this.adapter.flows) {
|
|
4471
|
+
throw new Error("Flows feature is not enabled.");
|
|
4472
|
+
}
|
|
4473
|
+
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
4474
|
+
if (!dbFlow) {
|
|
4475
|
+
throw new Error(`Flow with id "${flowId}" not found`);
|
|
4476
|
+
}
|
|
4477
|
+
if (dbFlow.system) {
|
|
4478
|
+
throw new Error("Cannot delete system flows. System flows are protected.");
|
|
4479
|
+
}
|
|
4480
|
+
await this.adapter.flows.delete(flowId);
|
|
4481
|
+
}
|
|
4482
|
+
// ============================================================================
|
|
4483
|
+
// PRIVATE HELPERS
|
|
4484
|
+
// ============================================================================
|
|
4485
|
+
/**
|
|
4486
|
+
* Validate flow name format (kebab-case)
|
|
4487
|
+
*/
|
|
4488
|
+
validateFlowName(name) {
|
|
4489
|
+
if (!name || name.length === 0) {
|
|
4490
|
+
throw new Error("Flow name cannot be empty");
|
|
4491
|
+
}
|
|
4492
|
+
if (name.length > 63) {
|
|
4493
|
+
throw new Error("Flow name is too long (max 63 characters)");
|
|
4494
|
+
}
|
|
4495
|
+
const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
|
|
4496
|
+
if (!kebabCaseRegex.test(name)) {
|
|
4497
|
+
throw new Error(
|
|
4498
|
+
"Invalid flow name format. Name must be in kebab-case (e.g., 'couple-creation', 'new-contact')"
|
|
4499
|
+
);
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
/**
|
|
4503
|
+
* Validate flow structure (slots, pages, relations)
|
|
4504
|
+
*/
|
|
4505
|
+
validateFlowStructure(input) {
|
|
4506
|
+
if (!input.slots || input.slots.length === 0) {
|
|
4507
|
+
throw new Error("Flow must have at least one slot");
|
|
4508
|
+
}
|
|
4509
|
+
if (!input.pages || input.pages.length === 0) {
|
|
4510
|
+
throw new Error("Flow must have at least one page");
|
|
4511
|
+
}
|
|
4512
|
+
const slotIds = new Set(input.slots.map((s) => s.id));
|
|
4513
|
+
if (slotIds.size !== input.slots.length) {
|
|
4514
|
+
throw new Error("Duplicate slot IDs detected");
|
|
4515
|
+
}
|
|
4516
|
+
for (const page of input.pages) {
|
|
4517
|
+
for (const row of page.rows) {
|
|
4518
|
+
for (const field of row.fields) {
|
|
4519
|
+
if (!slotIds.has(field.slotId)) {
|
|
4520
|
+
throw new Error(`Field "${field.id}" references unknown slot "${field.slotId}"`);
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
for (const relation2 of input.relations) {
|
|
4526
|
+
if (!slotIds.has(relation2.sourceSlotId)) {
|
|
4527
|
+
throw new Error(`Relation references unknown source slot "${relation2.sourceSlotId}"`);
|
|
4528
|
+
}
|
|
4529
|
+
if (!slotIds.has(relation2.targetSlotId)) {
|
|
4530
|
+
throw new Error(`Relation references unknown target slot "${relation2.targetSlotId}"`);
|
|
4531
|
+
}
|
|
4532
|
+
if (relation2.sourceSlotId === relation2.targetSlotId) {
|
|
4533
|
+
throw new Error(`Relation cannot link a slot to itself: "${relation2.sourceSlotId}"`);
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
/**
|
|
4538
|
+
* Convert database flow to FlowDefinition
|
|
4539
|
+
*/
|
|
4540
|
+
convertDBFlowToDefinition(dbFlow) {
|
|
4541
|
+
return {
|
|
4542
|
+
id: dbFlow.id,
|
|
4543
|
+
name: dbFlow.name,
|
|
4544
|
+
label: dbFlow.label,
|
|
4545
|
+
description: dbFlow.description,
|
|
4546
|
+
icon: dbFlow.icon,
|
|
4547
|
+
status: dbFlow.status,
|
|
4548
|
+
version: dbFlow.version,
|
|
4549
|
+
slots: dbFlow.slots,
|
|
4550
|
+
pages: dbFlow.pages,
|
|
4551
|
+
relations: dbFlow.relations,
|
|
4552
|
+
system: dbFlow.system,
|
|
4553
|
+
tenantId: dbFlow.tenantId,
|
|
4554
|
+
metadata: dbFlow.metadata,
|
|
4555
|
+
createdAt: dbFlow.createdAt,
|
|
4556
|
+
updatedAt: dbFlow.updatedAt
|
|
4557
|
+
};
|
|
4558
|
+
}
|
|
4559
|
+
};
|
|
4560
|
+
|
|
4561
|
+
// src/runtime/services/geocoding.service.ts
|
|
4562
|
+
var GeocodingService = class {
|
|
4563
|
+
constructor(adapter) {
|
|
4564
|
+
this.adapter = adapter;
|
|
4565
|
+
}
|
|
4566
|
+
/**
|
|
4567
|
+
* Search for address suggestions as the user types
|
|
4568
|
+
*/
|
|
4569
|
+
async autocomplete(params) {
|
|
4570
|
+
if (!params.query || params.query.trim().length < 2) {
|
|
4571
|
+
return [];
|
|
4572
|
+
}
|
|
4573
|
+
return await this.adapter.autocomplete({
|
|
4574
|
+
...params,
|
|
4575
|
+
query: params.query.trim(),
|
|
4576
|
+
limit: params.limit ?? 5
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4579
|
+
/**
|
|
4580
|
+
* Reverse geocode coordinates to an address
|
|
4581
|
+
*/
|
|
4582
|
+
async reverse(params) {
|
|
4583
|
+
if (!this.adapter.reverse) {
|
|
4584
|
+
throw new Error("Reverse geocoding is not supported by the configured adapter");
|
|
4585
|
+
}
|
|
4586
|
+
return await this.adapter.reverse(params);
|
|
4587
|
+
}
|
|
4588
|
+
/**
|
|
4589
|
+
* Geocode a structured address to coordinates
|
|
4590
|
+
*/
|
|
4591
|
+
async geocode(params) {
|
|
4592
|
+
if (!this.adapter.geocode) {
|
|
4593
|
+
throw new Error("Geocoding is not supported by the configured adapter");
|
|
4594
|
+
}
|
|
4595
|
+
return await this.adapter.geocode(params);
|
|
4596
|
+
}
|
|
4597
|
+
};
|
|
4598
|
+
|
|
4599
|
+
// src/runtime/services/global-search.service.ts
|
|
4600
|
+
var GlobalSearchService = class {
|
|
4601
|
+
constructor(adapter, tenantId) {
|
|
4602
|
+
this.adapter = adapter;
|
|
4603
|
+
this.tenantId = tenantId;
|
|
4604
|
+
}
|
|
4605
|
+
/**
|
|
4606
|
+
* Search across all object records for the tenant
|
|
4607
|
+
*
|
|
4608
|
+
* @param query - Search query string
|
|
4609
|
+
* @param options - Search options (pagination, object filters)
|
|
4610
|
+
* @returns Matching records with object metadata and total count
|
|
3302
4611
|
*
|
|
3303
4612
|
* @example
|
|
3304
4613
|
* ```typescript
|
|
@@ -3366,9 +4675,12 @@ var GlobalSearchService = class {
|
|
|
3366
4675
|
|
|
3367
4676
|
// src/runtime/services/object-schema.service.ts
|
|
3368
4677
|
var ObjectSchemaService = class {
|
|
3369
|
-
constructor(adapter, nativeRegistry) {
|
|
4678
|
+
constructor(adapter, nativeRegistry, options) {
|
|
3370
4679
|
this.adapter = adapter;
|
|
3371
4680
|
this.nativeRegistry = nativeRegistry;
|
|
4681
|
+
this.auditService = options?.auditService;
|
|
4682
|
+
this.userId = options?.userId;
|
|
4683
|
+
this.userEmail = options?.userEmail;
|
|
3372
4684
|
}
|
|
3373
4685
|
/**
|
|
3374
4686
|
* Create a new custom object
|
|
@@ -3410,41 +4722,57 @@ var ObjectSchemaService = class {
|
|
|
3410
4722
|
...objectDef.metadata,
|
|
3411
4723
|
icon: objectDef.icon
|
|
3412
4724
|
};
|
|
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
|
-
|
|
4725
|
+
const { dbObject, createdAttributes } = await this.adapter.transaction(async (tx) => {
|
|
4726
|
+
const dbObj = await tx.objects.create({
|
|
4727
|
+
tenantId,
|
|
4728
|
+
name: objectDef.name,
|
|
4729
|
+
label: objectDef.label,
|
|
4730
|
+
pluralLabel: objectDef.pluralLabel,
|
|
4731
|
+
description: objectDef.description,
|
|
4732
|
+
labelExpression: objectDef.labelExpression,
|
|
4733
|
+
system: false,
|
|
4734
|
+
// Always false for custom objects
|
|
4735
|
+
metadata
|
|
4736
|
+
});
|
|
4737
|
+
const attrs = [];
|
|
4738
|
+
if (objectDef.attributes.length > 0) {
|
|
4739
|
+
for (let i = 0; i < objectDef.attributes.length; i++) {
|
|
4740
|
+
const attr = objectDef.attributes[i];
|
|
4741
|
+
const config = validatedConfigs[i];
|
|
4742
|
+
const dbAttr = await tx.attributes.create({
|
|
4743
|
+
objectId: dbObj.id,
|
|
4744
|
+
name: attr.name,
|
|
4745
|
+
label: attr.label,
|
|
4746
|
+
type: attr.type,
|
|
4747
|
+
required: attr.required ?? false,
|
|
4748
|
+
unique: false,
|
|
4749
|
+
system: false,
|
|
4750
|
+
config
|
|
4751
|
+
});
|
|
4752
|
+
attrs.push(this.convertDBAttributeToAttribute(dbAttr));
|
|
4753
|
+
}
|
|
3440
4754
|
}
|
|
3441
|
-
|
|
3442
|
-
|
|
4755
|
+
return { dbObject: dbObj, createdAttributes: attrs };
|
|
4756
|
+
});
|
|
4757
|
+
const result = {
|
|
3443
4758
|
...objectDef,
|
|
3444
4759
|
id: dbObject.id,
|
|
3445
4760
|
attributes: createdAttributes,
|
|
3446
4761
|
system: false
|
|
3447
4762
|
};
|
|
4763
|
+
if (this.auditService && this.userId) {
|
|
4764
|
+
await this.auditService.logSchemaAction({
|
|
4765
|
+
action: "object.created",
|
|
4766
|
+
actorId: this.userId,
|
|
4767
|
+
actorEmail: this.userEmail,
|
|
4768
|
+
resourceType: "object",
|
|
4769
|
+
resourceId: dbObject.id,
|
|
4770
|
+
resourceLabel: dbObject.label,
|
|
4771
|
+
objectName: dbObject.name,
|
|
4772
|
+
objectId: dbObject.id
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
return result;
|
|
3448
4776
|
}
|
|
3449
4777
|
/**
|
|
3450
4778
|
* Add a custom attribute to an existing object
|
|
@@ -3480,6 +4808,18 @@ var ObjectSchemaService = class {
|
|
|
3480
4808
|
// Always false for attributes added via API
|
|
3481
4809
|
config
|
|
3482
4810
|
});
|
|
4811
|
+
if (this.auditService && this.userId) {
|
|
4812
|
+
await this.auditService.logSchemaAction({
|
|
4813
|
+
action: "attribute.created",
|
|
4814
|
+
actorId: this.userId,
|
|
4815
|
+
actorEmail: this.userEmail,
|
|
4816
|
+
resourceType: "attribute",
|
|
4817
|
+
resourceId: dbAttr.id,
|
|
4818
|
+
resourceLabel: dbAttr.label,
|
|
4819
|
+
objectName: dbObject.name,
|
|
4820
|
+
objectId: dbObject.id
|
|
4821
|
+
});
|
|
4822
|
+
}
|
|
3483
4823
|
return this.convertDBAttributeToAttribute(dbAttr);
|
|
3484
4824
|
}
|
|
3485
4825
|
/**
|
|
@@ -3523,11 +4863,39 @@ var ObjectSchemaService = class {
|
|
|
3523
4863
|
if (dbAttr.type === "relation" && updates.targets) {
|
|
3524
4864
|
await this.validateRelationTargets(mergedInput, tenantId);
|
|
3525
4865
|
}
|
|
4866
|
+
const oldValues = {
|
|
4867
|
+
label: dbAttr.label,
|
|
4868
|
+
required: dbAttr.required
|
|
4869
|
+
};
|
|
3526
4870
|
const updatedDbAttr = await this.adapter.attributes.update(attributeId, {
|
|
3527
4871
|
label: updates.label,
|
|
3528
4872
|
required: updates.required,
|
|
3529
4873
|
config
|
|
3530
4874
|
});
|
|
4875
|
+
if (this.auditService && this.userId) {
|
|
4876
|
+
const changes = buildAuditChanges(oldValues, updates, ["label", "required"]);
|
|
4877
|
+
if (changes.length > 0) {
|
|
4878
|
+
const dbObject = await this.adapter.objects.findById(dbAttr.objectId);
|
|
4879
|
+
await this.auditService.logSchemaAction({
|
|
4880
|
+
action: "attribute.updated",
|
|
4881
|
+
actorId: this.userId,
|
|
4882
|
+
actorEmail: this.userEmail,
|
|
4883
|
+
resourceType: "attribute",
|
|
4884
|
+
resourceId: attributeId,
|
|
4885
|
+
resourceLabel: updatedDbAttr.label,
|
|
4886
|
+
objectName: dbObject?.name,
|
|
4887
|
+
objectId: dbAttr.objectId,
|
|
4888
|
+
changes
|
|
4889
|
+
});
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
if (updates.required !== void 0 && updates.required !== oldValues.required) {
|
|
4893
|
+
const schema = await this.getObjectSchema(dbAttr.objectId);
|
|
4894
|
+
await this.adapter.objectRecords.batchRefreshStatus(
|
|
4895
|
+
dbAttr.objectId,
|
|
4896
|
+
(values) => computeRecordStatus(schema, values)
|
|
4897
|
+
);
|
|
4898
|
+
}
|
|
3531
4899
|
return this.convertDBAttributeToAttribute(updatedDbAttr);
|
|
3532
4900
|
}
|
|
3533
4901
|
/**
|
|
@@ -3546,7 +4914,27 @@ var ObjectSchemaService = class {
|
|
|
3546
4914
|
`Cannot delete system attribute "${dbAttr.name}". System attributes are protected.`
|
|
3547
4915
|
);
|
|
3548
4916
|
}
|
|
4917
|
+
const dbObject = await this.adapter.objects.findById(dbAttr.objectId);
|
|
4918
|
+
if (dbObject?.labelExpression) {
|
|
4919
|
+
const usedAttributes = extractAttributeNames(dbObject.labelExpression);
|
|
4920
|
+
if (usedAttributes.includes(dbAttr.name)) {
|
|
4921
|
+
throw new AttributeInUseError(dbAttr.name, "labelExpression");
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
3549
4924
|
await this.adapter.attributes.delete(attributeId);
|
|
4925
|
+
await this.adapter.objectRecords.removeAttributeData(dbAttr.objectId, dbAttr.name);
|
|
4926
|
+
if (this.auditService && this.userId) {
|
|
4927
|
+
await this.auditService.logSchemaAction({
|
|
4928
|
+
action: "attribute.deleted",
|
|
4929
|
+
actorId: this.userId,
|
|
4930
|
+
actorEmail: this.userEmail,
|
|
4931
|
+
resourceType: "attribute",
|
|
4932
|
+
resourceId: attributeId,
|
|
4933
|
+
resourceLabel: dbAttr.label,
|
|
4934
|
+
objectName: dbObject?.name,
|
|
4935
|
+
objectId: dbAttr.objectId
|
|
4936
|
+
});
|
|
4937
|
+
}
|
|
3550
4938
|
}
|
|
3551
4939
|
/**
|
|
3552
4940
|
* List attributes for an object
|
|
@@ -3591,6 +4979,13 @@ var ObjectSchemaService = class {
|
|
|
3591
4979
|
`Cannot modify system object "${dbObject.name}". System objects are protected.`
|
|
3592
4980
|
);
|
|
3593
4981
|
}
|
|
4982
|
+
const oldValues = {
|
|
4983
|
+
label: dbObject.label,
|
|
4984
|
+
pluralLabel: dbObject.pluralLabel,
|
|
4985
|
+
description: dbObject.description,
|
|
4986
|
+
labelExpression: dbObject.labelExpression,
|
|
4987
|
+
icon: dbObject.metadata?.icon
|
|
4988
|
+
};
|
|
3594
4989
|
let metadata = dbObject.metadata;
|
|
3595
4990
|
if (updates.icon !== void 0 || updates.metadata !== void 0) {
|
|
3596
4991
|
metadata = {
|
|
@@ -3606,6 +5001,35 @@ var ObjectSchemaService = class {
|
|
|
3606
5001
|
labelExpression: updates.labelExpression,
|
|
3607
5002
|
metadata
|
|
3608
5003
|
});
|
|
5004
|
+
if (this.auditService && this.userId) {
|
|
5005
|
+
const changes = buildAuditChanges(oldValues, { ...updates, icon: updates.icon }, [
|
|
5006
|
+
"label",
|
|
5007
|
+
"pluralLabel",
|
|
5008
|
+
"description",
|
|
5009
|
+
"labelExpression",
|
|
5010
|
+
"icon"
|
|
5011
|
+
]);
|
|
5012
|
+
if (changes.length > 0) {
|
|
5013
|
+
await this.auditService.logSchemaAction({
|
|
5014
|
+
action: "object.updated",
|
|
5015
|
+
actorId: this.userId,
|
|
5016
|
+
actorEmail: this.userEmail,
|
|
5017
|
+
resourceType: "object",
|
|
5018
|
+
resourceId: objectId,
|
|
5019
|
+
resourceLabel: updatedDbObject.label,
|
|
5020
|
+
objectName: updatedDbObject.name,
|
|
5021
|
+
objectId,
|
|
5022
|
+
changes
|
|
5023
|
+
});
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
if (updates.labelExpression !== void 0 && updates.labelExpression !== oldValues.labelExpression) {
|
|
5027
|
+
const newExpression = updates.labelExpression;
|
|
5028
|
+
await this.adapter.objectRecords.batchRefreshLabels(
|
|
5029
|
+
objectId,
|
|
5030
|
+
(values) => renderLabelExpression(newExpression, values)
|
|
5031
|
+
);
|
|
5032
|
+
}
|
|
3609
5033
|
const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
3610
5034
|
return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
|
|
3611
5035
|
}
|
|
@@ -3621,7 +5045,7 @@ var ObjectSchemaService = class {
|
|
|
3621
5045
|
* await service.deleteObject("obj-123");
|
|
3622
5046
|
* ```
|
|
3623
5047
|
*/
|
|
3624
|
-
async deleteObject(objectId) {
|
|
5048
|
+
async deleteObject(objectId, tenantId) {
|
|
3625
5049
|
const dbObject = await this.adapter.objects.findById(objectId);
|
|
3626
5050
|
if (!dbObject) {
|
|
3627
5051
|
throw new Error(`Object with id "${objectId}" not found`);
|
|
@@ -3631,7 +5055,29 @@ var ObjectSchemaService = class {
|
|
|
3631
5055
|
`Cannot delete system object "${dbObject.name}". System objects are protected.`
|
|
3632
5056
|
);
|
|
3633
5057
|
}
|
|
5058
|
+
const effectiveTenantId = tenantId ?? dbObject.tenantId;
|
|
5059
|
+
if (effectiveTenantId) {
|
|
5060
|
+
const referencingObjects = await this.findObjectsWithRelationTo(
|
|
5061
|
+
dbObject.name,
|
|
5062
|
+
effectiveTenantId
|
|
5063
|
+
);
|
|
5064
|
+
if (referencingObjects.length > 0) {
|
|
5065
|
+
throw new ObjectReferencedError(dbObject.name, referencingObjects);
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
3634
5068
|
await this.adapter.objects.delete(objectId);
|
|
5069
|
+
if (this.auditService && this.userId) {
|
|
5070
|
+
await this.auditService.logSchemaAction({
|
|
5071
|
+
action: "object.deleted",
|
|
5072
|
+
actorId: this.userId,
|
|
5073
|
+
actorEmail: this.userEmail,
|
|
5074
|
+
resourceType: "object",
|
|
5075
|
+
resourceId: objectId,
|
|
5076
|
+
resourceLabel: dbObject.label,
|
|
5077
|
+
objectName: dbObject.name,
|
|
5078
|
+
objectId
|
|
5079
|
+
});
|
|
5080
|
+
}
|
|
3635
5081
|
}
|
|
3636
5082
|
/**
|
|
3637
5083
|
* Validate attribute name format (variable identifier)
|
|
@@ -3724,148 +5170,595 @@ var ObjectSchemaService = class {
|
|
|
3724
5170
|
return schemas;
|
|
3725
5171
|
}
|
|
3726
5172
|
/**
|
|
3727
|
-
* Merge native object from registry with custom attributes from DB
|
|
3728
|
-
* @internal
|
|
5173
|
+
* Merge native object from registry with custom attributes from DB
|
|
5174
|
+
* @internal
|
|
5175
|
+
*/
|
|
5176
|
+
mergeNativeObject(dbObject, dbAttributes) {
|
|
5177
|
+
const nativeObject = this.nativeRegistry.getByName(dbObject.name);
|
|
5178
|
+
if (!nativeObject) {
|
|
5179
|
+
throw new Error(
|
|
5180
|
+
`Native object "${dbObject.name}" not found in registry. Make sure to register it before querying.`
|
|
5181
|
+
);
|
|
5182
|
+
}
|
|
5183
|
+
const systemAttributesFromDB = dbAttributes.filter((attr) => attr.system);
|
|
5184
|
+
const customAttributes = dbAttributes.filter((attr) => !attr.system);
|
|
5185
|
+
const dbAttrByName = new Map(systemAttributesFromDB.map((attr) => [attr.name, attr]));
|
|
5186
|
+
const systemAttrs = nativeObject.attributes.map((registryAttr) => {
|
|
5187
|
+
const dbAttr = dbAttrByName.get(registryAttr.name);
|
|
5188
|
+
if (dbAttr) {
|
|
5189
|
+
return {
|
|
5190
|
+
...registryAttr,
|
|
5191
|
+
id: dbAttr.id
|
|
5192
|
+
};
|
|
5193
|
+
}
|
|
5194
|
+
return registryAttr;
|
|
5195
|
+
});
|
|
5196
|
+
const customAttrs = customAttributes.map(
|
|
5197
|
+
(dbAttr) => this.convertDBAttributeToAttribute(dbAttr)
|
|
5198
|
+
);
|
|
5199
|
+
return {
|
|
5200
|
+
...nativeObject,
|
|
5201
|
+
id: dbObject.id,
|
|
5202
|
+
attributes: [
|
|
5203
|
+
...systemAttrs,
|
|
5204
|
+
// System attributes from registry (with DB IDs if synced)
|
|
5205
|
+
...customAttrs
|
|
5206
|
+
// Custom attributes from DB
|
|
5207
|
+
]
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
5210
|
+
/**
|
|
5211
|
+
* Convert DB object + attributes to ObjectDefinition
|
|
5212
|
+
* @internal
|
|
5213
|
+
*/
|
|
5214
|
+
convertDBObjectToDefinition(dbObject, dbAttributes) {
|
|
5215
|
+
const attributes = dbAttributes.map(
|
|
5216
|
+
(dbAttr) => this.convertDBAttributeToAttribute(dbAttr)
|
|
5217
|
+
);
|
|
5218
|
+
return {
|
|
5219
|
+
id: dbObject.id,
|
|
5220
|
+
name: dbObject.name,
|
|
5221
|
+
label: dbObject.label,
|
|
5222
|
+
pluralLabel: dbObject.pluralLabel,
|
|
5223
|
+
description: dbObject.description,
|
|
5224
|
+
icon: dbObject.metadata?.icon,
|
|
5225
|
+
labelExpression: dbObject.labelExpression,
|
|
5226
|
+
attributes,
|
|
5227
|
+
system: dbObject.system,
|
|
5228
|
+
metadata: dbObject.metadata
|
|
5229
|
+
};
|
|
5230
|
+
}
|
|
5231
|
+
/**
|
|
5232
|
+
* Validate attribute input and return parsed config
|
|
5233
|
+
* Validates: name format, config schema, relation targets
|
|
5234
|
+
* @internal
|
|
5235
|
+
*/
|
|
5236
|
+
async validateAttributeInput(attribute, tenantId) {
|
|
5237
|
+
this.validateAttributeName(attribute.name);
|
|
5238
|
+
const config = this.buildAttributeConfig(attribute);
|
|
5239
|
+
if (attribute.type === "relation") {
|
|
5240
|
+
await this.validateRelationTargets(attribute, tenantId);
|
|
5241
|
+
}
|
|
5242
|
+
return config;
|
|
5243
|
+
}
|
|
5244
|
+
/**
|
|
5245
|
+
* Build and validate attribute config from input
|
|
5246
|
+
* Uses Zod schema to validate and strip unknown properties
|
|
5247
|
+
* @internal
|
|
5248
|
+
*/
|
|
5249
|
+
buildAttributeConfig(attribute) {
|
|
5250
|
+
const DB_COLUMN_FIELDS = ["name", "label", "type", "required", "unique"];
|
|
5251
|
+
const configInput = {};
|
|
5252
|
+
for (const [key, value] of Object.entries(attribute)) {
|
|
5253
|
+
if (!DB_COLUMN_FIELDS.includes(key)) {
|
|
5254
|
+
configInput[key] = value;
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
try {
|
|
5258
|
+
return parseAttributeConfig(attribute.type, configInput);
|
|
5259
|
+
} catch (error) {
|
|
5260
|
+
if (error instanceof Error) {
|
|
5261
|
+
throw new Error(`Invalid config for ${attribute.type} attribute: ${error.message}`);
|
|
5262
|
+
}
|
|
5263
|
+
throw error;
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
/**
|
|
5267
|
+
* Check if an object exists (in tenant, system, or native registry)
|
|
5268
|
+
* @internal
|
|
5269
|
+
*/
|
|
5270
|
+
async objectExists(name, tenantId) {
|
|
5271
|
+
if (tenantId) {
|
|
5272
|
+
const obj = await this.adapter.objects.findByName(tenantId, name);
|
|
5273
|
+
if (obj) return true;
|
|
5274
|
+
}
|
|
5275
|
+
const systemObj = await this.adapter.objects.findSystemByName(name);
|
|
5276
|
+
if (systemObj) return true;
|
|
5277
|
+
return this.nativeRegistry.getByName(name) !== void 0;
|
|
5278
|
+
}
|
|
5279
|
+
/**
|
|
5280
|
+
* Validate that relation target objects exist
|
|
5281
|
+
* @internal
|
|
5282
|
+
*/
|
|
5283
|
+
async validateRelationTargets(attribute, tenantId) {
|
|
5284
|
+
const targets = attribute.targets;
|
|
5285
|
+
if (!targets || targets.length === 0) {
|
|
5286
|
+
throw new Error("Relation attribute must have at least one target object");
|
|
5287
|
+
}
|
|
5288
|
+
const cardinality = attribute.cardinality;
|
|
5289
|
+
const isValidCardinality = cardinality === "one" || cardinality === "many";
|
|
5290
|
+
if (!isValidCardinality) {
|
|
5291
|
+
throw new Error("Relation attribute must specify cardinality: 'one' or 'many'");
|
|
5292
|
+
}
|
|
5293
|
+
for (const target of targets) {
|
|
5294
|
+
if (!target.object) {
|
|
5295
|
+
throw new Error("Each relation target must specify an object name");
|
|
5296
|
+
}
|
|
5297
|
+
const exists = await this.objectExists(target.object, tenantId);
|
|
5298
|
+
if (!exists) {
|
|
5299
|
+
throw new Error(
|
|
5300
|
+
`Relation target object "${target.object}" does not exist. Make sure the object is created before adding a relation to it.`
|
|
5301
|
+
);
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
/**
|
|
5306
|
+
* Find objects that have a relation attribute targeting the given object.
|
|
5307
|
+
* Used to prevent deletion of objects that are relation targets.
|
|
5308
|
+
* @internal
|
|
5309
|
+
*/
|
|
5310
|
+
async findObjectsWithRelationTo(targetObjectName, tenantId) {
|
|
5311
|
+
const allObjects = await this.adapter.objects.list(tenantId);
|
|
5312
|
+
const referencing = [];
|
|
5313
|
+
for (const obj of allObjects) {
|
|
5314
|
+
if (obj.name === targetObjectName) continue;
|
|
5315
|
+
const attrs = await this.adapter.attributes.findByObjectId(obj.id);
|
|
5316
|
+
const hasRelationToTarget = attrs.some((attr) => {
|
|
5317
|
+
if (attr.type !== "relation") return false;
|
|
5318
|
+
const config = attr.config;
|
|
5319
|
+
return config?.targets?.some((t) => t.object === targetObjectName) ?? false;
|
|
5320
|
+
});
|
|
5321
|
+
if (hasRelationToTarget) {
|
|
5322
|
+
referencing.push(obj.name);
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
return referencing;
|
|
5326
|
+
}
|
|
5327
|
+
/**
|
|
5328
|
+
* Convert DB attribute to Attribute type
|
|
5329
|
+
* @internal
|
|
5330
|
+
*/
|
|
5331
|
+
convertDBAttributeToAttribute(dbAttr) {
|
|
5332
|
+
const baseAttr = {
|
|
5333
|
+
...dbAttr.config,
|
|
5334
|
+
id: dbAttr.id,
|
|
5335
|
+
name: dbAttr.name,
|
|
5336
|
+
label: dbAttr.label,
|
|
5337
|
+
type: dbAttr.type,
|
|
5338
|
+
required: dbAttr.required,
|
|
5339
|
+
system: dbAttr.system,
|
|
5340
|
+
unique: dbAttr.unique
|
|
5341
|
+
};
|
|
5342
|
+
return baseAttr;
|
|
5343
|
+
}
|
|
5344
|
+
};
|
|
5345
|
+
|
|
5346
|
+
// src/runtime/services/permission.service.ts
|
|
5347
|
+
var PermissionService = class {
|
|
5348
|
+
constructor(adapter, tenantId, options) {
|
|
5349
|
+
this.adapter = adapter;
|
|
5350
|
+
this.tenantId = tenantId;
|
|
5351
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
5352
|
+
if (!adapter.permissions) {
|
|
5353
|
+
throw new Error(
|
|
5354
|
+
"PermissionService requires a DatabaseAdapter with permissions repository. Make sure your adapter implements the permissions property."
|
|
5355
|
+
);
|
|
5356
|
+
}
|
|
5357
|
+
this.permissionsRepo = adapter.permissions;
|
|
5358
|
+
this.cacheTtlMs = options?.cacheTtlMs ?? 6e4;
|
|
5359
|
+
this.maxCacheSize = options?.maxCacheSize ?? 1e4;
|
|
5360
|
+
this.auditService = options?.auditService;
|
|
5361
|
+
this.userId = options?.userId;
|
|
5362
|
+
this.userEmail = options?.userEmail;
|
|
5363
|
+
}
|
|
5364
|
+
// ============================================================================
|
|
5365
|
+
// PERMISSION CHECKS
|
|
5366
|
+
// ============================================================================
|
|
5367
|
+
/**
|
|
5368
|
+
* Check if user can perform an action on an object.
|
|
5369
|
+
*
|
|
5370
|
+
* @param userProfileId - User profile ID to check
|
|
5371
|
+
* @param objectName - Object name to check access for
|
|
5372
|
+
* @param action - Action to check (read, create, update, delete)
|
|
5373
|
+
* @returns true if user has permission, false otherwise
|
|
5374
|
+
*/
|
|
5375
|
+
async canAccessObject(userProfileId, objectName, action) {
|
|
5376
|
+
const permissions = await this.getEffectivePermissions(userProfileId);
|
|
5377
|
+
if (permissions.isAdmin) {
|
|
5378
|
+
return true;
|
|
5379
|
+
}
|
|
5380
|
+
const wildcardPerms = permissions.objectPermissions["*"];
|
|
5381
|
+
if (wildcardPerms?.includes(action)) {
|
|
5382
|
+
return true;
|
|
5383
|
+
}
|
|
5384
|
+
const objectPerms = permissions.objectPermissions[objectName];
|
|
5385
|
+
return objectPerms?.includes(action) ?? false;
|
|
5386
|
+
}
|
|
5387
|
+
/**
|
|
5388
|
+
* Check if user can access an object, throw ForbiddenError if not.
|
|
5389
|
+
*
|
|
5390
|
+
* @param userProfileId - User profile ID to check
|
|
5391
|
+
* @param objectName - Object name to check access for
|
|
5392
|
+
* @param action - Action to check
|
|
5393
|
+
* @throws ForbiddenError if user lacks permission
|
|
5394
|
+
*/
|
|
5395
|
+
async checkObjectAccess(userProfileId, objectName, action) {
|
|
5396
|
+
const canAccess = await this.canAccessObject(userProfileId, objectName, action);
|
|
5397
|
+
if (!canAccess) {
|
|
5398
|
+
throw new ForbiddenError(objectName, action, userProfileId);
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
/**
|
|
5402
|
+
* Get permissions for a specific object as boolean flags.
|
|
5403
|
+
* Optimized to fetch permissions once and compute all flags.
|
|
5404
|
+
*
|
|
5405
|
+
* @param userProfileId - User profile ID to check
|
|
5406
|
+
* @param objectName - Object name to check
|
|
5407
|
+
* @returns Object with canRead, canCreate, canUpdate, canDelete flags
|
|
5408
|
+
*/
|
|
5409
|
+
async getObjectPermissions(userProfileId, objectName) {
|
|
5410
|
+
const permissions = await this.getEffectivePermissions(userProfileId);
|
|
5411
|
+
if (permissions.isAdmin) {
|
|
5412
|
+
return { canRead: true, canCreate: true, canUpdate: true, canDelete: true };
|
|
5413
|
+
}
|
|
5414
|
+
const wildcardPerms = permissions.objectPermissions["*"] ?? [];
|
|
5415
|
+
const objectPerms = permissions.objectPermissions[objectName] ?? [];
|
|
5416
|
+
const allPerms = /* @__PURE__ */ new Set([...wildcardPerms, ...objectPerms]);
|
|
5417
|
+
return {
|
|
5418
|
+
canRead: allPerms.has("read"),
|
|
5419
|
+
canCreate: allPerms.has("create"),
|
|
5420
|
+
canUpdate: allPerms.has("update"),
|
|
5421
|
+
canDelete: allPerms.has("delete")
|
|
5422
|
+
};
|
|
5423
|
+
}
|
|
5424
|
+
// ============================================================================
|
|
5425
|
+
// SYSTEM PERMISSION CHECKS
|
|
5426
|
+
// ============================================================================
|
|
5427
|
+
/**
|
|
5428
|
+
* Check if user can perform an action on a system resource.
|
|
5429
|
+
*
|
|
5430
|
+
* @param userProfileId - User profile ID to check
|
|
5431
|
+
* @param resource - System resource (users, schema, roles, settings)
|
|
5432
|
+
* @param action - Action to check (read, create, update, delete)
|
|
5433
|
+
* @returns true if user has permission, false otherwise
|
|
5434
|
+
*/
|
|
5435
|
+
async canAccessSystem(userProfileId, resource, action) {
|
|
5436
|
+
const permissions = await this.getEffectivePermissions(userProfileId);
|
|
5437
|
+
if (permissions.isAdmin) {
|
|
5438
|
+
return true;
|
|
5439
|
+
}
|
|
5440
|
+
const wildcardPerms = permissions.systemPermissions?.["*"];
|
|
5441
|
+
if (wildcardPerms?.includes(action)) {
|
|
5442
|
+
return true;
|
|
5443
|
+
}
|
|
5444
|
+
const resourcePerms = permissions.systemPermissions?.[resource];
|
|
5445
|
+
return resourcePerms?.includes(action) ?? false;
|
|
5446
|
+
}
|
|
5447
|
+
/**
|
|
5448
|
+
* Check if user can access a system resource, throw ForbiddenError if not.
|
|
5449
|
+
*
|
|
5450
|
+
* @param userProfileId - User profile ID to check
|
|
5451
|
+
* @param resource - System resource to check access for
|
|
5452
|
+
* @param action - Action to check
|
|
5453
|
+
* @throws ForbiddenError if user lacks permission
|
|
5454
|
+
*/
|
|
5455
|
+
async checkSystemAccess(userProfileId, resource, action) {
|
|
5456
|
+
const canAccess = await this.canAccessSystem(userProfileId, resource, action);
|
|
5457
|
+
if (!canAccess) {
|
|
5458
|
+
throw new ForbiddenError(`_${resource}`, action, userProfileId);
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
/**
|
|
5462
|
+
* Get permissions for a specific system resource as boolean flags.
|
|
5463
|
+
* Optimized to fetch permissions once and compute all flags.
|
|
5464
|
+
*
|
|
5465
|
+
* @param userProfileId - User profile ID to check
|
|
5466
|
+
* @param resource - System resource to check
|
|
5467
|
+
* @returns Object with canRead, canCreate, canUpdate, canDelete flags
|
|
5468
|
+
*/
|
|
5469
|
+
async getSystemPermissions(userProfileId, resource) {
|
|
5470
|
+
const permissions = await this.getEffectivePermissions(userProfileId);
|
|
5471
|
+
if (permissions.isAdmin) {
|
|
5472
|
+
return { canRead: true, canCreate: true, canUpdate: true, canDelete: true };
|
|
5473
|
+
}
|
|
5474
|
+
const wildcardPerms = permissions.systemPermissions?.["*"] ?? [];
|
|
5475
|
+
const resourcePerms = permissions.systemPermissions?.[resource] ?? [];
|
|
5476
|
+
const allPerms = /* @__PURE__ */ new Set([...wildcardPerms, ...resourcePerms]);
|
|
5477
|
+
return {
|
|
5478
|
+
canRead: allPerms.has("read"),
|
|
5479
|
+
canCreate: allPerms.has("create"),
|
|
5480
|
+
canUpdate: allPerms.has("update"),
|
|
5481
|
+
canDelete: allPerms.has("delete")
|
|
5482
|
+
};
|
|
5483
|
+
}
|
|
5484
|
+
// ============================================================================
|
|
5485
|
+
// EFFECTIVE PERMISSIONS
|
|
5486
|
+
// ============================================================================
|
|
5487
|
+
/**
|
|
5488
|
+
* Get effective permissions for a user.
|
|
5489
|
+
* Results are cached with TTL and size limit.
|
|
5490
|
+
*
|
|
5491
|
+
* @param userProfileId - User profile ID
|
|
5492
|
+
* @returns Merged permissions from all user's roles
|
|
5493
|
+
*/
|
|
5494
|
+
async getEffectivePermissions(userProfileId) {
|
|
5495
|
+
const cacheKey = this.getCacheKey(userProfileId);
|
|
5496
|
+
const cached = this.cache.get(cacheKey);
|
|
5497
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
5498
|
+
return cached.permissions;
|
|
5499
|
+
}
|
|
5500
|
+
const permissions = await this.permissionsRepo.getEffectivePermissions(
|
|
5501
|
+
userProfileId,
|
|
5502
|
+
this.tenantId
|
|
5503
|
+
);
|
|
5504
|
+
if (this.cache.size >= this.maxCacheSize) {
|
|
5505
|
+
const firstKey = this.cache.keys().next().value;
|
|
5506
|
+
if (firstKey) {
|
|
5507
|
+
this.cache.delete(firstKey);
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
this.cache.set(cacheKey, {
|
|
5511
|
+
permissions,
|
|
5512
|
+
expiresAt: Date.now() + this.cacheTtlMs
|
|
5513
|
+
});
|
|
5514
|
+
return permissions;
|
|
5515
|
+
}
|
|
5516
|
+
// ============================================================================
|
|
5517
|
+
// CACHE MANAGEMENT
|
|
5518
|
+
// ============================================================================
|
|
5519
|
+
/**
|
|
5520
|
+
* Invalidate cached permissions for a specific user.
|
|
5521
|
+
* Call this after role/permission changes.
|
|
5522
|
+
*/
|
|
5523
|
+
invalidateCache(userProfileId) {
|
|
5524
|
+
this.cache.delete(this.getCacheKey(userProfileId));
|
|
5525
|
+
}
|
|
5526
|
+
/**
|
|
5527
|
+
* Invalidate all cached permissions.
|
|
5528
|
+
* Call this after bulk role/permission changes.
|
|
5529
|
+
*/
|
|
5530
|
+
invalidateAllCache() {
|
|
5531
|
+
this.cache.clear();
|
|
5532
|
+
}
|
|
5533
|
+
/**
|
|
5534
|
+
* Get current cache size (for monitoring).
|
|
5535
|
+
*/
|
|
5536
|
+
getCacheSize() {
|
|
5537
|
+
return this.cache.size;
|
|
5538
|
+
}
|
|
5539
|
+
getCacheKey(userProfileId) {
|
|
5540
|
+
return `${this.tenantId}:${userProfileId}`;
|
|
5541
|
+
}
|
|
5542
|
+
// ============================================================================
|
|
5543
|
+
// ROLE MANAGEMENT
|
|
5544
|
+
// ============================================================================
|
|
5545
|
+
/**
|
|
5546
|
+
* Get all roles for the current tenant.
|
|
5547
|
+
*/
|
|
5548
|
+
async getRoles() {
|
|
5549
|
+
return await this.permissionsRepo.getRolesByTenant(this.tenantId);
|
|
5550
|
+
}
|
|
5551
|
+
/**
|
|
5552
|
+
* Get a role by ID.
|
|
5553
|
+
*/
|
|
5554
|
+
async getRoleById(roleId) {
|
|
5555
|
+
return await this.permissionsRepo.getRoleById(roleId);
|
|
5556
|
+
}
|
|
5557
|
+
/**
|
|
5558
|
+
* Create a new role.
|
|
5559
|
+
*/
|
|
5560
|
+
async createRole(input) {
|
|
5561
|
+
const role = await this.permissionsRepo.createRole({
|
|
5562
|
+
...input,
|
|
5563
|
+
tenantId: this.tenantId
|
|
5564
|
+
});
|
|
5565
|
+
if (this.auditService && this.userId) {
|
|
5566
|
+
await this.auditService.logRoleAction({
|
|
5567
|
+
action: "role.created",
|
|
5568
|
+
actorId: this.userId,
|
|
5569
|
+
actorEmail: this.userEmail,
|
|
5570
|
+
roleId: role.id,
|
|
5571
|
+
roleLabel: role.label
|
|
5572
|
+
});
|
|
5573
|
+
}
|
|
5574
|
+
return role;
|
|
5575
|
+
}
|
|
5576
|
+
/**
|
|
5577
|
+
* Update a role.
|
|
5578
|
+
*/
|
|
5579
|
+
async updateRole(roleId, updates) {
|
|
5580
|
+
const oldRole = await this.permissionsRepo.getRoleById(roleId);
|
|
5581
|
+
const role = await this.permissionsRepo.updateRole(roleId, updates);
|
|
5582
|
+
if (this.auditService && this.userId && oldRole) {
|
|
5583
|
+
const changes = [];
|
|
5584
|
+
if (updates.label !== void 0 && updates.label !== oldRole.label) {
|
|
5585
|
+
changes.push({ field: "label", oldValue: oldRole.label, newValue: updates.label });
|
|
5586
|
+
}
|
|
5587
|
+
if (updates.description !== void 0 && updates.description !== oldRole.description) {
|
|
5588
|
+
changes.push({
|
|
5589
|
+
field: "description",
|
|
5590
|
+
oldValue: oldRole.description,
|
|
5591
|
+
newValue: updates.description
|
|
5592
|
+
});
|
|
5593
|
+
}
|
|
5594
|
+
if (changes.length > 0) {
|
|
5595
|
+
await this.auditService.logRoleAction({
|
|
5596
|
+
action: "role.updated",
|
|
5597
|
+
actorId: this.userId,
|
|
5598
|
+
actorEmail: this.userEmail,
|
|
5599
|
+
roleId: role.id,
|
|
5600
|
+
roleLabel: role.label,
|
|
5601
|
+
changes
|
|
5602
|
+
});
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
return role;
|
|
5606
|
+
}
|
|
5607
|
+
/**
|
|
5608
|
+
* Delete a role.
|
|
5609
|
+
*/
|
|
5610
|
+
async deleteRole(roleId) {
|
|
5611
|
+
const role = await this.permissionsRepo.getRoleById(roleId);
|
|
5612
|
+
await this.permissionsRepo.deleteRole(roleId);
|
|
5613
|
+
this.invalidateAllCache();
|
|
5614
|
+
if (this.auditService && this.userId && role) {
|
|
5615
|
+
await this.auditService.logRoleAction({
|
|
5616
|
+
action: "role.deleted",
|
|
5617
|
+
actorId: this.userId,
|
|
5618
|
+
actorEmail: this.userEmail,
|
|
5619
|
+
roleId,
|
|
5620
|
+
roleLabel: role.label
|
|
5621
|
+
});
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5624
|
+
// ============================================================================
|
|
5625
|
+
// PERMISSION MANAGEMENT
|
|
5626
|
+
// ============================================================================
|
|
5627
|
+
/**
|
|
5628
|
+
* Get all permissions for a role.
|
|
5629
|
+
*/
|
|
5630
|
+
async getPermissionsByRole(roleId) {
|
|
5631
|
+
return await this.permissionsRepo.getPermissionsByRole(roleId);
|
|
5632
|
+
}
|
|
5633
|
+
/**
|
|
5634
|
+
* Set permissions for a role (replaces existing).
|
|
3729
5635
|
*/
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
5636
|
+
async setPermissions(roleId, permissions) {
|
|
5637
|
+
await this.permissionsRepo.setPermissions(roleId, permissions);
|
|
5638
|
+
this.invalidateAllCache();
|
|
5639
|
+
if (this.auditService && this.userId) {
|
|
5640
|
+
const role = await this.permissionsRepo.getRoleById(roleId);
|
|
5641
|
+
await this.auditService.logRoleAction({
|
|
5642
|
+
action: "role.updated",
|
|
5643
|
+
actorId: this.userId,
|
|
5644
|
+
actorEmail: this.userEmail,
|
|
5645
|
+
roleId,
|
|
5646
|
+
roleLabel: role?.label ?? roleId,
|
|
5647
|
+
metadata: { permissionsUpdated: true, permissionCount: permissions.length }
|
|
5648
|
+
});
|
|
3736
5649
|
}
|
|
3737
|
-
const systemAttributesFromDB = dbAttributes.filter((attr) => attr.system);
|
|
3738
|
-
const customAttributes = dbAttributes.filter((attr) => !attr.system);
|
|
3739
|
-
const dbAttrByName = new Map(systemAttributesFromDB.map((attr) => [attr.name, attr]));
|
|
3740
|
-
const systemAttrs = nativeObject.attributes.map((registryAttr) => {
|
|
3741
|
-
const dbAttr = dbAttrByName.get(registryAttr.name);
|
|
3742
|
-
if (dbAttr) {
|
|
3743
|
-
return {
|
|
3744
|
-
...registryAttr,
|
|
3745
|
-
id: dbAttr.id
|
|
3746
|
-
};
|
|
3747
|
-
}
|
|
3748
|
-
return registryAttr;
|
|
3749
|
-
});
|
|
3750
|
-
const customAttrs = customAttributes.map(
|
|
3751
|
-
(dbAttr) => this.convertDBAttributeToAttribute(dbAttr)
|
|
3752
|
-
);
|
|
3753
|
-
return {
|
|
3754
|
-
...nativeObject,
|
|
3755
|
-
id: dbObject.id,
|
|
3756
|
-
attributes: [
|
|
3757
|
-
...systemAttrs,
|
|
3758
|
-
// System attributes from registry (with DB IDs if synced)
|
|
3759
|
-
...customAttrs
|
|
3760
|
-
// Custom attributes from DB
|
|
3761
|
-
]
|
|
3762
|
-
};
|
|
3763
5650
|
}
|
|
5651
|
+
// ============================================================================
|
|
5652
|
+
// USER ROLE MANAGEMENT
|
|
5653
|
+
// ============================================================================
|
|
3764
5654
|
/**
|
|
3765
|
-
*
|
|
3766
|
-
* @internal
|
|
5655
|
+
* Get all roles assigned to a user.
|
|
3767
5656
|
*/
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
(dbAttr) => this.convertDBAttributeToAttribute(dbAttr)
|
|
3771
|
-
);
|
|
3772
|
-
return {
|
|
3773
|
-
id: dbObject.id,
|
|
3774
|
-
name: dbObject.name,
|
|
3775
|
-
label: dbObject.label,
|
|
3776
|
-
pluralLabel: dbObject.pluralLabel,
|
|
3777
|
-
description: dbObject.description,
|
|
3778
|
-
icon: dbObject.metadata?.icon,
|
|
3779
|
-
labelExpression: dbObject.labelExpression,
|
|
3780
|
-
attributes,
|
|
3781
|
-
system: dbObject.system,
|
|
3782
|
-
metadata: dbObject.metadata
|
|
3783
|
-
};
|
|
5657
|
+
async getUserRoles(userProfileId) {
|
|
5658
|
+
return await this.permissionsRepo.getUserRoles(userProfileId, this.tenantId);
|
|
3784
5659
|
}
|
|
3785
5660
|
/**
|
|
3786
|
-
*
|
|
3787
|
-
* Validates: name format, config schema, relation targets
|
|
3788
|
-
* @internal
|
|
5661
|
+
* Assign a role to a user.
|
|
3789
5662
|
*/
|
|
3790
|
-
async
|
|
3791
|
-
this.
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
5663
|
+
async assignRole(userProfileId, roleId, assignedBy) {
|
|
5664
|
+
const result = await this.permissionsRepo.assignRole({
|
|
5665
|
+
userProfileId,
|
|
5666
|
+
roleId,
|
|
5667
|
+
tenantId: this.tenantId,
|
|
5668
|
+
assignedBy
|
|
5669
|
+
});
|
|
5670
|
+
this.invalidateCache(userProfileId);
|
|
5671
|
+
if (this.auditService && this.userId) {
|
|
5672
|
+
const role = await this.permissionsRepo.getRoleById(roleId);
|
|
5673
|
+
await this.auditService.logRoleAction({
|
|
5674
|
+
action: "role.assigned",
|
|
5675
|
+
actorId: this.userId,
|
|
5676
|
+
actorEmail: this.userEmail,
|
|
5677
|
+
roleId,
|
|
5678
|
+
roleLabel: role?.label ?? roleId,
|
|
5679
|
+
targetUserId: userProfileId
|
|
5680
|
+
});
|
|
3795
5681
|
}
|
|
3796
|
-
return
|
|
5682
|
+
return result;
|
|
3797
5683
|
}
|
|
3798
5684
|
/**
|
|
3799
|
-
*
|
|
3800
|
-
* Uses Zod schema to validate and strip unknown properties
|
|
3801
|
-
* @internal
|
|
5685
|
+
* Revoke a role from a user.
|
|
3802
5686
|
*/
|
|
3803
|
-
|
|
3804
|
-
const
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
throw new Error(`Invalid config for ${attribute.type} attribute: ${error.message}`);
|
|
3817
|
-
}
|
|
3818
|
-
throw error;
|
|
5687
|
+
async revokeRole(userProfileId, roleId) {
|
|
5688
|
+
const role = await this.permissionsRepo.getRoleById(roleId);
|
|
5689
|
+
await this.permissionsRepo.revokeRole(userProfileId, roleId, this.tenantId);
|
|
5690
|
+
this.invalidateCache(userProfileId);
|
|
5691
|
+
if (this.auditService && this.userId) {
|
|
5692
|
+
await this.auditService.logRoleAction({
|
|
5693
|
+
action: "role.revoked",
|
|
5694
|
+
actorId: this.userId,
|
|
5695
|
+
actorEmail: this.userEmail,
|
|
5696
|
+
roleId,
|
|
5697
|
+
roleLabel: role?.label ?? roleId,
|
|
5698
|
+
targetUserId: userProfileId
|
|
5699
|
+
});
|
|
3819
5700
|
}
|
|
3820
5701
|
}
|
|
5702
|
+
// ============================================================================
|
|
5703
|
+
// INITIALIZATION
|
|
5704
|
+
// ============================================================================
|
|
3821
5705
|
/**
|
|
3822
|
-
*
|
|
3823
|
-
*
|
|
5706
|
+
* Initialize default roles for the tenant if they don't exist.
|
|
5707
|
+
*
|
|
5708
|
+
* Creates the following roles with their default permissions:
|
|
5709
|
+
* - **admin**: Full access to all system resources and objects
|
|
5710
|
+
* - **member**: No system access, can read/create/update objects (no delete)
|
|
5711
|
+
* - **guest**: No system access, read-only access to objects
|
|
5712
|
+
*
|
|
5713
|
+
* This method is idempotent - it only creates roles that don't already exist.
|
|
5714
|
+
* Should be called during tenant bootstrap or on first admin login.
|
|
5715
|
+
*
|
|
5716
|
+
* @example
|
|
5717
|
+
* ```typescript
|
|
5718
|
+
* const service = new PermissionService(adapter, tenantId);
|
|
5719
|
+
* await service.initializeDefaultRoles();
|
|
5720
|
+
* ```
|
|
3824
5721
|
*/
|
|
3825
|
-
async
|
|
3826
|
-
const
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
for (const
|
|
3835
|
-
if (
|
|
3836
|
-
|
|
5722
|
+
async initializeDefaultRoles() {
|
|
5723
|
+
const {
|
|
5724
|
+
DEFAULT_ROLES: DEFAULT_ROLES2,
|
|
5725
|
+
DEFAULT_ROLE_LABELS: DEFAULT_ROLE_LABELS2,
|
|
5726
|
+
DEFAULT_ROLE_DESCRIPTIONS: DEFAULT_ROLE_DESCRIPTIONS2,
|
|
5727
|
+
DEFAULT_ROLE_PERMISSIONS: DEFAULT_ROLE_PERMISSIONS2
|
|
5728
|
+
} = await Promise.resolve().then(() => (init_default_roles(), default_roles_exports));
|
|
5729
|
+
const existingRoles = await this.getRoles();
|
|
5730
|
+
const existingRoleNames = new Set(existingRoles.map((r) => r.name));
|
|
5731
|
+
for (const roleName of Object.values(DEFAULT_ROLES2)) {
|
|
5732
|
+
if (existingRoleNames.has(roleName)) {
|
|
5733
|
+
continue;
|
|
3837
5734
|
}
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
5735
|
+
const role = await this.permissionsRepo.createRole({
|
|
5736
|
+
tenantId: this.tenantId,
|
|
5737
|
+
name: roleName,
|
|
5738
|
+
label: DEFAULT_ROLE_LABELS2[roleName],
|
|
5739
|
+
description: DEFAULT_ROLE_DESCRIPTIONS2[roleName]
|
|
5740
|
+
});
|
|
5741
|
+
const permConfig = DEFAULT_ROLE_PERMISSIONS2[roleName];
|
|
5742
|
+
const permissionInputs = [];
|
|
5743
|
+
for (const perm of permConfig.system) {
|
|
5744
|
+
permissionInputs.push({
|
|
5745
|
+
scope: "system",
|
|
5746
|
+
target: perm.target,
|
|
5747
|
+
actions: perm.actions
|
|
5748
|
+
});
|
|
3841
5749
|
}
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
5750
|
+
for (const perm of permConfig.object) {
|
|
5751
|
+
permissionInputs.push({
|
|
5752
|
+
scope: "object",
|
|
5753
|
+
target: perm.target,
|
|
5754
|
+
actions: perm.actions
|
|
5755
|
+
});
|
|
5756
|
+
}
|
|
5757
|
+
if (permissionInputs.length > 0) {
|
|
5758
|
+
await this.setPermissions(role.id, permissionInputs);
|
|
3849
5759
|
}
|
|
3850
5760
|
}
|
|
3851
5761
|
}
|
|
3852
|
-
/**
|
|
3853
|
-
* Convert DB attribute to Attribute type
|
|
3854
|
-
* @internal
|
|
3855
|
-
*/
|
|
3856
|
-
convertDBAttributeToAttribute(dbAttr) {
|
|
3857
|
-
const baseAttr = {
|
|
3858
|
-
...dbAttr.config,
|
|
3859
|
-
id: dbAttr.id,
|
|
3860
|
-
name: dbAttr.name,
|
|
3861
|
-
label: dbAttr.label,
|
|
3862
|
-
type: dbAttr.type,
|
|
3863
|
-
required: dbAttr.required,
|
|
3864
|
-
system: dbAttr.system,
|
|
3865
|
-
unique: dbAttr.unique
|
|
3866
|
-
};
|
|
3867
|
-
return baseAttr;
|
|
3868
|
-
}
|
|
3869
5762
|
};
|
|
3870
5763
|
|
|
3871
5764
|
// src/runtime/services/relation.service.ts
|
|
@@ -4042,8 +5935,7 @@ var RelationService = class {
|
|
|
4042
5935
|
objectName: objectSchema.name,
|
|
4043
5936
|
objectLabel: objectSchema.label,
|
|
4044
5937
|
objectIcon: objectSchema.icon,
|
|
4045
|
-
label
|
|
4046
|
-
data: record.values
|
|
5938
|
+
label
|
|
4047
5939
|
});
|
|
4048
5940
|
}
|
|
4049
5941
|
}
|
|
@@ -4058,40 +5950,58 @@ var RelationService = class {
|
|
|
4058
5950
|
* Resolve record IDs to their display labels
|
|
4059
5951
|
* Useful for displaying current values in the UI
|
|
4060
5952
|
*
|
|
5953
|
+
* Uses batch fetching for performance - fetches all records in one query,
|
|
5954
|
+
* then groups by objectId to minimize schema lookups.
|
|
5955
|
+
*
|
|
4061
5956
|
* @param ids - Record IDs to resolve
|
|
4062
|
-
* @param
|
|
5957
|
+
* @param _tenantId - Tenant ID for multi-tenant isolation (unused but kept for API compatibility)
|
|
5958
|
+
* @param attributeId - Relation attribute ID to use its displayTemplate for label rendering
|
|
4063
5959
|
*
|
|
4064
5960
|
* @example
|
|
4065
5961
|
* ```typescript
|
|
4066
|
-
* const resolved = await relationService.resolveIds(["rec-1", "rec-2"], "tenant-1");
|
|
5962
|
+
* const resolved = await relationService.resolveIds(["rec-1", "rec-2"], "tenant-1", "attr-123");
|
|
4067
5963
|
* // [{ id: "rec-1", label: "Nike Air Max", objectName: "products", ... }]
|
|
4068
5964
|
* ```
|
|
4069
5965
|
*/
|
|
4070
|
-
async resolveIds(ids,
|
|
5966
|
+
async resolveIds(ids, _tenantId, attributeId) {
|
|
4071
5967
|
if (!ids || ids.length === 0) {
|
|
4072
5968
|
return [];
|
|
4073
5969
|
}
|
|
4074
|
-
const
|
|
5970
|
+
const attribute = await this.findAttributeById(attributeId);
|
|
5971
|
+
const records = await this.adapter.objectRecords.findByIds(ids);
|
|
5972
|
+
if (records.length === 0) {
|
|
5973
|
+
return [];
|
|
5974
|
+
}
|
|
5975
|
+
const recordsByObjectId = /* @__PURE__ */ new Map();
|
|
5976
|
+
for (const record of records) {
|
|
5977
|
+
const existing = recordsByObjectId.get(record.objectId) ?? [];
|
|
5978
|
+
existing.push(record);
|
|
5979
|
+
recordsByObjectId.set(record.objectId, existing);
|
|
5980
|
+
}
|
|
4075
5981
|
const resolved = [];
|
|
4076
|
-
for (const
|
|
4077
|
-
const
|
|
4078
|
-
if (!record) {
|
|
4079
|
-
continue;
|
|
4080
|
-
}
|
|
4081
|
-
const objectSchema = await this.schemaService.getObjectSchema(record.objectId);
|
|
5982
|
+
for (const [objectId, objectRecords] of recordsByObjectId) {
|
|
5983
|
+
const objectSchema = await this.schemaService.getObjectSchema(objectId);
|
|
4082
5984
|
if (!objectSchema) {
|
|
4083
5985
|
continue;
|
|
4084
5986
|
}
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
5987
|
+
let template = objectSchema.labelExpression;
|
|
5988
|
+
if (attribute?.targets) {
|
|
5989
|
+
const targetConfig = attribute.targets.find((t) => t.object === objectSchema.name);
|
|
5990
|
+
if (targetConfig?.displayTemplate) {
|
|
5991
|
+
template = targetConfig.displayTemplate;
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
for (const record of objectRecords) {
|
|
5995
|
+
const label = renderLabelExpression(template, record.values);
|
|
5996
|
+
resolved.push({
|
|
5997
|
+
id: record.id,
|
|
5998
|
+
objectId: record.objectId,
|
|
5999
|
+
objectName: objectSchema.name,
|
|
6000
|
+
objectLabel: objectSchema.label,
|
|
6001
|
+
objectIcon: objectSchema.icon,
|
|
6002
|
+
label
|
|
6003
|
+
});
|
|
6004
|
+
}
|
|
4095
6005
|
}
|
|
4096
6006
|
return resolved;
|
|
4097
6007
|
}
|
|
@@ -4118,6 +6028,70 @@ var RecordService = class {
|
|
|
4118
6028
|
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
4119
6029
|
this.relationService = new RelationService(adapter, registry);
|
|
4120
6030
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
6031
|
+
this.permissionService = options?.permissionService;
|
|
6032
|
+
this.auditService = options?.auditService;
|
|
6033
|
+
this.userId = options?.userId;
|
|
6034
|
+
this.userEmail = options?.userEmail;
|
|
6035
|
+
}
|
|
6036
|
+
/**
|
|
6037
|
+
* Check permission for an action on an object.
|
|
6038
|
+
* Only checks if permissionService and userId are configured.
|
|
6039
|
+
* @internal
|
|
6040
|
+
*/
|
|
6041
|
+
async checkPermission(objectName, action) {
|
|
6042
|
+
if (this.permissionService && this.userId) {
|
|
6043
|
+
await this.permissionService.checkObjectAccess(this.userId, objectName, action);
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
/**
|
|
6047
|
+
* Resolve relation IDs to their display labels
|
|
6048
|
+
* @internal
|
|
6049
|
+
*/
|
|
6050
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complexity is ok here
|
|
6051
|
+
async resolveRelationLabels(relationAttrs, values) {
|
|
6052
|
+
const resolvedMap = /* @__PURE__ */ new Map();
|
|
6053
|
+
const idsWithAttrId = [];
|
|
6054
|
+
const idsWithoutAttrId = [];
|
|
6055
|
+
for (const attr of relationAttrs) {
|
|
6056
|
+
const val = values[attr.name];
|
|
6057
|
+
const ids = this.extractRelationIds(val);
|
|
6058
|
+
if (ids.length > 0) {
|
|
6059
|
+
if (attr.id) {
|
|
6060
|
+
idsWithAttrId.push({ attrId: attr.id, ids });
|
|
6061
|
+
} else {
|
|
6062
|
+
idsWithoutAttrId.push(...ids);
|
|
6063
|
+
}
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
6066
|
+
await Promise.all(
|
|
6067
|
+
idsWithAttrId.map(async ({ attrId, ids }) => {
|
|
6068
|
+
const resolved = await this.relationService.resolveIds(ids, this.tenantId, attrId);
|
|
6069
|
+
for (const r of resolved) {
|
|
6070
|
+
resolvedMap.set(r.id, r.label);
|
|
6071
|
+
}
|
|
6072
|
+
})
|
|
6073
|
+
);
|
|
6074
|
+
if (idsWithoutAttrId.length > 0) {
|
|
6075
|
+
const uniqueIds = [...new Set(idsWithoutAttrId)].filter((id) => !resolvedMap.has(id));
|
|
6076
|
+
if (uniqueIds.length > 0) {
|
|
6077
|
+
const records = await this.adapter.objectRecords.findByIds(uniqueIds);
|
|
6078
|
+
for (const record of records) {
|
|
6079
|
+
if (record.label) {
|
|
6080
|
+
resolvedMap.set(record.id, record.label);
|
|
6081
|
+
}
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
return resolvedMap;
|
|
6086
|
+
}
|
|
6087
|
+
/**
|
|
6088
|
+
* Extract relation IDs from a value (string or array)
|
|
6089
|
+
* @internal
|
|
6090
|
+
*/
|
|
6091
|
+
extractRelationIds(val) {
|
|
6092
|
+
if (typeof val === "string") return [val];
|
|
6093
|
+
if (Array.isArray(val) && typeof val[0] === "string") return [val[0]];
|
|
6094
|
+
return [];
|
|
4121
6095
|
}
|
|
4122
6096
|
/**
|
|
4123
6097
|
* Compute display label from schema expression
|
|
@@ -4132,27 +6106,16 @@ var RecordService = class {
|
|
|
4132
6106
|
if (relationAttrs.length === 0) {
|
|
4133
6107
|
return renderLabelExpression(schema.labelExpression, values);
|
|
4134
6108
|
}
|
|
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) {
|
|
6109
|
+
const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
|
|
6110
|
+
if (resolvedMap.size === 0) {
|
|
4145
6111
|
return renderLabelExpression(schema.labelExpression, values);
|
|
4146
6112
|
}
|
|
4147
|
-
const resolved = await this.relationService.resolveIds(idsToResolve, this.tenantId);
|
|
4148
|
-
const resolvedMap = new Map(resolved.map((r) => [r.id, r.label]));
|
|
4149
6113
|
const enrichedValues = { ...values };
|
|
4150
6114
|
for (const attr of relationAttrs) {
|
|
4151
6115
|
const val = values[attr.name];
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
enrichedValues[attr.name] = resolvedMap.get(val[0]);
|
|
6116
|
+
const ids = this.extractRelationIds(val);
|
|
6117
|
+
if (ids.length > 0 && resolvedMap.has(ids[0])) {
|
|
6118
|
+
enrichedValues[attr.name] = resolvedMap.get(ids[0]);
|
|
4156
6119
|
}
|
|
4157
6120
|
}
|
|
4158
6121
|
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
@@ -4188,6 +6151,7 @@ var RecordService = class {
|
|
|
4188
6151
|
*/
|
|
4189
6152
|
async createRecord(objectId, data, options) {
|
|
4190
6153
|
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
6154
|
+
await this.checkPermission(schema.name, "create");
|
|
4191
6155
|
const hookCtx = this.buildCreateHookContext(schema, data, options?.hookMetadata);
|
|
4192
6156
|
if (!options?.skipHooks) {
|
|
4193
6157
|
await this.hookRegistry.execute("beforeCreate", schema.name, hookCtx);
|
|
@@ -4219,6 +6183,18 @@ var RecordService = class {
|
|
|
4219
6183
|
};
|
|
4220
6184
|
await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
|
|
4221
6185
|
}
|
|
6186
|
+
if (this.auditService && this.userId) {
|
|
6187
|
+
await this.auditService.logRecordAction({
|
|
6188
|
+
action: "record.created",
|
|
6189
|
+
actorId: this.userId,
|
|
6190
|
+
actorEmail: this.userEmail,
|
|
6191
|
+
objectName: schema.name,
|
|
6192
|
+
objectId: schema.id,
|
|
6193
|
+
recordId: record.id,
|
|
6194
|
+
recordLabel: record.label,
|
|
6195
|
+
metadata: options?.hookMetadata
|
|
6196
|
+
});
|
|
6197
|
+
}
|
|
4222
6198
|
return record;
|
|
4223
6199
|
}
|
|
4224
6200
|
/**
|
|
@@ -4277,6 +6253,7 @@ var RecordService = class {
|
|
|
4277
6253
|
async updateRecord(recordId, data, options) {
|
|
4278
6254
|
const existing = await this.getRecordOrThrow(recordId);
|
|
4279
6255
|
const schema = await this.schemaService.getObjectSchema(existing.objectId);
|
|
6256
|
+
await this.checkPermission(schema.name, "update");
|
|
4280
6257
|
const mergedData = { ...existing.values, ...data };
|
|
4281
6258
|
const changedAttributes = Object.keys(data).filter((key) => existing.values[key] !== data[key]);
|
|
4282
6259
|
const hookCtx = this.buildHookContext(
|
|
@@ -4314,6 +6291,24 @@ var RecordService = class {
|
|
|
4314
6291
|
};
|
|
4315
6292
|
await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
|
|
4316
6293
|
}
|
|
6294
|
+
if (this.auditService && this.userId && changedAttributes.length > 0) {
|
|
6295
|
+
const changes = changedAttributes.map((attr) => ({
|
|
6296
|
+
field: attr,
|
|
6297
|
+
oldValue: hookCtx.oldValues?.[attr],
|
|
6298
|
+
newValue: data[attr]
|
|
6299
|
+
}));
|
|
6300
|
+
await this.auditService.logRecordAction({
|
|
6301
|
+
action: "record.updated",
|
|
6302
|
+
actorId: this.userId,
|
|
6303
|
+
actorEmail: this.userEmail,
|
|
6304
|
+
objectName: schema.name,
|
|
6305
|
+
objectId: schema.id,
|
|
6306
|
+
recordId: updated.id,
|
|
6307
|
+
recordLabel: updated.label,
|
|
6308
|
+
changes,
|
|
6309
|
+
metadata: options?.hookMetadata
|
|
6310
|
+
});
|
|
6311
|
+
}
|
|
4317
6312
|
return updated;
|
|
4318
6313
|
}
|
|
4319
6314
|
/**
|
|
@@ -4403,11 +6398,18 @@ var RecordService = class {
|
|
|
4403
6398
|
async deleteRecord(recordId, options) {
|
|
4404
6399
|
const record = await this.getRecordOrThrow(recordId);
|
|
4405
6400
|
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
6401
|
+
await this.checkPermission(schema.name, "delete");
|
|
4406
6402
|
if (options?.checkSystem) {
|
|
4407
6403
|
if (schema.system) {
|
|
4408
6404
|
throw new Error(`Cannot delete record of system object "${schema.label}"`);
|
|
4409
6405
|
}
|
|
4410
6406
|
}
|
|
6407
|
+
if (!options?.skipReferenceCheck) {
|
|
6408
|
+
const references = await this.adapter.objectRecords.countRecordsReferencingId(recordId);
|
|
6409
|
+
if (references.length > 0) {
|
|
6410
|
+
throw new RecordReferencedError(recordId, references);
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
4411
6413
|
const hookCtx = this.buildDeleteHookContext(schema, record, options?.hookMetadata);
|
|
4412
6414
|
if (!options?.skipHooks) {
|
|
4413
6415
|
await this.hookRegistry.execute("beforeDelete", schema.name, hookCtx);
|
|
@@ -4416,6 +6418,110 @@ var RecordService = class {
|
|
|
4416
6418
|
if (!options?.skipHooks) {
|
|
4417
6419
|
await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
|
|
4418
6420
|
}
|
|
6421
|
+
if (this.auditService && this.userId) {
|
|
6422
|
+
await this.auditService.logRecordAction({
|
|
6423
|
+
action: "record.deleted",
|
|
6424
|
+
actorId: this.userId,
|
|
6425
|
+
actorEmail: this.userEmail,
|
|
6426
|
+
objectName: schema.name,
|
|
6427
|
+
objectId: schema.id,
|
|
6428
|
+
recordId: record.id,
|
|
6429
|
+
recordLabel: record.label,
|
|
6430
|
+
metadata: options?.hookMetadata
|
|
6431
|
+
});
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
/**
|
|
6435
|
+
* Restore a soft-deleted record
|
|
6436
|
+
*
|
|
6437
|
+
* Triggers beforeRestore and afterRestore hooks if a HookRegistry is configured.
|
|
6438
|
+
*
|
|
6439
|
+
* @param recordId - Record UUID
|
|
6440
|
+
* @param options - Restore options
|
|
6441
|
+
* @returns Restored record
|
|
6442
|
+
*
|
|
6443
|
+
* @example
|
|
6444
|
+
* ```typescript
|
|
6445
|
+
* // Restore a deleted record
|
|
6446
|
+
* const restored = await service.restoreRecord("rec-123");
|
|
6447
|
+
* console.log(restored.deletedAt); // null
|
|
6448
|
+
* ```
|
|
6449
|
+
*/
|
|
6450
|
+
async restoreRecord(recordId, options) {
|
|
6451
|
+
const record = await this.getRecordOrThrow(recordId);
|
|
6452
|
+
if (!record.deletedAt) {
|
|
6453
|
+
throw new Error(`Record "${recordId}" is not deleted`);
|
|
6454
|
+
}
|
|
6455
|
+
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
6456
|
+
await this.checkPermission(schema.name, "update");
|
|
6457
|
+
const hookCtx = this.buildRestoreHookContext(schema, record, options?.hookMetadata);
|
|
6458
|
+
if (!options?.skipHooks) {
|
|
6459
|
+
await this.hookRegistry.execute("beforeRestore", schema.name, hookCtx);
|
|
6460
|
+
}
|
|
6461
|
+
const restored = await this.adapter.objectRecords.restore(recordId);
|
|
6462
|
+
if (!options?.skipHooks) {
|
|
6463
|
+
const afterCtx = {
|
|
6464
|
+
...hookCtx,
|
|
6465
|
+
record: restored
|
|
6466
|
+
};
|
|
6467
|
+
await this.hookRegistry.execute("afterRestore", schema.name, afterCtx);
|
|
6468
|
+
}
|
|
6469
|
+
if (this.auditService && this.userId) {
|
|
6470
|
+
await this.auditService.logRecordAction({
|
|
6471
|
+
action: "record.restored",
|
|
6472
|
+
actorId: this.userId,
|
|
6473
|
+
actorEmail: this.userEmail,
|
|
6474
|
+
objectName: schema.name,
|
|
6475
|
+
objectId: schema.id,
|
|
6476
|
+
recordId: restored.id,
|
|
6477
|
+
recordLabel: restored.label,
|
|
6478
|
+
metadata: options?.hookMetadata
|
|
6479
|
+
});
|
|
6480
|
+
}
|
|
6481
|
+
return restored;
|
|
6482
|
+
}
|
|
6483
|
+
/**
|
|
6484
|
+
* Build hook context for restore operations
|
|
6485
|
+
* @internal
|
|
6486
|
+
*/
|
|
6487
|
+
buildRestoreHookContext(schema, record, metadata) {
|
|
6488
|
+
return {
|
|
6489
|
+
objectId: schema.id,
|
|
6490
|
+
objectName: schema.name,
|
|
6491
|
+
recordId: record.id,
|
|
6492
|
+
tenantId: this.tenantId,
|
|
6493
|
+
record,
|
|
6494
|
+
oldValues: record.values,
|
|
6495
|
+
newValues: record.values,
|
|
6496
|
+
changedAttributes: [],
|
|
6497
|
+
getChange: (attr) => ({
|
|
6498
|
+
oldValue: record.values[attr],
|
|
6499
|
+
newValue: record.values[attr],
|
|
6500
|
+
changed: false
|
|
6501
|
+
}),
|
|
6502
|
+
metadata: metadata ?? {},
|
|
6503
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
6504
|
+
};
|
|
6505
|
+
}
|
|
6506
|
+
/**
|
|
6507
|
+
* Permanently delete a record (hard delete)
|
|
6508
|
+
*
|
|
6509
|
+
* This cannot be undone. Use with caution - prefer soft delete for data safety.
|
|
6510
|
+
* Does NOT trigger delete hooks (already triggered on soft delete).
|
|
6511
|
+
*
|
|
6512
|
+
* @param recordId - Record UUID
|
|
6513
|
+
*
|
|
6514
|
+
* @example
|
|
6515
|
+
* ```typescript
|
|
6516
|
+
* // Permanently delete a record
|
|
6517
|
+
* await service.hardDeleteRecord("rec-123");
|
|
6518
|
+
* ```
|
|
6519
|
+
*/
|
|
6520
|
+
async hardDeleteRecord(recordId) {
|
|
6521
|
+
const record = await this.getRecordOrThrow(recordId);
|
|
6522
|
+
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
6523
|
+
await this.checkPermission(schema.name, "delete");
|
|
6524
|
+
await this.adapter.objectRecords.hardDelete(recordId);
|
|
4419
6525
|
}
|
|
4420
6526
|
/**
|
|
4421
6527
|
* List records for an object with pagination
|
|
@@ -4425,6 +6531,10 @@ var RecordService = class {
|
|
|
4425
6531
|
* @returns Records and total count
|
|
4426
6532
|
*/
|
|
4427
6533
|
async listRecords(objectId, options) {
|
|
6534
|
+
if (this.permissionService && this.userId) {
|
|
6535
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
6536
|
+
await this.checkPermission(schema.name, "read");
|
|
6537
|
+
}
|
|
4428
6538
|
return await this.adapter.objectRecords.list(this.tenantId, objectId, options);
|
|
4429
6539
|
}
|
|
4430
6540
|
/**
|
|
@@ -4436,6 +6546,10 @@ var RecordService = class {
|
|
|
4436
6546
|
* @returns Matching records and total count
|
|
4437
6547
|
*/
|
|
4438
6548
|
async searchRecords(objectId, query, options) {
|
|
6549
|
+
if (this.permissionService && this.userId) {
|
|
6550
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
6551
|
+
await this.checkPermission(schema.name, "read");
|
|
6552
|
+
}
|
|
4439
6553
|
return await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
|
|
4440
6554
|
}
|
|
4441
6555
|
/**
|
|
@@ -4483,9 +6597,12 @@ var RecordService = class {
|
|
|
4483
6597
|
|
|
4484
6598
|
// src/runtime/services/user-profile.service.ts
|
|
4485
6599
|
var UserProfileService = class {
|
|
4486
|
-
constructor(adapter, tenantId) {
|
|
6600
|
+
constructor(adapter, tenantId, options) {
|
|
4487
6601
|
this.adapter = adapter;
|
|
4488
6602
|
this.tenantId = tenantId;
|
|
6603
|
+
this.auditService = options?.auditService;
|
|
6604
|
+
this.userId = options?.userId;
|
|
6605
|
+
this.userEmail = options?.userEmail;
|
|
4489
6606
|
}
|
|
4490
6607
|
/**
|
|
4491
6608
|
* Create a new user profile (typically after first auth)
|
|
@@ -4523,7 +6640,17 @@ var UserProfileService = class {
|
|
|
4523
6640
|
if (existingEmail) {
|
|
4524
6641
|
throw new Error(`User profile with email "${data.email}" already exists in this tenant`);
|
|
4525
6642
|
}
|
|
4526
|
-
|
|
6643
|
+
const profile = await this.adapter.userProfiles.create(data);
|
|
6644
|
+
if (this.auditService && this.userId) {
|
|
6645
|
+
await this.auditService.logUserAction({
|
|
6646
|
+
action: "user.created",
|
|
6647
|
+
actorId: this.userId,
|
|
6648
|
+
actorEmail: this.userEmail,
|
|
6649
|
+
targetUserId: profile.id,
|
|
6650
|
+
targetUserEmail: profile.email
|
|
6651
|
+
});
|
|
6652
|
+
}
|
|
6653
|
+
return profile;
|
|
4527
6654
|
}
|
|
4528
6655
|
/**
|
|
4529
6656
|
* Get user profile by ID
|
|
@@ -4584,8 +6711,27 @@ var UserProfileService = class {
|
|
|
4584
6711
|
* Update user profile
|
|
4585
6712
|
*/
|
|
4586
6713
|
async updateProfile(profileId, data) {
|
|
4587
|
-
await this.getProfileOrThrow(profileId);
|
|
4588
|
-
|
|
6714
|
+
const existing = await this.getProfileOrThrow(profileId);
|
|
6715
|
+
const updated = await this.adapter.userProfiles.update(profileId, data);
|
|
6716
|
+
if (this.auditService && this.userId) {
|
|
6717
|
+
const changes = buildAuditChanges(existing, data, [
|
|
6718
|
+
"firstName",
|
|
6719
|
+
"lastName",
|
|
6720
|
+
"role",
|
|
6721
|
+
"status"
|
|
6722
|
+
]);
|
|
6723
|
+
if (changes.length > 0) {
|
|
6724
|
+
await this.auditService.logUserAction({
|
|
6725
|
+
action: "user.updated",
|
|
6726
|
+
actorId: this.userId,
|
|
6727
|
+
actorEmail: this.userEmail,
|
|
6728
|
+
targetUserId: profileId,
|
|
6729
|
+
targetUserEmail: updated.email,
|
|
6730
|
+
changes
|
|
6731
|
+
});
|
|
6732
|
+
}
|
|
6733
|
+
}
|
|
6734
|
+
return updated;
|
|
4589
6735
|
}
|
|
4590
6736
|
/**
|
|
4591
6737
|
* Delete user profile
|
|
@@ -4594,17 +6740,25 @@ var UserProfileService = class {
|
|
|
4594
6740
|
* @param options - Delete options
|
|
4595
6741
|
*/
|
|
4596
6742
|
async deleteProfile(profileId, options) {
|
|
6743
|
+
const profile = await this.getProfileOrThrow(profileId);
|
|
4597
6744
|
if (options?.checkAdmin) {
|
|
4598
|
-
const profile = await this.getProfileOrThrow(profileId);
|
|
4599
6745
|
if (profile.role === "admin") {
|
|
4600
|
-
const
|
|
4601
|
-
const adminCount = allProfiles.filter((p) => p.role === "admin").length;
|
|
6746
|
+
const adminCount = await this.adapter.userProfiles.countByRole(this.tenantId, "admin");
|
|
4602
6747
|
if (adminCount <= 1) {
|
|
4603
6748
|
throw new Error("Cannot delete the last admin user");
|
|
4604
6749
|
}
|
|
4605
6750
|
}
|
|
4606
6751
|
}
|
|
4607
6752
|
await this.adapter.userProfiles.delete(profileId);
|
|
6753
|
+
if (this.auditService && this.userId) {
|
|
6754
|
+
await this.auditService.logUserAction({
|
|
6755
|
+
action: "user.deleted",
|
|
6756
|
+
actorId: this.userId,
|
|
6757
|
+
actorEmail: this.userEmail,
|
|
6758
|
+
targetUserId: profileId,
|
|
6759
|
+
targetUserEmail: profile.email
|
|
6760
|
+
});
|
|
6761
|
+
}
|
|
4608
6762
|
}
|
|
4609
6763
|
/**
|
|
4610
6764
|
* List all user profiles for the tenant
|
|
@@ -4741,9 +6895,6 @@ var ViewService = class {
|
|
|
4741
6895
|
`Cannot create view "${input.name}": a system view with this name already exists`
|
|
4742
6896
|
);
|
|
4743
6897
|
}
|
|
4744
|
-
if (!input.tabs || input.tabs.length === 0) {
|
|
4745
|
-
throw new Error("View must have at least one tab");
|
|
4746
|
-
}
|
|
4747
6898
|
const dbView = await this.adapter.views.create({
|
|
4748
6899
|
tenantId,
|
|
4749
6900
|
objectName: input.objectName,
|
|
@@ -4751,7 +6902,7 @@ var ViewService = class {
|
|
|
4751
6902
|
label: input.label,
|
|
4752
6903
|
description: input.description,
|
|
4753
6904
|
icon: input.icon,
|
|
4754
|
-
tabs: input.tabs,
|
|
6905
|
+
tabs: input.tabs ?? [],
|
|
4755
6906
|
default: input.default ?? false,
|
|
4756
6907
|
system: false,
|
|
4757
6908
|
// Custom views are never system
|
|
@@ -4774,9 +6925,6 @@ var ViewService = class {
|
|
|
4774
6925
|
if (dbView.system) {
|
|
4775
6926
|
throw new Error("Cannot modify system views. System views are protected.");
|
|
4776
6927
|
}
|
|
4777
|
-
if (input.tabs && input.tabs.length === 0) {
|
|
4778
|
-
throw new Error("View must have at least one tab");
|
|
4779
|
-
}
|
|
4780
6928
|
const updated = await this.adapter.views.update(viewId, {
|
|
4781
6929
|
label: input.label,
|
|
4782
6930
|
description: input.description,
|
|
@@ -5191,12 +7339,24 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
5191
7339
|
}
|
|
5192
7340
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5193
7341
|
0 && (module.exports = {
|
|
7342
|
+
ALL_SYSTEM_RESOURCES,
|
|
7343
|
+
AttributeInUseError,
|
|
5194
7344
|
AttributeNotFoundError,
|
|
7345
|
+
AuditService,
|
|
5195
7346
|
CustomTabConfig,
|
|
5196
7347
|
DEFAULT_LABEL_FALLBACK,
|
|
7348
|
+
DEFAULT_ROLES,
|
|
7349
|
+
DEFAULT_ROLE_DESCRIPTIONS,
|
|
7350
|
+
DEFAULT_ROLE_LABELS,
|
|
7351
|
+
DEFAULT_ROLE_PERMISSIONS,
|
|
5197
7352
|
DuplicateError,
|
|
5198
7353
|
FileNotFoundError,
|
|
5199
7354
|
FileService,
|
|
7355
|
+
FlowBuilder,
|
|
7356
|
+
FlowPageBuilder,
|
|
7357
|
+
FlowRowBuilder,
|
|
7358
|
+
FlowService,
|
|
7359
|
+
ForbiddenError,
|
|
5200
7360
|
GeocodingService,
|
|
5201
7361
|
GlobalSearchService,
|
|
5202
7362
|
GroupBuilder,
|
|
@@ -5208,11 +7368,18 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
5208
7368
|
OPERATORS_BY_TYPE,
|
|
5209
7369
|
ObjectBuilder,
|
|
5210
7370
|
ObjectNotFoundError,
|
|
7371
|
+
ObjectReferencedError,
|
|
5211
7372
|
ObjectSchemaService,
|
|
7373
|
+
PermissionService,
|
|
5212
7374
|
ProtectedResourceError,
|
|
7375
|
+
ProtectedRoleError,
|
|
5213
7376
|
RecordNotFoundError,
|
|
7377
|
+
RecordReferencedError,
|
|
5214
7378
|
RecordService,
|
|
5215
7379
|
RelationService,
|
|
7380
|
+
RoleNotFoundError,
|
|
7381
|
+
SYSTEM_RESOURCES,
|
|
7382
|
+
SYSTEM_RESOURCE_LABELS,
|
|
5216
7383
|
SchemaError,
|
|
5217
7384
|
SchemaErrorCode,
|
|
5218
7385
|
SyncError,
|
|
@@ -5224,6 +7391,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
5224
7391
|
ViewBuilder,
|
|
5225
7392
|
ViewService,
|
|
5226
7393
|
attributeConfigSchemas,
|
|
7394
|
+
buildAuditChanges,
|
|
5227
7395
|
checkbox,
|
|
5228
7396
|
checkboxConfigSchema,
|
|
5229
7397
|
computeRecordStatus,
|
|
@@ -5255,6 +7423,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
5255
7423
|
extractAttributeNames,
|
|
5256
7424
|
file,
|
|
5257
7425
|
fileConfigSchema,
|
|
7426
|
+
flow,
|
|
5258
7427
|
generateId,
|
|
5259
7428
|
generatePrefixedId,
|
|
5260
7429
|
getAttributeConfigSchema,
|
|
@@ -5264,6 +7433,10 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
5264
7433
|
group,
|
|
5265
7434
|
isAdvancedFilterState,
|
|
5266
7435
|
isCustomTab,
|
|
7436
|
+
isDefaultRole,
|
|
7437
|
+
isFlowDefinition,
|
|
7438
|
+
isFlowPublished,
|
|
7439
|
+
isForbiddenError,
|
|
5267
7440
|
isFormTab,
|
|
5268
7441
|
isLabelExpression,
|
|
5269
7442
|
isNoValueOperator,
|
|
@@ -5271,6 +7444,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
5271
7444
|
isProtectedResourceError,
|
|
5272
7445
|
isRecordComplete,
|
|
5273
7446
|
isSchemaError,
|
|
7447
|
+
isSystemFlow,
|
|
5274
7448
|
isTableTab,
|
|
5275
7449
|
isValidationError,
|
|
5276
7450
|
location,
|