@carlonicora/nextjs-jsonapi 1.52.0 → 1.53.0

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 (99) hide show
  1. package/dist/{AuthComponent-BkK4Sf3q.d.mts → AuthComponent-CK9aRRW2.d.mts} +1 -1
  2. package/dist/{AuthComponent-DCfP4o32.d.ts → AuthComponent-IqFWLNIU.d.ts} +1 -1
  3. package/dist/{BlockNoteEditor-KQPSJCYG.js → BlockNoteEditor-AROKR3J6.js} +14 -14
  4. package/dist/{BlockNoteEditor-KQPSJCYG.js.map → BlockNoteEditor-AROKR3J6.js.map} +1 -1
  5. package/dist/{BlockNoteEditor-WUVRCTQI.mjs → BlockNoteEditor-CNMSBGCL.mjs} +4 -4
  6. package/dist/ModulePathsInterface-49EWvbWy.d.mts +31 -0
  7. package/dist/ModulePathsInterface-wVS5Raa4.d.ts +31 -0
  8. package/dist/{auth.interface-C4kEZscm.d.ts → auth.interface-C1WjZ0fM.d.ts} +1 -1
  9. package/dist/{auth.interface-24ID4yhT.d.mts → auth.interface-fBFqIrw4.d.mts} +1 -1
  10. package/dist/billing/index.js +346 -346
  11. package/dist/billing/index.mjs +3 -3
  12. package/dist/{chunk-BUCV5VFT.mjs → chunk-FE26PIZK.mjs} +53 -2
  13. package/dist/chunk-FE26PIZK.mjs.map +1 -0
  14. package/dist/{chunk-BTLJZIDS.mjs → chunk-G5473JP3.mjs} +869 -40
  15. package/dist/chunk-G5473JP3.mjs.map +1 -0
  16. package/dist/{chunk-XNISXVQL.mjs → chunk-J2PYGXVD.mjs} +70 -1
  17. package/dist/chunk-J2PYGXVD.mjs.map +1 -0
  18. package/dist/{chunk-YKPIFJOB.js → chunk-PQIXFKHT.js} +1457 -628
  19. package/dist/chunk-PQIXFKHT.js.map +1 -0
  20. package/dist/{chunk-QIA5FOQB.js → chunk-QOLVON35.js} +71 -2
  21. package/dist/chunk-QOLVON35.js.map +1 -0
  22. package/dist/{chunk-V63TFESU.js → chunk-UJBUJALX.js} +53 -2
  23. package/dist/chunk-UJBUJALX.js.map +1 -0
  24. package/dist/client/index.d.mts +25 -7
  25. package/dist/client/index.d.ts +25 -7
  26. package/dist/client/index.js +10 -4
  27. package/dist/client/index.js.map +1 -1
  28. package/dist/client/index.mjs +9 -3
  29. package/dist/components/index.d.mts +52 -10
  30. package/dist/components/index.d.ts +52 -10
  31. package/dist/components/index.js +16 -4
  32. package/dist/components/index.js.map +1 -1
  33. package/dist/components/index.mjs +15 -3
  34. package/dist/{config-CPN6QZfo.d.ts → config-DZWAFB7H.d.ts} +1 -1
  35. package/dist/{config-DaxjKdIo.d.mts → config-ndRJIQsP.d.mts} +1 -1
  36. package/dist/{content.interface-DvPs_JbX.d.mts → content.interface-B5ySfiOE.d.mts} +1 -1
  37. package/dist/{content.interface-Czin-YRh.d.ts → content.interface-mmz0uMwm.d.ts} +1 -1
  38. package/dist/contexts/index.d.mts +2 -2
  39. package/dist/contexts/index.d.ts +2 -2
  40. package/dist/contexts/index.js +4 -4
  41. package/dist/contexts/index.mjs +3 -3
  42. package/dist/core/index.d.mts +15 -10
  43. package/dist/core/index.d.ts +15 -10
  44. package/dist/core/index.js +6 -2
  45. package/dist/core/index.js.map +1 -1
  46. package/dist/core/index.mjs +5 -1
  47. package/dist/index.d.mts +47 -10
  48. package/dist/index.d.ts +47 -10
  49. package/dist/index.js +17 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/index.mjs +16 -2
  52. package/dist/{notification.interface-DEW8hR8g.d.ts → notification.interface-COKHDQeE.d.ts} +1 -1
  53. package/dist/{notification.interface-DKR5WGKH.d.mts → notification.interface-DG7cq9oG.d.mts} +1 -1
  54. package/dist/{s3.service-BHjcTA0t.d.mts → s3.service-BoRPFx82.d.mts} +4 -4
  55. package/dist/{s3.service-C_K1VHyx.d.ts → s3.service-ppn9iGJU.d.ts} +4 -4
  56. package/dist/server/index.d.mts +4 -4
  57. package/dist/server/index.d.ts +4 -4
  58. package/dist/server/index.js +3 -3
  59. package/dist/server/index.mjs +1 -1
  60. package/dist/useRbacState-DhuYYr0S.d.mts +77 -0
  61. package/dist/useRbacState-NnzNL2ED.d.ts +77 -0
  62. package/dist/{useSocket-BW6haECW.d.mts → useSocket-CtfuR5wD.d.mts} +1 -1
  63. package/dist/{useSocket-C9FmYuRM.d.ts → useSocket-bsV-K4qR.d.ts} +1 -1
  64. package/package.json +1 -1
  65. package/src/client/index.ts +4 -0
  66. package/src/components/containers/RoundPageContainer.tsx +1 -1
  67. package/src/components/containers/RoundPageContainerTitle.tsx +1 -1
  68. package/src/components/index.ts +6 -0
  69. package/src/core/index.ts +1 -0
  70. package/src/core/registry/ModuleRegistry.ts +3 -0
  71. package/src/features/rbac/components/RbacContainer.tsx +82 -0
  72. package/src/features/rbac/components/RbacFeatureSection.tsx +66 -0
  73. package/src/features/rbac/components/RbacModuleTable.tsx +121 -0
  74. package/src/features/rbac/components/RbacPermissionCell.tsx +97 -0
  75. package/src/features/rbac/components/RbacPermissionPicker.tsx +179 -0
  76. package/src/features/rbac/components/RbacToolbar.tsx +40 -0
  77. package/src/features/rbac/data/ModulePaths.ts +25 -0
  78. package/src/features/rbac/data/ModulePathsInterface.ts +6 -0
  79. package/src/features/rbac/data/PermissionMapping.ts +43 -0
  80. package/src/features/rbac/data/PermissionMappingInterface.ts +12 -0
  81. package/src/features/rbac/data/RbacService.ts +47 -0
  82. package/src/features/rbac/data/RbacTypes.ts +15 -0
  83. package/src/features/rbac/data/index.ts +6 -0
  84. package/src/features/rbac/hooks/useRbacState.test.ts +178 -0
  85. package/src/features/rbac/hooks/useRbacState.ts +319 -0
  86. package/src/features/rbac/index.ts +19 -0
  87. package/src/features/rbac/rbac.module.ts +19 -0
  88. package/src/features/rbac/utils/RbacMigrationGenerator.test.ts +124 -0
  89. package/src/features/rbac/utils/RbacMigrationGenerator.ts +184 -0
  90. package/src/index.ts +4 -0
  91. package/dist/chunk-BTLJZIDS.mjs.map +0 -1
  92. package/dist/chunk-BUCV5VFT.mjs.map +0 -1
  93. package/dist/chunk-QIA5FOQB.js.map +0 -1
  94. package/dist/chunk-V63TFESU.js.map +0 -1
  95. package/dist/chunk-XNISXVQL.mjs.map +0 -1
  96. package/dist/chunk-YKPIFJOB.js.map +0 -1
  97. package/dist/useDataListRetriever-BqJSFBck.d.mts +0 -33
  98. package/dist/useDataListRetriever-BqJSFBck.d.ts +0 -33
  99. /package/dist/{BlockNoteEditor-WUVRCTQI.mjs.map → BlockNoteEditor-CNMSBGCL.mjs.map} +0 -0
@@ -0,0 +1,319 @@
1
+ "use client";
2
+
3
+ import { FeatureInterface } from "../../feature";
4
+ import { ModuleInterface } from "../../module";
5
+ import { RoleInterface } from "../../role";
6
+ import { useCallback, useMemo, useReducer } from "react";
7
+ import { ModulePathsInterface } from "../data/ModulePathsInterface";
8
+ import { PermissionMappingInterface } from "../data/PermissionMappingInterface";
9
+ import { ActionType, ACTION_TYPES, PermissionsMap, PermissionValue } from "../data/RbacTypes";
10
+
11
+ // --- State shape ---
12
+
13
+ interface OriginalData {
14
+ features: FeatureInterface[];
15
+ roles: RoleInterface[];
16
+ permissionMappings: PermissionMappingInterface[];
17
+ moduleRelationshipPaths: Map<string, string[]>;
18
+ }
19
+
20
+ interface RbacState {
21
+ original: OriginalData | null;
22
+ // Edited values: only store overrides from original
23
+ featureIsCore: Map<string, boolean>;
24
+ modulePermissions: Map<string, PermissionsMap>; // moduleId -> permissions overrides
25
+ rolePermissions: Map<string, PermissionsMap | null>; // "roleId:moduleId" -> permissions (null = cleared/inherit)
26
+ }
27
+
28
+ // --- Actions ---
29
+
30
+ type RbacAction =
31
+ | { type: "INIT"; payload: OriginalData }
32
+ | { type: "SET_FEATURE_IS_CORE"; featureId: string; isCore: boolean }
33
+ | { type: "SET_MODULE_DEFAULT_PERMISSION"; moduleId: string; actionType: ActionType; value: PermissionValue }
34
+ | { type: "SET_ROLE_PERMISSION"; roleId: string; moduleId: string; actionType: ActionType; value: PermissionValue }
35
+ | { type: "CLEAR_ROLE_PERMISSION"; roleId: string; moduleId: string; actionType: ActionType }
36
+ | { type: "CLEAR_ALL_ROLE_PERMISSIONS"; roleId: string; moduleId: string }
37
+ | { type: "RESET" };
38
+
39
+ function createInitialState(): RbacState {
40
+ return {
41
+ original: null,
42
+ featureIsCore: new Map(),
43
+ modulePermissions: new Map(),
44
+ rolePermissions: new Map(),
45
+ };
46
+ }
47
+
48
+ function findModule(features: FeatureInterface[], moduleId: string): ModuleInterface | undefined {
49
+ for (const feature of features) {
50
+ for (const mod of feature.modules) {
51
+ if (mod.id === moduleId) return mod;
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+
57
+ function findPermissionMapping(
58
+ mappings: PermissionMappingInterface[],
59
+ roleId: string,
60
+ moduleId: string,
61
+ ): PermissionMappingInterface | undefined {
62
+ return mappings.find((pm) => pm.roleId === roleId && pm.moduleId === moduleId);
63
+ }
64
+
65
+ function rbacReducer(state: RbacState, action: RbacAction): RbacState {
66
+ switch (action.type) {
67
+ case "INIT": {
68
+ return {
69
+ ...createInitialState(),
70
+ original: action.payload,
71
+ };
72
+ }
73
+
74
+ case "SET_FEATURE_IS_CORE": {
75
+ const newMap = new Map(state.featureIsCore);
76
+ const originalFeature = state.original?.features.find((f) => f.id === action.featureId);
77
+ if (originalFeature && originalFeature.isCore === action.isCore) {
78
+ newMap.delete(action.featureId);
79
+ } else {
80
+ newMap.set(action.featureId, action.isCore);
81
+ }
82
+ return { ...state, featureIsCore: newMap };
83
+ }
84
+
85
+ case "SET_MODULE_DEFAULT_PERMISSION": {
86
+ const newMap = new Map(state.modulePermissions);
87
+ const originalModule = state.original ? findModule(state.original.features, action.moduleId) : undefined;
88
+ const current = newMap.get(action.moduleId) ?? { ...originalModule?.permissions };
89
+ const updated = { ...current, [action.actionType]: action.value };
90
+ newMap.set(action.moduleId, updated);
91
+ return { ...state, modulePermissions: newMap };
92
+ }
93
+
94
+ case "SET_ROLE_PERMISSION": {
95
+ const key = `${action.roleId}:${action.moduleId}`;
96
+ const newMap = new Map(state.rolePermissions);
97
+ const existing = newMap.get(key);
98
+
99
+ if (existing === null) {
100
+ // Was cleared, start fresh
101
+ newMap.set(key, { [action.actionType]: action.value });
102
+ } else {
103
+ const originalMapping = state.original
104
+ ? findPermissionMapping(state.original.permissionMappings, action.roleId, action.moduleId)
105
+ : undefined;
106
+ const current = existing ?? (originalMapping ? { ...originalMapping.permissions } : {});
107
+ newMap.set(key, { ...current, [action.actionType]: action.value });
108
+ }
109
+ return { ...state, rolePermissions: newMap };
110
+ }
111
+
112
+ case "CLEAR_ROLE_PERMISSION": {
113
+ const key = `${action.roleId}:${action.moduleId}`;
114
+ const newMap = new Map(state.rolePermissions);
115
+ const existing = newMap.get(key);
116
+ if (existing === null) return state;
117
+
118
+ const originalMapping = state.original
119
+ ? findPermissionMapping(state.original.permissionMappings, action.roleId, action.moduleId)
120
+ : undefined;
121
+ const current = existing ?? (originalMapping ? { ...originalMapping.permissions } : {});
122
+ const updated = { ...current };
123
+ delete updated[action.actionType];
124
+
125
+ // If all actions cleared, set to null (inherit all)
126
+ const hasAnyPermission = ACTION_TYPES.some((at) => updated[at] !== undefined);
127
+ newMap.set(key, hasAnyPermission ? updated : null);
128
+ return { ...state, rolePermissions: newMap };
129
+ }
130
+
131
+ case "CLEAR_ALL_ROLE_PERMISSIONS": {
132
+ const key = `${action.roleId}:${action.moduleId}`;
133
+ const newMap = new Map(state.rolePermissions);
134
+ newMap.set(key, null);
135
+ return { ...state, rolePermissions: newMap };
136
+ }
137
+
138
+ case "RESET":
139
+ return {
140
+ ...createInitialState(),
141
+ original: state.original,
142
+ };
143
+
144
+ default:
145
+ return state;
146
+ }
147
+ }
148
+
149
+ // --- Hook ---
150
+
151
+ export function useRbacState() {
152
+ const [state, dispatch] = useReducer(rbacReducer, undefined, createInitialState);
153
+
154
+ const init = useCallback(
155
+ (
156
+ features: FeatureInterface[],
157
+ roles: RoleInterface[],
158
+ permissionMappings: PermissionMappingInterface[],
159
+ modulePaths: ModulePathsInterface[],
160
+ ) => {
161
+ const moduleRelationshipPaths = new Map<string, string[]>();
162
+ for (const mp of modulePaths) {
163
+ moduleRelationshipPaths.set(mp.moduleId, mp.paths);
164
+ }
165
+ dispatch({ type: "INIT", payload: { features, roles, permissionMappings, moduleRelationshipPaths } });
166
+ },
167
+ [],
168
+ );
169
+
170
+ const setFeatureIsCore = useCallback((featureId: string, isCore: boolean) => {
171
+ dispatch({ type: "SET_FEATURE_IS_CORE", featureId, isCore });
172
+ }, []);
173
+
174
+ const setModuleDefaultPermission = useCallback((moduleId: string, actionType: ActionType, value: PermissionValue) => {
175
+ dispatch({ type: "SET_MODULE_DEFAULT_PERMISSION", moduleId, actionType, value });
176
+ }, []);
177
+
178
+ const setRolePermission = useCallback(
179
+ (roleId: string, moduleId: string, actionType: ActionType, value: PermissionValue) => {
180
+ dispatch({ type: "SET_ROLE_PERMISSION", roleId, moduleId, actionType, value });
181
+ },
182
+ [],
183
+ );
184
+
185
+ const clearRolePermission = useCallback((roleId: string, moduleId: string, actionType: ActionType) => {
186
+ dispatch({ type: "CLEAR_ROLE_PERMISSION", roleId, moduleId, actionType });
187
+ }, []);
188
+
189
+ const clearAllRolePermissions = useCallback((roleId: string, moduleId: string) => {
190
+ dispatch({ type: "CLEAR_ALL_ROLE_PERMISSIONS", roleId, moduleId });
191
+ }, []);
192
+
193
+ const resetModulePermissions = useCallback((moduleId: string, roles: { id: string }[]) => {
194
+ // Set all default permissions to true
195
+ for (const actionType of ACTION_TYPES) {
196
+ dispatch({ type: "SET_MODULE_DEFAULT_PERMISSION", moduleId, actionType, value: true });
197
+ }
198
+ // Clear all role permissions for this module (set to inherit / "-")
199
+ for (const role of roles) {
200
+ dispatch({ type: "CLEAR_ALL_ROLE_PERMISSIONS", roleId: role.id, moduleId });
201
+ }
202
+ }, []);
203
+
204
+ const reset = useCallback(() => {
205
+ dispatch({ type: "RESET" });
206
+ }, []);
207
+
208
+ // --- Getters ---
209
+
210
+ const getFeatureIsCore = useCallback(
211
+ (featureId: string): boolean => {
212
+ if (state.featureIsCore.has(featureId)) return state.featureIsCore.get(featureId)!;
213
+ const feature = state.original?.features.find((f) => f.id === featureId);
214
+ return feature?.isCore ?? false;
215
+ },
216
+ [state.featureIsCore, state.original],
217
+ );
218
+
219
+ const getModuleDefaultPermission = useCallback(
220
+ (moduleId: string, actionType: ActionType): PermissionValue | undefined => {
221
+ const edited = state.modulePermissions.get(moduleId);
222
+ if (edited && edited[actionType] !== undefined) return edited[actionType];
223
+ if (!state.original) return undefined;
224
+ const mod = findModule(state.original.features, moduleId);
225
+ return mod?.permissions[actionType];
226
+ },
227
+ [state.modulePermissions, state.original],
228
+ );
229
+
230
+ const getRolePermission = useCallback(
231
+ (roleId: string, moduleId: string, actionType: ActionType): PermissionValue | undefined | null => {
232
+ const key = `${roleId}:${moduleId}`;
233
+ if (state.rolePermissions.has(key)) {
234
+ const perms = state.rolePermissions.get(key);
235
+ if (perms === null || perms === undefined) return null; // explicitly cleared = inherit
236
+ return perms[actionType] ?? null; // has role mapping but no entry for this action = inherit
237
+ }
238
+ // Fall back to original
239
+ if (!state.original) return undefined;
240
+ const mapping = findPermissionMapping(state.original.permissionMappings, roleId, moduleId);
241
+ if (!mapping) return undefined; // no mapping exists
242
+ return mapping.permissions[actionType] ?? null;
243
+ },
244
+ [state.rolePermissions, state.original],
245
+ );
246
+
247
+ const isDirty = useMemo(() => {
248
+ return state.featureIsCore.size > 0 || state.modulePermissions.size > 0 || state.rolePermissions.size > 0;
249
+ }, [state.featureIsCore, state.modulePermissions, state.rolePermissions]);
250
+
251
+ // Build the full effective configuration for migration generation
252
+ const getEffectiveConfiguration = useCallback(() => {
253
+ if (!state.original) return null;
254
+
255
+ const features = state.original.features.map((f) => ({
256
+ id: f.id,
257
+ name: f.name,
258
+ isCore: state.featureIsCore.has(f.id) ? state.featureIsCore.get(f.id)! : f.isCore,
259
+ modules: f.modules.map((m) => {
260
+ const editedPerms = state.modulePermissions.get(m.id);
261
+ return {
262
+ id: m.id,
263
+ name: m.name,
264
+ permissions: editedPerms ?? m.permissions,
265
+ };
266
+ }),
267
+ }));
268
+
269
+ // Build role permissions map
270
+ const rolePermissionsMap = new Map<string, PermissionsMap>();
271
+
272
+ // Start with originals
273
+ for (const pm of state.original.permissionMappings) {
274
+ rolePermissionsMap.set(`${pm.roleId}:${pm.moduleId}`, { ...pm.permissions });
275
+ }
276
+
277
+ // Apply edits
278
+ for (const [key, perms] of state.rolePermissions) {
279
+ if (perms === null) {
280
+ rolePermissionsMap.delete(key);
281
+ } else {
282
+ rolePermissionsMap.set(key, perms);
283
+ }
284
+ }
285
+
286
+ return {
287
+ features,
288
+ roles: state.original.roles,
289
+ rolePermissionsMap,
290
+ };
291
+ }, [state]);
292
+
293
+ const getModuleRelationshipPaths = useCallback(
294
+ (moduleId: string): string[] => {
295
+ return state.original?.moduleRelationshipPaths.get(moduleId) ?? [];
296
+ },
297
+ [state.original],
298
+ );
299
+
300
+ return {
301
+ original: state.original,
302
+ isDirty,
303
+ init,
304
+ setFeatureIsCore,
305
+ setModuleDefaultPermission,
306
+ setRolePermission,
307
+ clearRolePermission,
308
+ clearAllRolePermissions,
309
+ resetModulePermissions,
310
+ reset,
311
+ getFeatureIsCore,
312
+ getModuleDefaultPermission,
313
+ getRolePermission,
314
+ getEffectiveConfiguration,
315
+ getModuleRelationshipPaths,
316
+ };
317
+ }
318
+
319
+ export type RbacStateApi = ReturnType<typeof useRbacState>;
@@ -0,0 +1,19 @@
1
+ // Data layer
2
+ export * from "./data";
3
+
4
+ // Hooks
5
+ export { useRbacState } from "./hooks/useRbacState";
6
+
7
+ // Utils
8
+ export { generateMigrationFile, downloadMigrationFile } from "./utils/RbacMigrationGenerator";
9
+
10
+ // Components
11
+ export { RbacContainer } from "./components/RbacContainer";
12
+ export { RbacToolbar } from "./components/RbacToolbar";
13
+ export { RbacFeatureSection } from "./components/RbacFeatureSection";
14
+ export { RbacModuleTable } from "./components/RbacModuleTable";
15
+ export { RbacPermissionCell } from "./components/RbacPermissionCell";
16
+ export { RbacPermissionPicker } from "./components/RbacPermissionPicker";
17
+
18
+ // Module registrations
19
+ export { PermissionMappingModule, ModulePathsModule } from "./rbac.module";
@@ -0,0 +1,19 @@
1
+ import { ModuleFactory } from "../../permissions";
2
+ import { PermissionMapping } from "./data/PermissionMapping";
3
+ import { ModulePaths } from "./data/ModulePaths";
4
+
5
+ export const PermissionMappingModule = (factory: ModuleFactory) =>
6
+ factory({
7
+ pageUrl: "/rbac/permission-mappings",
8
+ name: "rbac/permission-mappings",
9
+ model: PermissionMapping,
10
+ moduleId: "f3aef019-c75f-4ca8-a9b5-a7495d901fc8",
11
+ });
12
+
13
+ export const ModulePathsModule = (factory: ModuleFactory) =>
14
+ factory({
15
+ pageUrl: "/rbac/module-paths",
16
+ name: "rbac/module-paths",
17
+ model: ModulePaths,
18
+ moduleId: "f4fb3f01-a947-4c2e-89c8-354a518cdb13",
19
+ });
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { generateMigrationFile, downloadMigrationFile } from "./RbacMigrationGenerator";
3
+ import { RoleInterface } from "../../role";
4
+ import { PermissionsMap } from "../data/RbacTypes";
5
+
6
+ const mockFeatures = [
7
+ {
8
+ id: "feat-1",
9
+ name: "CRM",
10
+ isCore: false,
11
+ modules: [
12
+ {
13
+ id: "mod-1",
14
+ name: "pipelines",
15
+ permissions: { create: true, read: true, update: true, delete: false } as PermissionsMap,
16
+ },
17
+ ],
18
+ },
19
+ ];
20
+
21
+ const mockRoles: RoleInterface[] = [
22
+ {
23
+ id: "role-1",
24
+ type: "roles",
25
+ included: [],
26
+ createdAt: new Date(),
27
+ updatedAt: new Date(),
28
+ name: "Manager",
29
+ description: "",
30
+ isSelectable: true,
31
+ requiredFeature: undefined,
32
+ } as unknown as RoleInterface,
33
+ ];
34
+
35
+ const mockRolePermissionsMap = new Map<string, PermissionsMap>();
36
+ mockRolePermissionsMap.set("role-1:mod-1", { create: true, read: true, update: true, delete: true });
37
+
38
+ describe("RbacMigrationGenerator", () => {
39
+ describe("Scenario: Generate valid migration file content", () => {
40
+ it("should produce a valid TypeScript file with MigrationInterface structure", () => {
41
+ const content = generateMigrationFile({
42
+ features: mockFeatures,
43
+ roles: mockRoles,
44
+ rolePermissionsMap: mockRolePermissionsMap,
45
+ });
46
+
47
+ expect(content).toContain("MigrationInterface");
48
+ expect(content).toContain("moduleQuery");
49
+ expect(content).toContain("pipelines");
50
+ expect(content).toContain("permissionQuery");
51
+ });
52
+
53
+ it("should include default permissions for each module", () => {
54
+ const content = generateMigrationFile({
55
+ features: mockFeatures,
56
+ roles: mockRoles,
57
+ rolePermissionsMap: mockRolePermissionsMap,
58
+ });
59
+
60
+ expect(content).toContain("Action.Create");
61
+ expect(content).toContain("Action.Read");
62
+ expect(content).toContain("Action.Update");
63
+ expect(content).toContain("Action.Delete");
64
+ });
65
+ });
66
+
67
+ describe("Scenario: CompanyAdministrator always gets all-true permissions", () => {
68
+ it("should include CompanyAdministrator role with all true permissions", () => {
69
+ const content = generateMigrationFile({
70
+ features: mockFeatures,
71
+ roles: mockRoles,
72
+ rolePermissionsMap: mockRolePermissionsMap,
73
+ });
74
+
75
+ expect(content).toContain("2e1eee00-6cba-4506-9059-ccd24e4ea5b0");
76
+ });
77
+ });
78
+
79
+ describe("Scenario: Migration file contains role-specific overrides", () => {
80
+ it("should include role permission overrides in generated queries", () => {
81
+ const content = generateMigrationFile({
82
+ features: mockFeatures,
83
+ roles: mockRoles,
84
+ rolePermissionsMap: mockRolePermissionsMap,
85
+ });
86
+
87
+ expect(content).toContain("Manager");
88
+ expect(content).toContain("permissionQuery");
89
+ });
90
+ });
91
+
92
+ describe("Scenario: Download triggers browser download", () => {
93
+ it("should create a blob and trigger download", () => {
94
+ const mockAnchor = {
95
+ href: "",
96
+ download: "",
97
+ click: vi.fn(),
98
+ style: {},
99
+ };
100
+ const mockCreateElement = vi.fn().mockReturnValue(mockAnchor);
101
+ const mockCreateObjectURL = vi.fn().mockReturnValue("blob:url");
102
+ const mockRevokeObjectURL = vi.fn();
103
+ const mockAppendChild = vi.fn();
104
+ const mockRemoveChild = vi.fn();
105
+
106
+ vi.stubGlobal("document", {
107
+ createElement: mockCreateElement,
108
+ body: { appendChild: mockAppendChild, removeChild: mockRemoveChild },
109
+ });
110
+ vi.stubGlobal("URL", {
111
+ createObjectURL: mockCreateObjectURL,
112
+ revokeObjectURL: mockRevokeObjectURL,
113
+ });
114
+ vi.stubGlobal("Blob", vi.fn());
115
+
116
+ downloadMigrationFile("test content");
117
+
118
+ expect(mockCreateElement).toHaveBeenCalledWith("a");
119
+ expect(mockAnchor.click).toHaveBeenCalled();
120
+
121
+ vi.unstubAllGlobals();
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,184 @@
1
+ import { RoleInterface } from "../../role";
2
+ import { ActionType, ACTION_TYPES, COMPANY_ADMINISTRATOR_ROLE_ID, PermissionsMap } from "../data/RbacTypes";
3
+
4
+ const ACTION_ENUM_MAP: Record<ActionType, string> = {
5
+ read: "Action.Read",
6
+ create: "Action.Create",
7
+ update: "Action.Update",
8
+ delete: "Action.Delete",
9
+ };
10
+
11
+ function formatPermissionValue(value: boolean | string): string {
12
+ if (value === true) return "true";
13
+ if (value === false) return "false";
14
+ return `"${value}"`;
15
+ }
16
+
17
+ function formatPermissionsFromMap(perms: PermissionsMap): string {
18
+ const entries: string[] = [];
19
+ for (const actionType of ACTION_TYPES) {
20
+ const value = perms[actionType];
21
+ if (value !== undefined) {
22
+ entries.push(`{ type: ${ACTION_ENUM_MAP[actionType]}, value: ${formatPermissionValue(value)} }`);
23
+ }
24
+ }
25
+
26
+ if (entries.length === 0) return "[]";
27
+ if (entries.length === 1) return `[${entries[0]}]`;
28
+ return `[\n ${entries.join(",\n ")},\n ]`;
29
+ }
30
+
31
+ interface EffectiveFeature {
32
+ id: string;
33
+ name: string;
34
+ isCore: boolean;
35
+ modules: Array<{
36
+ id: string;
37
+ name: string;
38
+ permissions: PermissionsMap;
39
+ }>;
40
+ }
41
+
42
+ export function generateMigrationFile(params: {
43
+ features: EffectiveFeature[];
44
+ roles: RoleInterface[];
45
+ rolePermissionsMap: Map<string, PermissionsMap>;
46
+ }): string {
47
+ const { features, roles, rolePermissionsMap } = params;
48
+ const lines: string[] = [];
49
+
50
+ // Header
51
+ lines.push(`/**`);
52
+ lines.push(` * RBAC Migration - Generated on ${new Date().toISOString().split("T")[0]}`);
53
+ lines.push(` * Contains features, modules, roles, and permission mappings.`);
54
+ lines.push(` */`);
55
+ lines.push(``);
56
+ lines.push(`import { Action } from "src/common/enums/action";`);
57
+ lines.push(`import { MigrationInterface } from "src/core/migrator/interfaces/migration.interface";`);
58
+ lines.push(
59
+ `import { featureQuery, moduleQuery, roleQuery, permissionQuery } from "src/neo4j.migrations/queries/migration.queries";`,
60
+ );
61
+ lines.push(``);
62
+ lines.push(`export const migration: MigrationInterface[] = [`);
63
+
64
+ // Features
65
+ lines.push(` /* ************************************ */`);
66
+ lines.push(` /* FEATURES */`);
67
+ lines.push(` /* ************************************ */`);
68
+ for (const feature of features) {
69
+ lines.push(` {`);
70
+ lines.push(` query: featureQuery,`);
71
+ lines.push(` queryParams: {`);
72
+ lines.push(` featureId: "${feature.id}",`);
73
+ lines.push(` featureName: "${feature.name}",`);
74
+ lines.push(` isCore: ${feature.isCore},`);
75
+ lines.push(` },`);
76
+ lines.push(` },`);
77
+ }
78
+
79
+ // Modules grouped by feature
80
+ for (const feature of features) {
81
+ if (feature.modules.length === 0) continue;
82
+
83
+ lines.push(` /* ************************************ */`);
84
+ lines.push(` /* ${feature.name.toUpperCase().padEnd(37)} */`);
85
+ lines.push(` /* ************************************ */`);
86
+
87
+ for (const mod of feature.modules) {
88
+ lines.push(` {`);
89
+ lines.push(` query: moduleQuery,`);
90
+ lines.push(` queryParams: {`);
91
+ lines.push(` moduleName: "${mod.name}",`);
92
+ lines.push(` moduleId: "${mod.id}",`);
93
+ lines.push(` featureId: "${feature.id}",`);
94
+ lines.push(` permissions: JSON.stringify(${formatPermissionsFromMap(mod.permissions)}),`);
95
+ lines.push(` },`);
96
+ lines.push(` },`);
97
+ }
98
+ }
99
+
100
+ // Roles
101
+ lines.push(` /* ************************************ */`);
102
+ lines.push(` /* ROLES */`);
103
+ lines.push(` /* ************************************ */`);
104
+ for (const role of roles) {
105
+ lines.push(` {`);
106
+ lines.push(` query: roleQuery,`);
107
+ lines.push(` queryParams: {`);
108
+ lines.push(` roleId: "${role.id}",`);
109
+ lines.push(` roleName: "${role.name}",`);
110
+ lines.push(` isSelectable: ${role.isSelectable},`);
111
+ lines.push(` },`);
112
+ lines.push(` },`);
113
+ }
114
+
115
+ // Permission mappings
116
+ lines.push(` /* ************************************ */`);
117
+ lines.push(` /* PERMISSIONS */`);
118
+ lines.push(` /* ************************************ */`);
119
+
120
+ // Collect all module IDs for CompanyAdministrator (always all-true)
121
+ const allModuleIds: string[] = [];
122
+ for (const feature of features) {
123
+ for (const mod of feature.modules) {
124
+ allModuleIds.push(mod.id);
125
+ }
126
+ }
127
+
128
+ const allTruePermissions: PermissionsMap = { read: true, create: true, update: true, delete: true };
129
+
130
+ // Group by role for readability
131
+ const permsByRole = new Map<string, Array<{ moduleId: string; permissions: PermissionsMap }>>();
132
+
133
+ // CompanyAdministrator always gets all-true for every module
134
+ permsByRole.set(
135
+ COMPANY_ADMINISTRATOR_ROLE_ID,
136
+ allModuleIds.map((moduleId) => ({ moduleId, permissions: allTruePermissions })),
137
+ );
138
+
139
+ for (const [key, perms] of rolePermissionsMap) {
140
+ const [roleId, moduleId] = key.split(":");
141
+ if (roleId === COMPANY_ADMINISTRATOR_ROLE_ID) continue; // already handled
142
+ if (!permsByRole.has(roleId)) permsByRole.set(roleId, []);
143
+ permsByRole.get(roleId)!.push({ moduleId, permissions: perms });
144
+ }
145
+
146
+ for (const [roleId, moduleMappings] of permsByRole) {
147
+ const role = roles.find((r) => r.id === roleId);
148
+ if (role) {
149
+ lines.push(` // ${role.name}`);
150
+ }
151
+
152
+ for (const mapping of moduleMappings) {
153
+ lines.push(` {`);
154
+ lines.push(` query: permissionQuery,`);
155
+ lines.push(` queryParams: {`);
156
+ lines.push(` roleId: "${roleId}",`);
157
+ lines.push(` moduleId: "${mapping.moduleId}",`);
158
+ lines.push(` permissions: JSON.stringify(${formatPermissionsFromMap(mapping.permissions)}),`);
159
+ lines.push(` },`);
160
+ lines.push(` },`);
161
+ }
162
+ }
163
+
164
+ lines.push(`];`);
165
+ lines.push(``);
166
+
167
+ return lines.join("\n");
168
+ }
169
+
170
+ export function downloadMigrationFile(content: string): void {
171
+ const now = new Date();
172
+ const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`;
173
+ const filename = `${dateStr}_001.ts`;
174
+
175
+ const blob = new Blob([content], { type: "text/plain" });
176
+ const url = URL.createObjectURL(blob);
177
+ const a = document.createElement("a");
178
+ a.href = url;
179
+ a.download = filename;
180
+ document.body.appendChild(a);
181
+ a.click();
182
+ document.body.removeChild(a);
183
+ URL.revokeObjectURL(url);
184
+ }
package/src/index.ts CHANGED
@@ -42,3 +42,7 @@ export type { ReferralConfig } from "./features/referral/config";
42
42
 
43
43
  // Toast utilities
44
44
  export { showToast, showError, dismissToast, showCustomToast, type ToastOptions } from "./utils/toast";
45
+
46
+ // RBAC feature (data + modules only; components via /components, hooks via /client)
47
+ export * from "./features/rbac/data";
48
+ export * from "./features/rbac/rbac.module";