@carlonicora/nextjs-jsonapi 1.77.3 → 1.78.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 (102) hide show
  1. package/dist/AssistantInterface-BYgI5z1-.d.mts +12 -0
  2. package/dist/AssistantInterface-DfDcz0gJ.d.ts +12 -0
  3. package/dist/AssistantMessageInterface-D0Kwf8CR.d.mts +36 -0
  4. package/dist/AssistantMessageInterface-DS_tyJTV.d.ts +36 -0
  5. package/dist/{BlockNoteEditor-UB7T7V67.js → BlockNoteEditor-2G5UYALC.js} +14 -14
  6. package/dist/{BlockNoteEditor-UB7T7V67.js.map → BlockNoteEditor-2G5UYALC.js.map} +1 -1
  7. package/dist/{BlockNoteEditor-7HAAXN3H.mjs → BlockNoteEditor-JXK3JGKJ.mjs} +4 -4
  8. package/dist/billing/index.js +346 -346
  9. package/dist/billing/index.mjs +3 -3
  10. package/dist/{chunk-FKLP4NED.js → chunk-FDJQRIMY.js} +320 -18
  11. package/dist/chunk-FDJQRIMY.js.map +1 -0
  12. package/dist/{chunk-XI35ALWY.mjs → chunk-I65SSQ5Z.mjs} +303 -1
  13. package/dist/chunk-I65SSQ5Z.mjs.map +1 -0
  14. package/dist/{chunk-F44ET4AC.mjs → chunk-NB6TIKHK.mjs} +2087 -1463
  15. package/dist/chunk-NB6TIKHK.mjs.map +1 -0
  16. package/dist/{chunk-JOJZRGZL.mjs → chunk-NZOUEN67.mjs} +2 -2
  17. package/dist/{chunk-OTZEXASK.js → chunk-X4YDETTD.js} +11 -11
  18. package/dist/{chunk-OTZEXASK.js.map → chunk-X4YDETTD.js.map} +1 -1
  19. package/dist/{chunk-CV7UOUKQ.js → chunk-ZEDB6JVB.js} +1356 -732
  20. package/dist/chunk-ZEDB6JVB.js.map +1 -0
  21. package/dist/client/index.js +4 -4
  22. package/dist/client/index.mjs +3 -3
  23. package/dist/components/index.d.mts +21 -2
  24. package/dist/components/index.d.ts +21 -2
  25. package/dist/components/index.js +10 -4
  26. package/dist/components/index.js.map +1 -1
  27. package/dist/components/index.mjs +9 -3
  28. package/dist/contexts/index.d.mts +26 -2
  29. package/dist/contexts/index.d.ts +26 -2
  30. package/dist/contexts/index.js +8 -4
  31. package/dist/contexts/index.js.map +1 -1
  32. package/dist/contexts/index.mjs +7 -3
  33. package/dist/core/index.d.mts +110 -3
  34. package/dist/core/index.d.ts +110 -3
  35. package/dist/core/index.js +14 -2
  36. package/dist/core/index.js.map +1 -1
  37. package/dist/core/index.mjs +13 -1
  38. package/dist/index.d.mts +3 -2
  39. package/dist/index.d.ts +3 -2
  40. package/dist/index.js +15 -3
  41. package/dist/index.js.map +1 -1
  42. package/dist/index.mjs +14 -2
  43. package/dist/server/index.js +3 -3
  44. package/dist/server/index.mjs +1 -1
  45. package/package.json +1 -1
  46. package/src/components/index.ts +2 -0
  47. package/src/contexts/index.ts +1 -0
  48. package/src/core/index.ts +4 -0
  49. package/src/core/registry/ModuleRegistry.ts +9 -0
  50. package/src/features/assistant/AssistantModule.ts +19 -0
  51. package/src/features/assistant/components/containers/AssistantContainer.tsx +56 -0
  52. package/src/features/assistant/components/containers/__tests__/AssistantContainer.spec.tsx +101 -0
  53. package/src/features/assistant/components/index.ts +1 -0
  54. package/src/features/assistant/components/parts/AssistantComposer.tsx +56 -0
  55. package/src/features/assistant/components/parts/AssistantEmptyState.tsx +47 -0
  56. package/src/features/assistant/components/parts/AssistantSidebar.tsx +64 -0
  57. package/src/features/assistant/components/parts/AssistantStatusLine.tsx +19 -0
  58. package/src/features/assistant/components/parts/AssistantThread.tsx +36 -0
  59. package/src/features/assistant/components/parts/AssistantThreadHeader.tsx +91 -0
  60. package/src/features/assistant/components/parts/__tests__/AssistantComposer.spec.tsx +32 -0
  61. package/src/features/assistant/components/parts/__tests__/AssistantEmptyState.spec.tsx +27 -0
  62. package/src/features/assistant/components/parts/__tests__/AssistantSidebar.spec.tsx +58 -0
  63. package/src/features/assistant/components/parts/__tests__/AssistantStatusLine.spec.tsx +19 -0
  64. package/src/features/assistant/components/parts/__tests__/AssistantThread.spec.tsx +39 -0
  65. package/src/features/assistant/components/parts/__tests__/AssistantThreadHeader.spec.tsx +67 -0
  66. package/src/features/assistant/contexts/AssistantContext.tsx +255 -0
  67. package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +375 -0
  68. package/src/features/assistant/data/Assistant.ts +37 -0
  69. package/src/features/assistant/data/AssistantInterface.ts +11 -0
  70. package/src/features/assistant/data/AssistantService.ts +79 -0
  71. package/src/features/assistant/data/index.ts +3 -0
  72. package/src/features/assistant/index.ts +2 -0
  73. package/src/features/assistant/utils/__tests__/groupThreadsByBucket.spec.ts +24 -0
  74. package/src/features/assistant/utils/__tests__/resolveReferenceableModules.spec.ts +92 -0
  75. package/src/features/assistant/utils/groupThreadsByBucket.ts +26 -0
  76. package/src/features/assistant/utils/resolveReferenceableModules.ts +14 -0
  77. package/src/features/assistant-message/AssistantMessageModule.ts +28 -0
  78. package/src/features/assistant-message/components/MessageItem.tsx +60 -0
  79. package/src/features/assistant-message/components/MessageList.tsx +38 -0
  80. package/src/features/assistant-message/components/__tests__/MessageItem.spec.tsx +108 -0
  81. package/src/features/assistant-message/components/index.ts +2 -0
  82. package/src/features/assistant-message/components/parts/ReferenceBadges.tsx +46 -0
  83. package/src/features/assistant-message/components/parts/SuggestedFollowUps.tsx +52 -0
  84. package/src/features/assistant-message/components/parts/__tests__/ReferenceBadges.spec.tsx +59 -0
  85. package/src/features/assistant-message/components/parts/__tests__/SuggestedFollowUps.spec.tsx +29 -0
  86. package/src/features/assistant-message/data/AssistantMessage.ts +95 -0
  87. package/src/features/assistant-message/data/AssistantMessageInterface.ts +21 -0
  88. package/src/features/assistant-message/data/AssistantMessageService.ts +40 -0
  89. package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +158 -0
  90. package/src/features/assistant-message/data/index.ts +3 -0
  91. package/src/features/assistant-message/index.ts +2 -0
  92. package/src/features/user/contexts/CurrentUserContext.tsx +5 -13
  93. package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +141 -0
  94. package/src/index.ts +4 -0
  95. package/dist/HowToInterface-BKhnkzBp.d.ts +0 -17
  96. package/dist/HowToInterface-Cj8OuQFf.d.mts +0 -17
  97. package/dist/chunk-CV7UOUKQ.js.map +0 -1
  98. package/dist/chunk-F44ET4AC.mjs.map +0 -1
  99. package/dist/chunk-FKLP4NED.js.map +0 -1
  100. package/dist/chunk-XI35ALWY.mjs.map +0 -1
  101. /package/dist/{BlockNoteEditor-7HAAXN3H.mjs.map → BlockNoteEditor-JXK3JGKJ.mjs.map} +0 -0
  102. /package/dist/{chunk-JOJZRGZL.mjs.map → chunk-NZOUEN67.mjs.map} +0 -0
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { AbstractApiData } from "../../../../core/abstracts/AbstractApiData";
3
+ import { DataClassRegistry } from "../../../../core/registry/DataClassRegistry";
4
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
5
+ import { JsonApiHydratedDataInterface } from "../../../../core/interfaces/JsonApiHydratedDataInterface";
6
+ import { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
7
+ import { AssistantMessage } from "../AssistantMessage";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Minimal test-only model — avoids collision with any real app module.
11
+ // identifierFields = ["name"] resolves "Acme" from attributes.name.
12
+ // ---------------------------------------------------------------------------
13
+ class TestAccount extends AbstractApiData {
14
+ static identifierFields: string[] = ["name"];
15
+
16
+ rehydrate(data: JsonApiHydratedDataInterface): this {
17
+ super.rehydrate(data);
18
+ return this;
19
+ }
20
+
21
+ createJsonApi(_data?: any): any {
22
+ return {};
23
+ }
24
+ }
25
+
26
+ const testAccountModule: ApiRequestDataTypeInterface = {
27
+ name: "test-accounts",
28
+ model: TestAccount,
29
+ } as any;
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Setup / teardown
33
+ // ---------------------------------------------------------------------------
34
+ const assistantMessageModule: ApiRequestDataTypeInterface = {
35
+ name: "assistant-messages",
36
+ model: AssistantMessage,
37
+ } as any;
38
+
39
+ const assistantModule: ApiRequestDataTypeInterface = {
40
+ name: "assistants",
41
+ model: class {},
42
+ } as any;
43
+
44
+ beforeAll(() => {
45
+ DataClassRegistry.clear();
46
+ DataClassRegistry.registerObjectClass(testAccountModule, TestAccount);
47
+ ModuleRegistry.register("TestAccount", testAccountModule);
48
+ ModuleRegistry.register("AssistantMessage", assistantMessageModule);
49
+ ModuleRegistry.register("Assistant", assistantModule);
50
+ });
51
+
52
+ afterAll(() => {
53
+ DataClassRegistry.clear();
54
+ });
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Helpers
58
+ // ---------------------------------------------------------------------------
59
+ function makeRehydratedMessage(jsonApiData: any, included: any[] = []): AssistantMessage {
60
+ const data: JsonApiHydratedDataInterface = {
61
+ jsonApi: jsonApiData,
62
+ included,
63
+ };
64
+ return new AssistantMessage().rehydrate(data);
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Tests
69
+ // ---------------------------------------------------------------------------
70
+ describe("AssistantMessage.rehydrate", () => {
71
+ it("rehydrates `references` as an array of the correct Module's model", () => {
72
+ // GIVEN: JSON:API assistant-message with a `references` relationship pointing to
73
+ // a test-accounts entity, plus that entity in the `included` array.
74
+ const jsonApiData = {
75
+ id: "msg-1",
76
+ type: "assistant-messages",
77
+ attributes: {
78
+ role: "assistant",
79
+ content: "Here is the account.",
80
+ position: 1,
81
+ },
82
+ relationships: {
83
+ references: {
84
+ data: [{ type: "test-accounts", id: "acc-1" }],
85
+ },
86
+ },
87
+ };
88
+ const included = [
89
+ {
90
+ type: "test-accounts",
91
+ id: "acc-1",
92
+ attributes: { name: "Acme" },
93
+ },
94
+ ];
95
+
96
+ // WHEN
97
+ const message = makeRehydratedMessage(jsonApiData, included);
98
+
99
+ // THEN: references should contain one hydrated TestAccount instance
100
+ expect(message.references).toHaveLength(1);
101
+
102
+ const ref = message.references[0] as any;
103
+ // Type and id should be populated from the included entry
104
+ expect(ref.type).toBe("test-accounts");
105
+ expect(ref.id).toBe("acc-1");
106
+ // identifier reads from identifierFields = ["name"] → attributes.name = "Acme"
107
+ expect(ref.identifier).toBe("Acme");
108
+ });
109
+
110
+ it("returns an empty array when the relationship is absent", () => {
111
+ // GIVEN: JSON:API assistant-message with no `references` attribute or relationship
112
+ const jsonApiData = {
113
+ id: "msg-2",
114
+ type: "assistant-messages",
115
+ attributes: {
116
+ role: "user",
117
+ content: "Hello.",
118
+ position: 0,
119
+ },
120
+ };
121
+
122
+ // WHEN
123
+ const message = makeRehydratedMessage(jsonApiData, []);
124
+
125
+ // THEN
126
+ expect(message.references).toEqual([]);
127
+ });
128
+ });
129
+
130
+ describe("AssistantMessage.buildOptimistic", () => {
131
+ it("creates a user message with a tmp-prefixed id and the given content + position", () => {
132
+ const msg = AssistantMessage.buildOptimistic({
133
+ content: "hello",
134
+ assistantId: "a-1",
135
+ position: 3,
136
+ });
137
+
138
+ expect(msg.id.startsWith("tmp-")).toBe(true);
139
+ expect(msg.role).toBe("user");
140
+ expect(msg.content).toBe("hello");
141
+ expect(msg.position).toBe(3);
142
+ });
143
+
144
+ it("allows an omitted assistantId (first-message case)", () => {
145
+ const msg = AssistantMessage.buildOptimistic({ content: "first", position: 1 });
146
+
147
+ expect(msg.id.startsWith("tmp-")).toBe(true);
148
+ expect(msg.role).toBe("user");
149
+ expect(msg.content).toBe("first");
150
+ expect(msg.position).toBe(1);
151
+ });
152
+
153
+ it("produces distinct ids across successive calls", () => {
154
+ const a = AssistantMessage.buildOptimistic({ content: "x", position: 1 });
155
+ const b = AssistantMessage.buildOptimistic({ content: "x", position: 2 });
156
+ expect(a.id).not.toEqual(b.id);
157
+ });
158
+ });
@@ -0,0 +1,3 @@
1
+ export * from "./AssistantMessage";
2
+ export * from "./AssistantMessageInterface";
3
+ export * from "./AssistantMessageService";
@@ -0,0 +1,2 @@
1
+ export * from "./AssistantMessageModule";
2
+ export * from "./data";
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
 
3
- import { getCookie } from "cookies-next";
4
3
  import { useAtom } from "jotai";
5
4
  import { atomWithStorage } from "jotai/utils";
6
5
  import { usePathname } from "next/navigation";
@@ -46,11 +45,6 @@ export const CurrentUserProvider = ({ children }: { children: React.ReactNode })
46
45
 
47
46
  const [dehydratedUser, setDehydratedUser] = useAtom(userAtom);
48
47
 
49
- useEffect(() => {
50
- const token = getCookie("token");
51
- if (!token && dehydratedUser) setDehydratedUser(null);
52
- }, [dehydratedUser, setDehydratedUser]);
53
-
54
48
  const matchUrlToModule = (_params?: { path: string }): ModuleWithPermissions | undefined => {
55
49
  const moduleKeys = Object.getOwnPropertyNames(Modules).filter(
56
50
  (key) => key !== "prototype" && key !== "_factory" && key !== "length" && key !== "name",
@@ -148,13 +142,9 @@ export const CurrentUserProvider = ({ children }: { children: React.ReactNode })
148
142
  try {
149
143
  const fullUser = await UserService.findFullUser();
150
144
  if (fullUser) {
151
- const dehydrated = fullUser.dehydrate();
152
-
153
- setDehydratedUser(dehydrated as any);
154
- setUser(fullUser);
155
-
156
- // Update authentication cookies with fresh user data
157
- // Skip when triggered by WebSocket to prevent page reload (Server Actions modify cookies)
145
+ // Update authentication cookies with fresh user data BEFORE writing the atom,
146
+ // so downstream observers see cookies-then-user, never user-then-cookies.
147
+ // Skip when triggered by WebSocket to prevent page reload (Server Actions modify cookies).
158
148
  if (!options?.skipCookieUpdate) {
159
149
  await getTokenHandler()?.updateToken({
160
150
  userId: fullUser.id,
@@ -167,6 +157,8 @@ export const CurrentUserProvider = ({ children }: { children: React.ReactNode })
167
157
  })),
168
158
  });
169
159
  }
160
+
161
+ setDehydratedUser(fullUser.dehydrate() as any);
170
162
  }
171
163
  } catch (error) {
172
164
  console.error("Failed to refresh user data:", error);
@@ -0,0 +1,141 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+
4
+ vi.mock("cookies-next", () => ({
5
+ getCookie: vi.fn(() => undefined),
6
+ }));
7
+
8
+ vi.mock("next/navigation", () => ({
9
+ usePathname: () => "/",
10
+ }));
11
+
12
+ vi.mock("../../../../contexts/SocketContext", () => ({
13
+ useSocketContext: () => ({ socket: null, isConnected: false }),
14
+ }));
15
+
16
+ vi.mock("../../../../core", () => ({
17
+ Modules: { User: { name: "users" } },
18
+ rehydrate: (_module: unknown, data: any) => data,
19
+ }));
20
+
21
+ vi.mock("../../../../permissions", () => ({
22
+ Action: { Read: "read", Write: "write", Create: "create", Update: "update", Delete: "delete" },
23
+ checkPermissions: () => true,
24
+ }));
25
+
26
+ vi.mock("../../../../roles", () => ({
27
+ getRoleId: () => ({ Administrator: "admin" }),
28
+ }));
29
+
30
+ vi.mock("../../../auth/config", () => ({
31
+ getTokenHandler: vi.fn(() => ({
32
+ updateToken: vi.fn().mockResolvedValue(undefined),
33
+ removeToken: vi.fn().mockResolvedValue(undefined),
34
+ })),
35
+ }));
36
+
37
+ vi.mock("../../data", () => ({
38
+ UserService: {
39
+ findFullUser: vi.fn(),
40
+ },
41
+ }));
42
+
43
+ import { CurrentUserProvider, useCurrentUserContext } from "../CurrentUserContext";
44
+ import { getCookie } from "cookies-next";
45
+ import { getTokenHandler } from "../../../auth/config";
46
+ import { UserService } from "../../data";
47
+
48
+ function Consumer() {
49
+ const { currentUser } = useCurrentUserContext();
50
+ return <div data-testid="current-user-id">{currentUser ? (currentUser as { id: string }).id : "null"}</div>;
51
+ }
52
+
53
+ describe("CurrentUserProvider", () => {
54
+ beforeEach(() => {
55
+ localStorage.clear();
56
+ vi.clearAllMocks();
57
+ });
58
+
59
+ it("keeps user hydrated from localStorage when token cookie is absent", async () => {
60
+ const storedUser = {
61
+ id: "user-1",
62
+ roles: [],
63
+ company: null,
64
+ modules: [],
65
+ };
66
+ localStorage.setItem("user", JSON.stringify(storedUser));
67
+ vi.mocked(getCookie).mockReturnValue(undefined);
68
+
69
+ const { getByTestId } = render(
70
+ <CurrentUserProvider>
71
+ <Consumer />
72
+ </CurrentUserProvider>,
73
+ );
74
+
75
+ await new Promise((resolve) => setTimeout(resolve, 100));
76
+
77
+ expect(getByTestId("current-user-id").textContent).toBe("user-1");
78
+ expect(localStorage.getItem("user")).toBe(JSON.stringify(storedUser));
79
+ });
80
+
81
+ it("awaits updateToken before writing the user atom in refreshUser", async () => {
82
+ const oldUser = { id: "old", roles: [], company: null, modules: [] };
83
+ localStorage.setItem("user", JSON.stringify(oldUser));
84
+
85
+ const newDehydrated = { id: "new", roles: [], company: null, modules: [] };
86
+ const newFullUser = {
87
+ id: "new",
88
+ roles: [],
89
+ company: null,
90
+ modules: [],
91
+ dehydrate: () => newDehydrated,
92
+ };
93
+ vi.mocked(UserService.findFullUser).mockResolvedValue(newFullUser as any);
94
+
95
+ let resolveUpdateToken!: () => void;
96
+ const updateTokenPromise = new Promise<void>((resolve) => {
97
+ resolveUpdateToken = resolve;
98
+ });
99
+ const updateToken = vi.fn(async () => {
100
+ await updateTokenPromise;
101
+ });
102
+ vi.mocked(getTokenHandler).mockReturnValue({
103
+ updateToken,
104
+ removeToken: vi.fn().mockResolvedValue(undefined),
105
+ } as any);
106
+
107
+ let capturedRefreshUser: ((options?: { skipCookieUpdate?: boolean }) => Promise<void>) | undefined;
108
+ function InnerConsumer() {
109
+ const ctx = useCurrentUserContext();
110
+ capturedRefreshUser = ctx.refreshUser;
111
+ return (
112
+ <div data-testid="current-user-id">{ctx.currentUser ? (ctx.currentUser as { id: string }).id : "null"}</div>
113
+ );
114
+ }
115
+
116
+ const { getByTestId } = render(
117
+ <CurrentUserProvider>
118
+ <InnerConsumer />
119
+ </CurrentUserProvider>,
120
+ );
121
+
122
+ await new Promise((resolve) => setTimeout(resolve, 50));
123
+ expect(getByTestId("current-user-id").textContent).toBe("old");
124
+
125
+ expect(capturedRefreshUser).toBeDefined();
126
+ const refreshPromise = capturedRefreshUser!();
127
+
128
+ await Promise.resolve();
129
+ await Promise.resolve();
130
+ await new Promise((resolve) => setTimeout(resolve, 50));
131
+
132
+ expect(updateToken).toHaveBeenCalledTimes(1);
133
+ expect(getByTestId("current-user-id").textContent).toBe("old");
134
+
135
+ resolveUpdateToken();
136
+ await refreshPromise;
137
+
138
+ await new Promise((resolve) => setTimeout(resolve, 50));
139
+ expect(getByTestId("current-user-id").textContent).toBe("new");
140
+ });
141
+ });
package/src/index.ts CHANGED
@@ -46,3 +46,7 @@ export { showToast, showError, dismissToast, showCustomToast, type ToastOptions
46
46
  // RBAC feature (data + modules only; components via /components, hooks via /client)
47
47
  export * from "./features/rbac/data";
48
48
  export * from "./features/rbac/rbac.module";
49
+
50
+ // Assistant + AssistantMessage feature data
51
+ export * from "./features/assistant/data";
52
+ export * from "./features/assistant-message/data";
@@ -1,17 +0,0 @@
1
- import { C as ContentInterface, a as ContentInput } from './notification.interface-DcSuc9CL.js';
2
-
3
- type BreadcrumbItemData = {
4
- name: string;
5
- href?: string;
6
- };
7
-
8
- type HowToInput = ContentInput & {
9
- description?: any;
10
- pages?: string | undefined | null;
11
- };
12
- interface HowToInterface extends ContentInterface {
13
- get description(): any;
14
- get pages(): string | undefined;
15
- }
16
-
17
- export type { BreadcrumbItemData as B, HowToInput as H, HowToInterface as a };
@@ -1,17 +0,0 @@
1
- import { C as ContentInterface, a as ContentInput } from './notification.interface-DG6obXUH.mjs';
2
-
3
- type BreadcrumbItemData = {
4
- name: string;
5
- href?: string;
6
- };
7
-
8
- type HowToInput = ContentInput & {
9
- description?: any;
10
- pages?: string | undefined | null;
11
- };
12
- interface HowToInterface extends ContentInterface {
13
- get description(): any;
14
- get pages(): string | undefined;
15
- }
16
-
17
- export type { BreadcrumbItemData as B, HowToInput as H, HowToInterface as a };