@carlonicora/nextjs-jsonapi 1.16.0 → 1.17.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 (123) hide show
  1. package/dist/ApiData-DPKNfY-9.d.mts +10 -0
  2. package/dist/ApiData-DPKNfY-9.d.ts +10 -0
  3. package/dist/ApiRequestDataTypeInterface-DIEOFn9s.d.mts +40 -0
  4. package/dist/ApiRequestDataTypeInterface-DIEOFn9s.d.ts +40 -0
  5. package/dist/{ApiResponseInterface-BvWIeLkq.d.ts → ApiResponseInterface-BKyod24U.d.ts} +2 -11
  6. package/dist/{ApiResponseInterface-CAbw0sv7.d.mts → ApiResponseInterface-Dqvu09tz.d.mts} +2 -11
  7. package/dist/{BlockNoteEditor-MBFDWP7X.js → BlockNoteEditor-34T5CY27.js} +17 -16
  8. package/dist/BlockNoteEditor-34T5CY27.js.map +1 -0
  9. package/dist/{BlockNoteEditor-HFX7Z5BQ.mjs → BlockNoteEditor-4Z6TZBJE.mjs} +7 -6
  10. package/dist/{BlockNoteEditor-HFX7Z5BQ.mjs.map → BlockNoteEditor-4Z6TZBJE.mjs.map} +1 -1
  11. package/dist/JsonApiContext-Bsm_Q2oe.d.mts +41 -0
  12. package/dist/JsonApiContext-Bsm_Q2oe.d.ts +41 -0
  13. package/dist/JsonApiRequest-54ZBO7WQ.js +24 -0
  14. package/dist/{JsonApiRequest-45CLE65I.js.map → JsonApiRequest-54ZBO7WQ.js.map} +1 -1
  15. package/dist/{JsonApiRequest-6IPS3DZJ.mjs → JsonApiRequest-XWQWTFEQ.mjs} +2 -2
  16. package/dist/chunk-3EPNHTMH.js +26 -0
  17. package/dist/chunk-3EPNHTMH.js.map +1 -0
  18. package/dist/{chunk-BCKYJQ3K.mjs → chunk-3VM3WAOV.mjs} +1 -1
  19. package/dist/{chunk-R5QSSISB.js → chunk-7DTKRMYW.js} +21 -14
  20. package/dist/chunk-7DTKRMYW.js.map +1 -0
  21. package/dist/{chunk-ONB2DAIV.js → chunk-D7H7SRWB.js} +455 -470
  22. package/dist/chunk-D7H7SRWB.js.map +1 -0
  23. package/dist/{chunk-BCQSE3EU.mjs → chunk-KUFWHMMY.mjs} +8 -8
  24. package/dist/{chunk-POKIJ56Q.mjs → chunk-KX7YG6LY.mjs} +22 -15
  25. package/dist/chunk-KX7YG6LY.mjs.map +1 -0
  26. package/dist/{chunk-GPGJNTHP.js → chunk-LI6CPNJI.js} +1 -1
  27. package/dist/{chunk-GPGJNTHP.js.map → chunk-LI6CPNJI.js.map} +1 -1
  28. package/dist/{chunk-5RAUCUAA.mjs → chunk-SXPXC2TY.mjs} +18 -33
  29. package/dist/chunk-SXPXC2TY.mjs.map +1 -0
  30. package/dist/{chunk-2AZLCF6D.js → chunk-UYY34W7R.js} +28 -28
  31. package/dist/{chunk-2AZLCF6D.js.map → chunk-UYY34W7R.js.map} +1 -1
  32. package/dist/chunk-VOXD3ZLY.mjs +26 -0
  33. package/dist/chunk-VOXD3ZLY.mjs.map +1 -0
  34. package/dist/client/index.d.mts +11 -45
  35. package/dist/client/index.d.ts +11 -45
  36. package/dist/client/index.js +9 -7
  37. package/dist/client/index.js.map +1 -1
  38. package/dist/client/index.mjs +11 -9
  39. package/dist/components/index.d.mts +4 -3
  40. package/dist/components/index.d.ts +4 -3
  41. package/dist/components/index.js +7 -6
  42. package/dist/components/index.js.map +1 -1
  43. package/dist/components/index.mjs +6 -5
  44. package/dist/{config-DEaUbBqR.d.ts → config--nwiW74Z.d.ts} +1 -1
  45. package/dist/{config-CWsTwnsK.d.mts → config-BKSQmUWU.d.mts} +1 -1
  46. package/dist/{content.interface-D_4b4RQt.d.ts → content.interface-4VICFRA0.d.ts} +2 -1
  47. package/dist/{content.interface-Dk4UZcJM.d.mts → content.interface-CFc97-Cj.d.mts} +2 -1
  48. package/dist/contexts/index.d.mts +3 -2
  49. package/dist/contexts/index.d.ts +3 -2
  50. package/dist/contexts/index.js +7 -6
  51. package/dist/contexts/index.js.map +1 -1
  52. package/dist/contexts/index.mjs +6 -5
  53. package/dist/core/index.d.mts +11 -8
  54. package/dist/core/index.d.ts +11 -8
  55. package/dist/core/index.js +4 -4
  56. package/dist/core/index.js.map +1 -1
  57. package/dist/core/index.mjs +3 -3
  58. package/dist/index.d.mts +15 -11
  59. package/dist/index.d.ts +15 -11
  60. package/dist/index.js +5 -5
  61. package/dist/index.js.map +1 -1
  62. package/dist/index.mjs +6 -6
  63. package/dist/{notification.interface-BllkURRm.d.ts → notification.interface-BGaPiCUM.d.mts} +2 -40
  64. package/dist/{notification.interface-BllkURRm.d.mts → notification.interface-CqwaOIgM.d.ts} +2 -40
  65. package/dist/{s3.service-BEfGqho0.d.ts → s3.service-BYs88XEE.d.ts} +3 -2
  66. package/dist/{s3.service-DIQRYe93.d.mts → s3.service-C0BjOdvn.d.mts} +3 -2
  67. package/dist/server/index.d.mts +6 -4
  68. package/dist/server/index.d.ts +6 -4
  69. package/dist/server/index.js +13 -13
  70. package/dist/server/index.js.map +1 -1
  71. package/dist/server/index.mjs +3 -3
  72. package/dist/{stripe-subscription.interface-C63L6hVg.d.mts → stripe-subscription.interface-B-TM40Io.d.ts} +1 -1
  73. package/dist/{stripe-subscription.interface-CUvNDvw5.d.ts → stripe-subscription.interface-DDxnpj0F.d.mts} +1 -1
  74. package/dist/testing/index.d.mts +338 -0
  75. package/dist/testing/index.d.ts +338 -0
  76. package/dist/testing/index.js +323 -0
  77. package/dist/testing/index.js.map +1 -0
  78. package/dist/testing/index.mjs +323 -0
  79. package/dist/testing/index.mjs.map +1 -0
  80. package/dist/{useSocket-BpenBR2z.d.mts → useSocket-BNj9PrRw.d.mts} +1 -1
  81. package/dist/{useSocket-D-QYA0Sr.d.ts → useSocket-Dwt8cz1x.d.ts} +1 -1
  82. package/package.json +21 -3
  83. package/src/client/hooks/__tests__/useJsonApiGet.test.tsx +229 -0
  84. package/src/client/hooks/__tests__/useJsonApiMutation.test.tsx +348 -0
  85. package/src/client/hooks/__tests__/useRehydration.test.ts +188 -0
  86. package/src/components/forms/__tests__/FormCheckbox.test.tsx +238 -0
  87. package/src/components/forms/__tests__/FormDate.test.tsx +212 -0
  88. package/src/components/forms/__tests__/FormInput.test.tsx +292 -0
  89. package/src/components/forms/__tests__/FormSelect.test.tsx +173 -0
  90. package/src/components/tables/__tests__/ContentListTable.test.tsx +411 -0
  91. package/src/core/endpoint/__tests__/EndpointCreator.test.ts +168 -0
  92. package/src/core/factories/__tests__/JsonApiDataFactory.test.ts +109 -0
  93. package/src/core/factories/__tests__/RehydrationFactory.test.ts +151 -0
  94. package/src/core/registry/__tests__/DataClassRegistry.test.ts +136 -0
  95. package/src/core/registry/__tests__/ModuleRegistrar.test.ts +159 -0
  96. package/src/features/auth/components/details/LandingComponent.tsx +14 -12
  97. package/src/hooks/__tests__/useDataListRetriever.test.ts +321 -0
  98. package/src/hooks/__tests__/useDebounce.test.ts +170 -0
  99. package/src/index.ts +4 -1
  100. package/src/login/config.ts +27 -0
  101. package/src/login/index.ts +2 -0
  102. package/src/testing/factories/createMockApiData.ts +143 -0
  103. package/src/testing/factories/createMockModule.ts +32 -0
  104. package/src/testing/factories/createMockResponse.ts +93 -0
  105. package/src/testing/factories/createMockService.ts +79 -0
  106. package/src/testing/index.ts +70 -0
  107. package/src/testing/matchers/jsonApiMatchers.ts +174 -0
  108. package/src/testing/providers/MockJsonApiProvider.tsx +58 -0
  109. package/src/testing/utils/renderWithProviders.tsx +76 -0
  110. package/src/utils/__tests__/date-formatter.test.ts +161 -0
  111. package/src/utils/__tests__/exists.test.ts +100 -0
  112. package/src/utils/cn.test.ts +44 -0
  113. package/dist/BlockNoteEditor-MBFDWP7X.js.map +0 -1
  114. package/dist/JsonApiRequest-45CLE65I.js +0 -24
  115. package/dist/chunk-5RAUCUAA.mjs.map +0 -1
  116. package/dist/chunk-ONB2DAIV.js.map +0 -1
  117. package/dist/chunk-POKIJ56Q.mjs.map +0 -1
  118. package/dist/chunk-R5QSSISB.js.map +0 -1
  119. package/src/discord/config.ts +0 -15
  120. package/src/discord/index.ts +0 -1
  121. /package/dist/{JsonApiRequest-6IPS3DZJ.mjs.map → JsonApiRequest-XWQWTFEQ.mjs.map} +0 -0
  122. /package/dist/{chunk-BCKYJQ3K.mjs.map → chunk-3VM3WAOV.mjs.map} +0 -0
  123. /package/dist/{chunk-BCQSE3EU.mjs.map → chunk-KUFWHMMY.mjs.map} +0 -0
@@ -0,0 +1,411 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { ContentListTable } from "../ContentListTable";
5
+ import { DataListRetriever } from "../../../hooks";
6
+ import React from "react";
7
+
8
+ // Mock useTableGenerator hook
9
+ vi.mock("../../../hooks", async () => {
10
+ const actual = await vi.importActual("../../../hooks");
11
+ return {
12
+ ...actual,
13
+ useTableGenerator: vi.fn((_, params) => ({
14
+ data: params.data.map((item: any, index: number) => ({
15
+ ...item,
16
+ _rowIndex: index,
17
+ })),
18
+ columns: [
19
+ {
20
+ id: "id",
21
+ header: "ID",
22
+ accessorKey: "id",
23
+ cell: ({ row }: any) => row.original.id,
24
+ },
25
+ {
26
+ id: "title",
27
+ header: "Title",
28
+ accessorKey: "title",
29
+ cell: ({ row }: any) => row.original.title,
30
+ },
31
+ ],
32
+ })),
33
+ };
34
+ });
35
+
36
+ // Mock ContentTableSearch
37
+ vi.mock("../ContentTableSearch", () => ({
38
+ ContentTableSearch: () => <div data-testid="content-table-search">Search</div>,
39
+ }));
40
+
41
+ // Create a mock DataListRetriever
42
+ function createMockDataRetriever(overrides: Partial<DataListRetriever<any>> = {}): DataListRetriever<any> {
43
+ return {
44
+ ready: true,
45
+ setReady: vi.fn(),
46
+ isLoaded: true,
47
+ data: [],
48
+ search: vi.fn(),
49
+ refresh: vi.fn(),
50
+ addAdditionalParameter: vi.fn(),
51
+ removeAdditionalParameter: vi.fn(),
52
+ setRefreshedElement: vi.fn(),
53
+ removeElement: vi.fn(),
54
+ isSearch: false,
55
+ ...overrides,
56
+ };
57
+ }
58
+
59
+ const mockModule = {
60
+ name: "articles",
61
+ model: class MockArticle {},
62
+ };
63
+
64
+ describe("ContentListTable", () => {
65
+ beforeEach(() => {
66
+ vi.clearAllMocks();
67
+ });
68
+
69
+ describe("rendering", () => {
70
+ it("should render table", () => {
71
+ const dataRetriever = createMockDataRetriever({
72
+ data: [
73
+ { id: "1", title: "Article 1" },
74
+ { id: "2", title: "Article 2" },
75
+ ],
76
+ });
77
+
78
+ render(
79
+ <ContentListTable
80
+ data={dataRetriever}
81
+ tableGeneratorType={mockModule as any}
82
+ fields={["id", "title"]}
83
+ />
84
+ );
85
+
86
+ expect(screen.getByRole("table")).toBeInTheDocument();
87
+ });
88
+
89
+ it("should render title when provided", () => {
90
+ const dataRetriever = createMockDataRetriever({
91
+ data: [{ id: "1", title: "Article 1" }],
92
+ });
93
+
94
+ render(
95
+ <ContentListTable
96
+ data={dataRetriever}
97
+ tableGeneratorType={mockModule as any}
98
+ fields={["id", "title"]}
99
+ title="Articles"
100
+ />
101
+ );
102
+
103
+ expect(screen.getByText("Articles")).toBeInTheDocument();
104
+ });
105
+
106
+ it("should render table headers", () => {
107
+ const dataRetriever = createMockDataRetriever({
108
+ data: [{ id: "1", title: "Article 1" }],
109
+ });
110
+
111
+ render(
112
+ <ContentListTable
113
+ data={dataRetriever}
114
+ tableGeneratorType={mockModule as any}
115
+ fields={["id", "title"]}
116
+ />
117
+ );
118
+
119
+ expect(screen.getByText("ID")).toBeInTheDocument();
120
+ expect(screen.getByText("Title")).toBeInTheDocument();
121
+ });
122
+
123
+ it("should render table rows with data", () => {
124
+ const dataRetriever = createMockDataRetriever({
125
+ data: [
126
+ { id: "1", title: "First Article" },
127
+ { id: "2", title: "Second Article" },
128
+ ],
129
+ });
130
+
131
+ render(
132
+ <ContentListTable
133
+ data={dataRetriever}
134
+ tableGeneratorType={mockModule as any}
135
+ fields={["id", "title"]}
136
+ />
137
+ );
138
+
139
+ expect(screen.getByText("1")).toBeInTheDocument();
140
+ expect(screen.getByText("First Article")).toBeInTheDocument();
141
+ expect(screen.getByText("2")).toBeInTheDocument();
142
+ expect(screen.getByText("Second Article")).toBeInTheDocument();
143
+ });
144
+
145
+ it("should render 'No results' when data is empty", () => {
146
+ const dataRetriever = createMockDataRetriever({
147
+ data: [],
148
+ });
149
+
150
+ render(
151
+ <ContentListTable
152
+ data={dataRetriever}
153
+ tableGeneratorType={mockModule as any}
154
+ fields={["id", "title"]}
155
+ />
156
+ );
157
+
158
+ expect(screen.getByText("No results.")).toBeInTheDocument();
159
+ });
160
+ });
161
+
162
+ describe("search", () => {
163
+ it("should render search component when allowSearch is true and title is set", () => {
164
+ const dataRetriever = createMockDataRetriever({
165
+ data: [{ id: "1", title: "Article 1" }],
166
+ });
167
+
168
+ render(
169
+ <ContentListTable
170
+ data={dataRetriever}
171
+ tableGeneratorType={mockModule as any}
172
+ fields={["id", "title"]}
173
+ title="Articles"
174
+ allowSearch={true}
175
+ />
176
+ );
177
+
178
+ expect(screen.getByTestId("content-table-search")).toBeInTheDocument();
179
+ });
180
+ });
181
+
182
+ describe("pagination", () => {
183
+ it("should render pagination buttons when next or previous is available", () => {
184
+ const dataRetriever = createMockDataRetriever({
185
+ data: [{ id: "1", title: "Article 1" }],
186
+ next: vi.fn(),
187
+ previous: vi.fn(),
188
+ });
189
+
190
+ render(
191
+ <ContentListTable
192
+ data={dataRetriever}
193
+ tableGeneratorType={mockModule as any}
194
+ fields={["id", "title"]}
195
+ />
196
+ );
197
+
198
+ // Should have navigation buttons
199
+ const buttons = screen.getAllByRole("button");
200
+ expect(buttons.length).toBeGreaterThanOrEqual(2);
201
+ });
202
+
203
+ it("should disable previous button when no previous page", () => {
204
+ const dataRetriever = createMockDataRetriever({
205
+ data: [{ id: "1", title: "Article 1" }],
206
+ next: vi.fn(),
207
+ previous: undefined,
208
+ });
209
+
210
+ render(
211
+ <ContentListTable
212
+ data={dataRetriever}
213
+ tableGeneratorType={mockModule as any}
214
+ fields={["id", "title"]}
215
+ />
216
+ );
217
+
218
+ const buttons = screen.getAllByRole("button");
219
+ const previousButton = buttons[0];
220
+ expect(previousButton).toBeDisabled();
221
+ });
222
+
223
+ it("should disable next button when no next page", () => {
224
+ const dataRetriever = createMockDataRetriever({
225
+ data: [{ id: "1", title: "Article 1" }],
226
+ next: undefined,
227
+ previous: vi.fn(),
228
+ });
229
+
230
+ render(
231
+ <ContentListTable
232
+ data={dataRetriever}
233
+ tableGeneratorType={mockModule as any}
234
+ fields={["id", "title"]}
235
+ />
236
+ );
237
+
238
+ const buttons = screen.getAllByRole("button");
239
+ const nextButton = buttons[buttons.length - 1];
240
+ expect(nextButton).toBeDisabled();
241
+ });
242
+
243
+ it("should call next when next button is clicked", async () => {
244
+ const user = userEvent.setup();
245
+ const nextFn = vi.fn();
246
+
247
+ const dataRetriever = createMockDataRetriever({
248
+ data: [{ id: "1", title: "Article 1" }],
249
+ next: nextFn,
250
+ });
251
+
252
+ render(
253
+ <ContentListTable
254
+ data={dataRetriever}
255
+ tableGeneratorType={mockModule as any}
256
+ fields={["id", "title"]}
257
+ />
258
+ );
259
+
260
+ const buttons = screen.getAllByRole("button");
261
+ const nextButton = buttons[buttons.length - 1];
262
+ await user.click(nextButton);
263
+
264
+ expect(nextFn).toHaveBeenCalledWith(true);
265
+ });
266
+
267
+ it("should call previous when previous button is clicked", async () => {
268
+ const user = userEvent.setup();
269
+ const previousFn = vi.fn();
270
+
271
+ const dataRetriever = createMockDataRetriever({
272
+ data: [{ id: "1", title: "Article 1" }],
273
+ previous: previousFn,
274
+ next: vi.fn(), // Need both to show footer
275
+ });
276
+
277
+ render(
278
+ <ContentListTable
279
+ data={dataRetriever}
280
+ tableGeneratorType={mockModule as any}
281
+ fields={["id", "title"]}
282
+ />
283
+ );
284
+
285
+ const buttons = screen.getAllByRole("button");
286
+ const previousButton = buttons[0];
287
+ await user.click(previousButton);
288
+
289
+ expect(previousFn).toHaveBeenCalledWith(true);
290
+ });
291
+
292
+ it("should display page info when available", () => {
293
+ const dataRetriever = createMockDataRetriever({
294
+ data: [{ id: "1", title: "Article 1" }],
295
+ next: vi.fn(),
296
+ pageInfo: {
297
+ startItem: 1,
298
+ endItem: 25,
299
+ pageSize: 25,
300
+ },
301
+ });
302
+
303
+ render(
304
+ <ContentListTable
305
+ data={dataRetriever}
306
+ tableGeneratorType={mockModule as any}
307
+ fields={["id", "title"]}
308
+ />
309
+ );
310
+
311
+ expect(screen.getByText("1-25")).toBeInTheDocument();
312
+ });
313
+ });
314
+
315
+ describe("functions and filters", () => {
316
+ it("should render functions when provided", () => {
317
+ const dataRetriever = createMockDataRetriever({
318
+ data: [{ id: "1", title: "Article 1" }],
319
+ });
320
+
321
+ render(
322
+ <ContentListTable
323
+ data={dataRetriever}
324
+ tableGeneratorType={mockModule as any}
325
+ fields={["id", "title"]}
326
+ title="Articles"
327
+ functions={<button data-testid="custom-function">Custom</button>}
328
+ />
329
+ );
330
+
331
+ expect(screen.getByTestId("custom-function")).toBeInTheDocument();
332
+ });
333
+
334
+ it("should render filters when provided", () => {
335
+ const dataRetriever = createMockDataRetriever({
336
+ data: [{ id: "1", title: "Article 1" }],
337
+ });
338
+
339
+ render(
340
+ <ContentListTable
341
+ data={dataRetriever}
342
+ tableGeneratorType={mockModule as any}
343
+ fields={["id", "title"]}
344
+ title="Articles"
345
+ filters={<div data-testid="custom-filter">Filter</div>}
346
+ />
347
+ );
348
+
349
+ expect(screen.getByTestId("custom-filter")).toBeInTheDocument();
350
+ });
351
+ });
352
+
353
+ describe("footer visibility", () => {
354
+ it("should show footer when functions are provided", () => {
355
+ const dataRetriever = createMockDataRetriever({
356
+ data: [{ id: "1", title: "Article 1" }],
357
+ });
358
+
359
+ render(
360
+ <ContentListTable
361
+ data={dataRetriever}
362
+ tableGeneratorType={mockModule as any}
363
+ fields={["id", "title"]}
364
+ functions={<button>Action</button>}
365
+ />
366
+ );
367
+
368
+ // Footer should be present with buttons
369
+ const buttons = screen.getAllByRole("button");
370
+ expect(buttons.length).toBeGreaterThan(0);
371
+ });
372
+
373
+ it("should show footer when next page is available", () => {
374
+ const dataRetriever = createMockDataRetriever({
375
+ data: [{ id: "1", title: "Article 1" }],
376
+ next: vi.fn(),
377
+ });
378
+
379
+ render(
380
+ <ContentListTable
381
+ data={dataRetriever}
382
+ tableGeneratorType={mockModule as any}
383
+ fields={["id", "title"]}
384
+ />
385
+ );
386
+
387
+ // Should have navigation buttons in footer
388
+ const buttons = screen.getAllByRole("button");
389
+ expect(buttons.length).toBeGreaterThanOrEqual(2);
390
+ });
391
+
392
+ it("should show footer when previous page is available", () => {
393
+ const dataRetriever = createMockDataRetriever({
394
+ data: [{ id: "1", title: "Article 1" }],
395
+ previous: vi.fn(),
396
+ });
397
+
398
+ render(
399
+ <ContentListTable
400
+ data={dataRetriever}
401
+ tableGeneratorType={mockModule as any}
402
+ fields={["id", "title"]}
403
+ />
404
+ );
405
+
406
+ // Should have navigation buttons in footer
407
+ const buttons = screen.getAllByRole("button");
408
+ expect(buttons.length).toBeGreaterThanOrEqual(2);
409
+ });
410
+ });
411
+ });
@@ -0,0 +1,168 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { EndpointCreator } from "../EndpointCreator";
3
+ import { createMockModule } from "../../../testing";
4
+
5
+ describe("EndpointCreator", () => {
6
+ describe("basic URL generation", () => {
7
+ it("should generate URL from string endpoint", () => {
8
+ const creator = new EndpointCreator({ endpoint: "articles" });
9
+ expect(creator.generate()).toBe("articles");
10
+ });
11
+
12
+ it("should generate URL from module endpoint", () => {
13
+ const mockModule = createMockModule({ name: "articles" });
14
+ const creator = new EndpointCreator({ endpoint: mockModule });
15
+ expect(creator.generate()).toBe("articles");
16
+ });
17
+
18
+ it("should include id in URL", () => {
19
+ const creator = new EndpointCreator({ endpoint: "articles", id: "123" });
20
+ expect(creator.generate()).toBe("articles/123");
21
+ });
22
+
23
+ it("should include child endpoint", () => {
24
+ const creator = new EndpointCreator({
25
+ endpoint: "articles",
26
+ id: "123",
27
+ childEndpoint: "comments",
28
+ });
29
+ expect(creator.generate()).toBe("articles/123/comments");
30
+ });
31
+
32
+ it("should include child id", () => {
33
+ const creator = new EndpointCreator({
34
+ endpoint: "articles",
35
+ id: "123",
36
+ childEndpoint: "comments",
37
+ childId: "456",
38
+ });
39
+ expect(creator.generate()).toBe("articles/123/comments/456");
40
+ });
41
+ });
42
+
43
+ describe("fluent API", () => {
44
+ it("should support chaining endpoint()", () => {
45
+ const creator = new EndpointCreator({ endpoint: "initial" });
46
+ const result = creator.endpoint("articles");
47
+ expect(result).toBe(creator);
48
+ expect(creator.generate()).toBe("articles");
49
+ });
50
+
51
+ it("should support chaining id()", () => {
52
+ const creator = new EndpointCreator({ endpoint: "articles" });
53
+ const result = creator.id("123");
54
+ expect(result).toBe(creator);
55
+ expect(creator.generate()).toBe("articles/123");
56
+ });
57
+
58
+ it("should support chaining childEndpoint()", () => {
59
+ const creator = new EndpointCreator({ endpoint: "articles", id: "123" });
60
+ const result = creator.childEndpoint("comments");
61
+ expect(result).toBe(creator);
62
+ expect(creator.generate()).toBe("articles/123/comments");
63
+ });
64
+
65
+ it("should support chaining childId()", () => {
66
+ const creator = new EndpointCreator({
67
+ endpoint: "articles",
68
+ id: "123",
69
+ childEndpoint: "comments",
70
+ });
71
+ const result = creator.childId("456");
72
+ expect(result).toBe(creator);
73
+ expect(creator.generate()).toBe("articles/123/comments/456");
74
+ });
75
+
76
+ it("should support full fluent chain", () => {
77
+ const creator = new EndpointCreator({ endpoint: "articles" })
78
+ .id("123")
79
+ .childEndpoint("comments")
80
+ .childId("456");
81
+ expect(creator.generate()).toBe("articles/123/comments/456");
82
+ });
83
+ });
84
+
85
+ describe("query parameters", () => {
86
+ it("should add additional params", () => {
87
+ const creator = new EndpointCreator({
88
+ endpoint: "articles",
89
+ additionalParams: [{ key: "page", value: "1" }],
90
+ });
91
+ expect(creator.generate()).toBe("articles?page=1");
92
+ });
93
+
94
+ it("should support addAdditionalParam()", () => {
95
+ const creator = new EndpointCreator({ endpoint: "articles" });
96
+ creator.addAdditionalParam("page", "1");
97
+ creator.addAdditionalParam("limit", "10");
98
+ expect(creator.generate()).toBe("articles?page=1&limit=10");
99
+ });
100
+
101
+ it("should support array values in params", () => {
102
+ const creator = new EndpointCreator({ endpoint: "articles" });
103
+ creator.addAdditionalParam("filter", ["tag1", "tag2"]);
104
+ expect(creator.generate()).toBe("articles?filter=tag1,tag2");
105
+ });
106
+
107
+ it("should combine URL and params", () => {
108
+ const creator = new EndpointCreator({
109
+ endpoint: "articles",
110
+ id: "123",
111
+ });
112
+ creator.addAdditionalParam("include", "author");
113
+ expect(creator.generate()).toBe("articles/123?include=author");
114
+ });
115
+ });
116
+
117
+ describe("limitToType", () => {
118
+ it("should add include param with single type", () => {
119
+ const creator = new EndpointCreator({ endpoint: "articles" });
120
+ creator.limitToType(["author"]);
121
+ expect(creator.generate()).toBe("articles?include=author");
122
+ });
123
+
124
+ it("should add include param with multiple types", () => {
125
+ const creator = new EndpointCreator({ endpoint: "articles" });
126
+ creator.limitToType(["author", "comments", "tags"]);
127
+ expect(creator.generate()).toBe("articles?include=author,comments,tags");
128
+ });
129
+ });
130
+
131
+ describe("limitToFields", () => {
132
+ it("should add fields param with single selector", () => {
133
+ const creator = new EndpointCreator({ endpoint: "articles" });
134
+ creator.limitToFields([{ type: "articles", fields: ["title", "body"] }]);
135
+ expect(creator.generate()).toBe("articles?fields[articles]=title,body");
136
+ });
137
+
138
+ it("should add fields param with multiple selectors", () => {
139
+ const creator = new EndpointCreator({ endpoint: "articles" });
140
+ creator.limitToFields([
141
+ { type: "articles", fields: ["title", "body"] },
142
+ { type: "authors", fields: ["name", "email"] },
143
+ ]);
144
+ expect(creator.generate()).toBe(
145
+ "articles?fields[articles]=title,body&fields[authors]=name,email"
146
+ );
147
+ });
148
+
149
+ it("should handle empty selectors array", () => {
150
+ const creator = new EndpointCreator({ endpoint: "articles" });
151
+ creator.limitToFields([]);
152
+ expect(creator.generate()).toBe("articles");
153
+ });
154
+ });
155
+
156
+ describe("module endpoint support", () => {
157
+ it("should work with module as child endpoint", () => {
158
+ const parentModule = createMockModule({ name: "users" });
159
+ const childModule = createMockModule({ name: "posts" });
160
+ const creator = new EndpointCreator({
161
+ endpoint: parentModule,
162
+ id: "1",
163
+ childEndpoint: childModule,
164
+ });
165
+ expect(creator.generate()).toBe("users/1/posts");
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { JsonApiDataFactory } from "../JsonApiDataFactory";
3
+ import { DataClassRegistry } from "../../registry/DataClassRegistry";
4
+ import { ApiDataInterface } from "../../interfaces/ApiDataInterface";
5
+
6
+ // Mock class that tracks createJsonApi calls
7
+ class MockDataClass implements Partial<ApiDataInterface> {
8
+ type = "articles";
9
+ id = "1";
10
+
11
+ createJsonApi = vi.fn((data: any) => ({
12
+ type: "articles",
13
+ attributes: data,
14
+ }));
15
+
16
+ get included() {
17
+ return [];
18
+ }
19
+ get createdAt() {
20
+ return new Date();
21
+ }
22
+ get updatedAt() {
23
+ return new Date();
24
+ }
25
+ get self() {
26
+ return "/articles/1";
27
+ }
28
+ get jsonApi() {
29
+ return { type: this.type, id: this.id };
30
+ }
31
+ generateApiUrl() {
32
+ return `/articles/${this.id}`;
33
+ }
34
+ dehydrate() {
35
+ return { jsonApi: this.jsonApi, included: [], allData: [] };
36
+ }
37
+ rehydrate(data: any) {
38
+ return this;
39
+ }
40
+ }
41
+
42
+ describe("JsonApiDataFactory", () => {
43
+ const mockModule = { name: "articles", model: MockDataClass };
44
+
45
+ beforeEach(() => {
46
+ DataClassRegistry.clear();
47
+ vi.clearAllMocks();
48
+ });
49
+
50
+ describe("create", () => {
51
+ it("should create JSON:API formatted data", () => {
52
+ DataClassRegistry.registerObjectClass(mockModule as any, MockDataClass as any);
53
+
54
+ const inputData = { title: "Test Article", body: "Content" };
55
+ const result = JsonApiDataFactory.create(mockModule as any, inputData);
56
+
57
+ expect(result).toEqual({
58
+ type: "articles",
59
+ attributes: inputData,
60
+ });
61
+ });
62
+
63
+ it("should use the registered class to create data", () => {
64
+ DataClassRegistry.registerObjectClass(mockModule as any, MockDataClass as any);
65
+
66
+ const inputData = { title: "Test" };
67
+ JsonApiDataFactory.create(mockModule as any, inputData);
68
+
69
+ // The create method should have been called
70
+ // Note: We can't directly verify createJsonApi was called since
71
+ // it's called on a new instance, but we can verify the output format
72
+ });
73
+
74
+ it("should throw for unregistered module", () => {
75
+ const unregisteredModule = { name: "unknown", model: MockDataClass };
76
+
77
+ expect(() =>
78
+ JsonApiDataFactory.create(unregisteredModule as any, { data: "test" })
79
+ ).toThrow("Class not registered for key: unknown");
80
+ });
81
+
82
+ it("should handle empty data", () => {
83
+ DataClassRegistry.registerObjectClass(mockModule as any, MockDataClass as any);
84
+
85
+ const result = JsonApiDataFactory.create(mockModule as any, {});
86
+
87
+ expect(result).toEqual({
88
+ type: "articles",
89
+ attributes: {},
90
+ });
91
+ });
92
+
93
+ it("should handle complex nested data", () => {
94
+ DataClassRegistry.registerObjectClass(mockModule as any, MockDataClass as any);
95
+
96
+ const complexData = {
97
+ title: "Test",
98
+ metadata: {
99
+ tags: ["tag1", "tag2"],
100
+ author: { name: "John", email: "john@example.com" },
101
+ },
102
+ };
103
+
104
+ const result = JsonApiDataFactory.create(mockModule as any, complexData);
105
+
106
+ expect(result.attributes).toEqual(complexData);
107
+ });
108
+ });
109
+ });