@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.
Files changed (72) hide show
  1. package/dist/chunk-2ASXJITY.mjs +5360 -0
  2. package/dist/chunk-5AWYVQKR.mjs +5310 -0
  3. package/dist/chunk-65HUQZHK.mjs +5374 -0
  4. package/dist/chunk-73P4F2OU.mjs +5809 -0
  5. package/dist/chunk-BHKKVBPP.mjs +5527 -0
  6. package/dist/chunk-BTVPMGYW.mjs +6327 -0
  7. package/dist/chunk-BUWVNOSK.mjs +5457 -0
  8. package/dist/chunk-C7HZ55GJ.mjs +6882 -0
  9. package/dist/chunk-CCEHW4E7.mjs +6071 -0
  10. package/dist/chunk-CNR3BZLU.mjs +5163 -0
  11. package/dist/chunk-DLMVX6DN.mjs +6074 -0
  12. package/dist/chunk-DUUORFHI.mjs +5379 -0
  13. package/dist/chunk-DWTD7YPJ.mjs +74 -0
  14. package/dist/chunk-FAR5ENGR.mjs +78 -0
  15. package/dist/chunk-ITWOVZOW.mjs +5809 -0
  16. package/dist/chunk-JDUTFH3J.mjs +6940 -0
  17. package/dist/chunk-KJLKY5X7.mjs +6314 -0
  18. package/dist/chunk-KTJXMGBR.mjs +5397 -0
  19. package/dist/chunk-NOVHQ7B2.mjs +6327 -0
  20. package/dist/chunk-PMFQ4ZL4.mjs +6230 -0
  21. package/dist/chunk-QAGN53HE.mjs +6327 -0
  22. package/dist/chunk-QC47SMCZ.mjs +5467 -0
  23. package/dist/chunk-UUOAVXAI.mjs +6327 -0
  24. package/dist/chunk-VYZ3BOSB.mjs +6946 -0
  25. package/dist/chunk-XAX75NSP.mjs +6321 -0
  26. package/dist/chunk-Z6LIJ5PO.mjs +5362 -0
  27. package/dist/chunk-ZG3PT3RL.mjs +6944 -0
  28. package/dist/default-roles-G4TOWUVM.mjs +20 -0
  29. package/dist/default-roles-QEZPRSUL.mjs +20 -0
  30. package/dist/index.d.mts +419 -16
  31. package/dist/index.d.ts +419 -16
  32. package/dist/index.js +2580 -406
  33. package/dist/index.mjs +80 -163
  34. package/dist/runtime-B1_syHVj.d.mts +4787 -0
  35. package/dist/runtime-B1_syHVj.d.ts +4787 -0
  36. package/dist/runtime-B7N-oycb.d.mts +4854 -0
  37. package/dist/runtime-B7N-oycb.d.ts +4854 -0
  38. package/dist/runtime-BDoAIuTe.d.mts +4872 -0
  39. package/dist/runtime-BDoAIuTe.d.ts +4872 -0
  40. package/dist/runtime-BPYJWZfC.d.mts +5130 -0
  41. package/dist/runtime-BPYJWZfC.d.ts +5130 -0
  42. package/dist/runtime-BVRmzg3d.d.mts +4718 -0
  43. package/dist/runtime-BVRmzg3d.d.ts +4718 -0
  44. package/dist/runtime-BrXwIidf.d.mts +4794 -0
  45. package/dist/runtime-BrXwIidf.d.ts +4794 -0
  46. package/dist/runtime-CC_HZhVN.d.mts +5224 -0
  47. package/dist/runtime-CC_HZhVN.d.ts +5224 -0
  48. package/dist/runtime-CEx57sBZ.d.mts +4791 -0
  49. package/dist/runtime-CEx57sBZ.d.ts +4791 -0
  50. package/dist/runtime-COGvuXIq.d.mts +4790 -0
  51. package/dist/runtime-COGvuXIq.d.ts +4790 -0
  52. package/dist/runtime-CSGWjztI.d.mts +5166 -0
  53. package/dist/runtime-CSGWjztI.d.ts +5166 -0
  54. package/dist/runtime-DRc_OhnM.d.mts +5142 -0
  55. package/dist/runtime-DRc_OhnM.d.ts +5142 -0
  56. package/dist/runtime-DUxWyRcq.d.mts +5399 -0
  57. package/dist/runtime-DUxWyRcq.d.ts +5399 -0
  58. package/dist/runtime-D_S3m9jf.d.mts +5142 -0
  59. package/dist/runtime-D_S3m9jf.d.ts +5142 -0
  60. package/dist/runtime-Db3rRxbJ.d.mts +4821 -0
  61. package/dist/runtime-Db3rRxbJ.d.ts +4821 -0
  62. package/dist/runtime-DlCiIIfr.d.mts +5130 -0
  63. package/dist/runtime-DlCiIIfr.d.ts +5130 -0
  64. package/dist/runtime-LO17NDk1.d.mts +5292 -0
  65. package/dist/runtime-LO17NDk1.d.ts +5292 -0
  66. package/dist/runtime-WqYxjhjJ.d.mts +5125 -0
  67. package/dist/runtime-WqYxjhjJ.d.ts +5125 -0
  68. package/dist/runtime.d.mts +1 -1
  69. package/dist/runtime.d.ts +1 -1
  70. package/dist/runtime.js +2178 -370
  71. package/dist/runtime.mjs +9 -1
  72. 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 import_zod = __toESM(require("zod"));
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 = import_zod.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-]*$/, {
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 import_zod.default.ZodError) {
1270
- throw new Error(`[ObjectBuilder] ${error.errors[0].message}`);
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 import_zod2 = require("zod");
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 = import_zod2.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-]*$/, {
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 import_zod2.z.ZodError) {
1644
- throw new Error(`[ViewBuilder] ${error.errors[0].message}`);
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 import_zod3 = require("zod");
1943
- var baseConfigSchema = import_zod3.z.object({
1944
- disabled: import_zod3.z.boolean().optional(),
1945
- placeholder: import_zod3.z.string().optional(),
1946
- description: import_zod3.z.string().optional(),
1947
- defaultValue: import_zod3.z.unknown().optional(),
1948
- icon: import_zod3.z.string().optional(),
1949
- order: import_zod3.z.number().int().optional(),
1950
- hidden: import_zod3.z.boolean().optional(),
1951
- archived: import_zod3.z.boolean().optional(),
1952
- deprecated: import_zod3.z.boolean().optional(),
1953
- metadata: import_zod3.z.record(import_zod3.z.unknown()).optional()
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 = import_zod3.z.object({
1956
- id: import_zod3.z.string().min(1),
1957
- label: import_zod3.z.string().min(1),
1958
- value: import_zod3.z.string().min(1),
1959
- color: import_zod3.z.string().optional(),
1960
- icon: import_zod3.z.string().optional(),
1961
- description: import_zod3.z.string().optional(),
1962
- group: import_zod3.z.enum(["idle", "in_progress", "finished"]).optional()
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 = import_zod3.z.object({
1965
- object: import_zod3.z.string().min(1),
1966
- displayTemplate: import_zod3.z.string().optional(),
1967
- filter: import_zod3.z.record(import_zod3.z.unknown()).optional()
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 = import_zod3.z.object({
1970
- type: import_zod3.z.enum([
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: import_zod3.z.string(),
1982
- faces: import_zod3.z.array(import_zod3.z.enum(["front", "back", "single"])),
1983
- attributeMapping: import_zod3.z.array(
1984
- import_zod3.z.object({
1985
- attributeId: import_zod3.z.string(),
1986
- extractedKey: import_zod3.z.string(),
1987
- face: import_zod3.z.enum(["front", "back", "single"]).optional(),
1988
- required: import_zod3.z.boolean().optional()
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 = import_zod3.z.object({
1993
- enabled: import_zod3.z.boolean(),
1994
- documentTypes: import_zod3.z.array(documentTypeConfigSchema),
1995
- autoExtract: import_zod3.z.boolean().optional(),
1996
- autoValidate: import_zod3.z.boolean().optional()
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: import_zod3.z.number().int().min(0).optional(),
2000
- maxLength: import_zod3.z.number().int().min(1).optional(),
2001
- pattern: import_zod3.z.string().optional()
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: import_zod3.z.number().optional(),
2006
- max: import_zod3.z.number().optional(),
2007
- unit: import_zod3.z.enum(["integer", "decimal", "percentage"]).optional(),
2008
- decimals: import_zod3.z.number().int().min(0).optional()
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: import_zod3.z.enum(["short", "long", "full", "relative"]).optional(),
2013
- minDate: import_zod3.z.string().optional(),
2014
- maxDate: import_zod3.z.string().optional()
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: import_zod3.z.string().length(3).optional()
2547
+ defaultCountryCode: import_zod4.z.string().length(3).optional()
2018
2548
  });
2019
2549
  var currencyConfigSchema = baseConfigSchema.extend({
2020
- defaultCurrency: import_zod3.z.string().length(3).optional(),
2021
- allowedCurrencies: import_zod3.z.array(import_zod3.z.string().length(3)).optional()
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: import_zod3.z.array(optionSchema).min(1)
2554
+ options: import_zod4.z.array(optionSchema).min(1)
2025
2555
  });
2026
2556
  var locationConfigSchema = baseConfigSchema.extend({
2027
- granularity: import_zod3.z.enum(["full", "address", "city", "state", "country", "coordinates"]),
2028
- enableAutocomplete: import_zod3.z.boolean().optional(),
2029
- enableMap: import_zod3.z.boolean().optional(),
2030
- defaultCountry: import_zod3.z.string().length(3).optional(),
2031
- allowedCountries: import_zod3.z.array(import_zod3.z.string().length(3)).optional(),
2032
- displayFormat: import_zod3.z.enum(["single_line", "multi_line", "compact"]).optional()
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: import_zod3.z.boolean().optional()
2565
+ autoUpdate: import_zod4.z.boolean().optional()
2036
2566
  });
2037
2567
  var selectConfigSchema = baseConfigSchema.extend({
2038
- options: import_zod3.z.array(optionSchema).min(1)
2568
+ options: import_zod4.z.array(optionSchema).min(1)
2039
2569
  });
2040
2570
  var multiselectConfigSchema = baseConfigSchema.extend({
2041
- options: import_zod3.z.array(optionSchema).min(1)
2571
+ options: import_zod4.z.array(optionSchema).min(1)
2042
2572
  });
2043
2573
  var fileConfigSchema = baseConfigSchema.extend({
2044
- maxFiles: import_zod3.z.number().int().min(1).optional(),
2045
- maxSize: import_zod3.z.number().int().min(1).optional(),
2046
- allowedTypes: import_zod3.z.array(import_zod3.z.string()).optional(),
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: import_zod3.z.array(import_zod3.z.string()).optional()
2580
+ allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional()
2051
2581
  });
2052
2582
  var relationConfigSchema = baseConfigSchema.extend({
2053
- targets: import_zod3.z.array(relationTargetSchema).min(1),
2054
- onDelete: import_zod3.z.enum(["restrict", "cascade", "set_null"]).optional(),
2055
- cardinality: import_zod3.z.enum(["one", "many"]),
2056
- minItems: import_zod3.z.number().int().min(0).optional(),
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: import_zod3.z.number().int().min(1).optional(),
2061
- iconType: import_zod3.z.enum(["star", "heart", "thumbs", "number"]).optional()
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.errors.map((err) => `${err.path.join(".")}: ${err.message}`)
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 = import_zod3.z.string();
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 = import_zod3.z.number();
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 import_zod3.z.boolean();
2666
+ return import_zod4.z.boolean();
2138
2667
  }
2139
2668
  function createDateValidator(attr) {
2140
- return import_zod3.z.string().datetime({ message: `${attr.label} must be a valid ISO date` });
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 import_zod3.z.object({
2144
- countryCode: import_zod3.z.string().length(3),
2145
- phoneNumber: import_zod3.z.string().min(1)
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 import_zod3.z.object({
2150
- code: import_zod3.z.string().length(3),
2151
- value: import_zod3.z.number().min(0)
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 import_zod3.z.enum(validValues, {
2157
- errorMap: () => ({
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 import_zod3.z.enum(validValues, {
2165
- errorMap: () => ({
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 import_zod3.z.array(
2173
- import_zod3.z.enum(validValues, {
2174
- errorMap: () => ({
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 import_zod3.z.object({
2182
- address: import_zod3.z.string().optional(),
2183
- address2: import_zod3.z.string().optional(),
2184
- city: import_zod3.z.string().optional(),
2185
- state: import_zod3.z.string().optional(),
2186
- postalCode: import_zod3.z.string().optional(),
2187
- country: import_zod3.z.string().length(3).optional(),
2188
- latitude: import_zod3.z.number().optional(),
2189
- longitude: import_zod3.z.number().optional()
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 import_zod3.z.number().int().positive();
2716
+ return import_zod4.z.number().int().positive();
2194
2717
  }
2195
2718
  function createFileValidator(_attr) {
2196
- return import_zod3.z.string().uuid();
2719
+ return import_zod4.z.string().uuid();
2197
2720
  }
2198
2721
  function createUserValidator(_attr) {
2199
- return import_zod3.z.string().uuid();
2722
+ return import_zod4.z.string().uuid();
2200
2723
  }
2201
2724
  function createSingleRelationValidator(attr) {
2202
- const uuidSchema = import_zod3.z.string().uuid({
2725
+ const uuidSchema = import_zod4.z.string().uuid({
2203
2726
  message: `${attr.label} must be a valid record ID`
2204
2727
  });
2205
- return import_zod3.z.union([uuidSchema, import_zod3.z.null()]);
2728
+ return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
2206
2729
  }
2207
2730
  function createMultiRelationValidator(attr) {
2208
- const uuidSchema = import_zod3.z.string().uuid({
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 = import_zod3.z.array(uuidSchema);
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 = import_zod3.z.number().int().min(0);
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 import_zod3.z.unknown();
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 import_zod3.z.object(shape);
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.errors.map((err) => ({
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.errors.map((err) => ({
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 import_zod3.z.object(shape);
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.errors.map((err) => ({
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
- findByObjectName(tenantId, objectName) {
2931
- return Promise.resolve(
2932
- Array.from(stores.views.values()).filter(
2933
- (v) => v.tenantId === tenantId && v.objectName === objectName
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/geocoding.service.ts
3253
- var GeocodingService = class {
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
- * Search for address suggestions as the user types
4301
+ * Get all flows for a tenant (system + custom)
3259
4302
  */
3260
- async autocomplete(params) {
3261
- if (!params.query || params.query.trim().length < 2) {
3262
- return [];
4303
+ async getAllFlows(tenantId) {
4304
+ const systemFlowsList = Array.from(this.systemFlows.values());
4305
+ if (!this.adapter.flows) {
4306
+ return systemFlowsList;
3263
4307
  }
3264
- return await this.adapter.autocomplete({
3265
- ...params,
3266
- query: params.query.trim(),
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
- * Reverse geocode coordinates to an address
4313
+ * Get published flows only
3272
4314
  */
3273
- async reverse(params) {
3274
- if (!this.adapter.reverse) {
3275
- throw new Error("Reverse geocoding is not supported by the configured adapter");
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
- return await this.adapter.reverse(params);
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
- * Geocode a structured address to coordinates
4337
+ * Get a flow by ID
3281
4338
  */
3282
- async geocode(params) {
3283
- if (!this.adapter.geocode) {
3284
- throw new Error("Geocoding is not supported by the configured adapter");
4339
+ async getFlowById(flowId) {
4340
+ if (!this.adapter.flows) {
4341
+ return null;
3285
4342
  }
3286
- return await this.adapter.geocode(params);
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
- // src/runtime/services/global-search.service.ts
3291
- var GlobalSearchService = class {
3292
- constructor(adapter, tenantId) {
3293
- this.adapter = adapter;
3294
- this.tenantId = tenantId;
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
- * Search across all object records for the tenant
3298
- *
3299
- * @param query - Search query string
3300
- * @param options - Search options (pagination, object filters)
3301
- * @returns Matching records with object metadata and total count
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.objects.create({
3414
- tenantId,
3415
- name: objectDef.name,
3416
- label: objectDef.label,
3417
- pluralLabel: objectDef.pluralLabel,
3418
- description: objectDef.description,
3419
- labelExpression: objectDef.labelExpression,
3420
- system: false,
3421
- // Always false for custom objects
3422
- metadata
3423
- });
3424
- const createdAttributes = [];
3425
- if (objectDef.attributes.length > 0) {
3426
- for (let i = 0; i < objectDef.attributes.length; i++) {
3427
- const attr = objectDef.attributes[i];
3428
- const config = validatedConfigs[i];
3429
- const dbAttr = await this.adapter.attributes.create({
3430
- objectId: dbObject.id,
3431
- name: attr.name,
3432
- label: attr.label,
3433
- type: attr.type,
3434
- required: attr.required ?? false,
3435
- unique: false,
3436
- system: false,
3437
- config
3438
- });
3439
- createdAttributes.push(this.convertDBAttributeToAttribute(dbAttr));
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
- return {
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
- mergeNativeObject(dbObject, dbAttributes) {
3731
- const nativeObject = this.nativeRegistry.getByName(dbObject.name);
3732
- if (!nativeObject) {
3733
- throw new Error(
3734
- `Native object "${dbObject.name}" not found in registry. Make sure to register it before querying.`
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
- * Convert DB object + attributes to ObjectDefinition
3766
- * @internal
5655
+ * Get all roles assigned to a user.
3767
5656
  */
3768
- convertDBObjectToDefinition(dbObject, dbAttributes) {
3769
- const attributes = dbAttributes.map(
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
- * Validate attribute input and return parsed config
3787
- * Validates: name format, config schema, relation targets
3788
- * @internal
5661
+ * Assign a role to a user.
3789
5662
  */
3790
- async validateAttributeInput(attribute, tenantId) {
3791
- this.validateAttributeName(attribute.name);
3792
- const config = this.buildAttributeConfig(attribute);
3793
- if (attribute.type === "relation") {
3794
- await this.validateRelationTargets(attribute, tenantId);
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 config;
5682
+ return result;
3797
5683
  }
3798
5684
  /**
3799
- * Build and validate attribute config from input
3800
- * Uses Zod schema to validate and strip unknown properties
3801
- * @internal
5685
+ * Revoke a role from a user.
3802
5686
  */
3803
- buildAttributeConfig(attribute) {
3804
- const {
3805
- name: _,
3806
- label: __,
3807
- type: ___,
3808
- required: ____,
3809
- unique: _____,
3810
- ...configInput
3811
- } = attribute;
3812
- try {
3813
- return parseAttributeConfig(attribute.type, configInput);
3814
- } catch (error) {
3815
- if (error instanceof Error) {
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
- * Validate that relation target objects exist
3823
- * @internal
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 validateRelationTargets(attribute, tenantId) {
3826
- const targets = attribute.targets;
3827
- if (!targets || targets.length === 0) {
3828
- throw new Error("Relation attribute must have at least one target object");
3829
- }
3830
- const cardinality = attribute.cardinality;
3831
- if (!(cardinality && ["one", "many"].includes(cardinality))) {
3832
- throw new Error("Relation attribute must specify cardinality: 'one' or 'many'");
3833
- }
3834
- for (const target of targets) {
3835
- if (!target.object) {
3836
- throw new Error("Each relation target must specify an object name");
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
- let targetObject = tenantId ? await this.adapter.objects.findByName(tenantId, target.object) : null;
3839
- if (!targetObject) {
3840
- targetObject = await this.adapter.objects.findSystemByName(target.object);
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
- if (!targetObject) {
3843
- const nativeObject = this.nativeRegistry.getByName(target.object);
3844
- if (!nativeObject) {
3845
- throw new Error(
3846
- `Relation target object "${target.object}" does not exist. Make sure the object is created before adding a relation to it.`
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 tenantId - Tenant ID for multi-tenant isolation
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, tenantId) {
5966
+ async resolveIds(ids, _tenantId, attributeId) {
4071
5967
  if (!ids || ids.length === 0) {
4072
5968
  return [];
4073
5969
  }
4074
- const recordService = new RecordService(this.adapter, tenantId);
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 id of ids) {
4077
- const record = await recordService.getRecord(id);
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
- const label = renderLabelExpression(objectSchema.labelExpression, record.values);
4086
- resolved.push({
4087
- id: record.id,
4088
- objectId: record.objectId,
4089
- objectName: objectSchema.name,
4090
- objectLabel: objectSchema.label,
4091
- objectIcon: objectSchema.icon,
4092
- label,
4093
- data: record.values
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 idsToResolve = [];
4136
- for (const attr of relationAttrs) {
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
- if (typeof val === "string" && resolvedMap.has(val)) {
4153
- enrichedValues[attr.name] = resolvedMap.get(val);
4154
- } else if (Array.isArray(val) && typeof val[0] === "string" && resolvedMap.has(val[0])) {
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
- return await this.adapter.userProfiles.create(data);
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
- return await this.adapter.userProfiles.update(profileId, data);
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 allProfiles = await this.listProfiles();
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,