@htlkg/data 0.0.1 → 0.0.3

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 (56) hide show
  1. package/README.md +53 -0
  2. package/dist/client/index.d.ts +117 -31
  3. package/dist/client/index.js +74 -22
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/content-collections/index.js +20 -24
  6. package/dist/content-collections/index.js.map +1 -1
  7. package/dist/hooks/index.d.ts +113 -5
  8. package/dist/hooks/index.js +165 -182
  9. package/dist/hooks/index.js.map +1 -1
  10. package/dist/index.d.ts +8 -4
  11. package/dist/index.js +305 -182
  12. package/dist/index.js.map +1 -1
  13. package/dist/queries/index.d.ts +78 -1
  14. package/dist/queries/index.js +47 -0
  15. package/dist/queries/index.js.map +1 -1
  16. package/dist/stores/index.d.ts +106 -0
  17. package/dist/stores/index.js +108 -0
  18. package/dist/stores/index.js.map +1 -0
  19. package/package.json +60 -37
  20. package/src/client/__tests__/server.test.ts +100 -0
  21. package/src/client/client.md +91 -0
  22. package/src/client/index.test.ts +232 -0
  23. package/src/client/index.ts +145 -0
  24. package/src/client/server.ts +118 -0
  25. package/src/content-collections/content-collections.md +87 -0
  26. package/src/content-collections/generator.ts +314 -0
  27. package/src/content-collections/index.ts +32 -0
  28. package/src/content-collections/schemas.ts +75 -0
  29. package/src/content-collections/sync.ts +139 -0
  30. package/src/hooks/README.md +293 -0
  31. package/src/hooks/createDataHook.ts +208 -0
  32. package/src/hooks/data-hook-errors.property.test.ts +270 -0
  33. package/src/hooks/data-hook-filters.property.test.ts +263 -0
  34. package/src/hooks/data-hooks.property.test.ts +190 -0
  35. package/src/hooks/hooks.test.ts +76 -0
  36. package/src/hooks/index.ts +21 -0
  37. package/src/hooks/useAccounts.ts +66 -0
  38. package/src/hooks/useBrands.ts +95 -0
  39. package/src/hooks/useProducts.ts +88 -0
  40. package/src/hooks/useUsers.ts +89 -0
  41. package/src/index.ts +32 -0
  42. package/src/mutations/accounts.ts +127 -0
  43. package/src/mutations/brands.ts +133 -0
  44. package/src/mutations/index.ts +32 -0
  45. package/src/mutations/mutations.md +96 -0
  46. package/src/mutations/users.ts +136 -0
  47. package/src/queries/accounts.ts +121 -0
  48. package/src/queries/brands.ts +176 -0
  49. package/src/queries/index.ts +45 -0
  50. package/src/queries/products.ts +282 -0
  51. package/src/queries/queries.md +88 -0
  52. package/src/queries/server-helpers.ts +114 -0
  53. package/src/queries/users.ts +199 -0
  54. package/src/stores/createStores.ts +148 -0
  55. package/src/stores/index.ts +15 -0
  56. package/src/stores/stores.md +104 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Property-Based Tests for Data Hook Error Handling
3
+ *
4
+ * Feature: htlkg-modular-architecture, Property 7: Data hook errors are handled
5
+ * Validates: Requirements 11.5
6
+ *
7
+ * Tests that data hooks correctly handle errors during data fetching.
8
+ */
9
+
10
+ import { describe, it, expect, vi, beforeEach } from "vitest";
11
+ import * as fc from "fast-check";
12
+ import { useBrands } from "./useBrands";
13
+ import { useAccounts } from "./useAccounts";
14
+ import { useUsers } from "./useUsers";
15
+ import { useProducts } from "./useProducts";
16
+
17
+ // Create separate mock functions for each model
18
+ const mockBrandList = vi.fn();
19
+ const mockAccountList = vi.fn();
20
+ const mockUserList = vi.fn();
21
+ const mockProductList = vi.fn();
22
+
23
+ // Mock console.error to verify error logging
24
+ const mockConsoleError = vi.spyOn(console, "error").mockImplementation(() => {});
25
+
26
+ vi.mock("../client", () => ({
27
+ generateClient: vi.fn(() => ({
28
+ models: {
29
+ Brand: { list: mockBrandList },
30
+ Account: { list: mockAccountList },
31
+ User: { list: mockUserList },
32
+ Product: { list: mockProductList },
33
+ },
34
+ })),
35
+ }));
36
+
37
+ describe("Data Hook Error Handling - Property 7: Data hook errors are handled", () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ mockConsoleError.mockClear();
41
+ });
42
+
43
+ /**
44
+ * Property: For any error during fetch, the error state should be set
45
+ * and the error should be logged
46
+ */
47
+ it("useBrands handles fetch errors", async () => {
48
+ await fc.assert(
49
+ fc.asyncProperty(fc.string({ minLength: 1 }), async (errorMessage) => {
50
+ // Reset mocks
51
+ mockBrandList.mockClear();
52
+ mockConsoleError.mockClear();
53
+
54
+ // Setup mock to throw an error
55
+ const testError = new Error(errorMessage);
56
+ mockBrandList.mockRejectedValue(testError);
57
+
58
+ const { error, loading, refetch } = useBrands({ autoFetch: false });
59
+
60
+ // Initially no error
61
+ expect(error.value).toBeNull();
62
+
63
+ // Trigger fetch
64
+ await refetch();
65
+
66
+ // Wait for async operations
67
+ await new Promise((resolve) => setTimeout(resolve, 10));
68
+
69
+ // Error state should be set
70
+ expect(error.value).toBeInstanceOf(Error);
71
+ expect(error.value?.message).toBe(errorMessage);
72
+
73
+ // Loading should be false after error
74
+ expect(loading.value).toBe(false);
75
+
76
+ // Error should be logged
77
+ expect(mockConsoleError).toHaveBeenCalled();
78
+ expect(mockConsoleError.mock.calls[0][0]).toContain("[useBrands]");
79
+ }),
80
+ { numRuns: 50 },
81
+ );
82
+ });
83
+
84
+ /**
85
+ * Property: For any GraphQL errors array, the first error message should be used
86
+ */
87
+ it("useBrands handles GraphQL errors", async () => {
88
+ await fc.assert(
89
+ fc.asyncProperty(fc.string({ minLength: 1 }), async (errorMessage) => {
90
+ // Reset mocks
91
+ mockBrandList.mockClear();
92
+ mockConsoleError.mockClear();
93
+
94
+ // Setup mock to return GraphQL errors
95
+ mockBrandList.mockResolvedValue({
96
+ data: null,
97
+ errors: [{ message: errorMessage }],
98
+ });
99
+
100
+ const { error, loading, refetch } = useBrands({ autoFetch: false });
101
+
102
+ // Trigger fetch
103
+ await refetch();
104
+
105
+ // Wait for async operations
106
+ await new Promise((resolve) => setTimeout(resolve, 10));
107
+
108
+ // Error state should be set with the GraphQL error message
109
+ expect(error.value).toBeInstanceOf(Error);
110
+ expect(error.value?.message).toBe(errorMessage);
111
+
112
+ // Loading should be false after error
113
+ expect(loading.value).toBe(false);
114
+
115
+ // Error should be logged
116
+ expect(mockConsoleError).toHaveBeenCalled();
117
+ }),
118
+ { numRuns: 50 },
119
+ );
120
+ });
121
+
122
+ /**
123
+ * Property: For any error during useAccounts fetch, error handling should work
124
+ */
125
+ it("useAccounts handles fetch errors", async () => {
126
+ await fc.assert(
127
+ fc.asyncProperty(fc.string({ minLength: 1 }), async (errorMessage) => {
128
+ // Reset mocks
129
+ mockAccountList.mockClear();
130
+ mockConsoleError.mockClear();
131
+
132
+ // Setup mock to throw an error
133
+ const testError = new Error(errorMessage);
134
+ mockAccountList.mockRejectedValue(testError);
135
+
136
+ const { error, loading, refetch } = useAccounts({ autoFetch: false });
137
+
138
+ // Trigger fetch
139
+ await refetch();
140
+
141
+ // Wait for async operations
142
+ await new Promise((resolve) => setTimeout(resolve, 10));
143
+
144
+ // Error state should be set
145
+ expect(error.value).toBeInstanceOf(Error);
146
+ expect(error.value?.message).toBe(errorMessage);
147
+
148
+ // Loading should be false after error
149
+ expect(loading.value).toBe(false);
150
+
151
+ // Error should be logged
152
+ expect(mockConsoleError).toHaveBeenCalled();
153
+ expect(mockConsoleError.mock.calls[0][0]).toContain("[useAccounts]");
154
+ }),
155
+ { numRuns: 50 },
156
+ );
157
+ });
158
+
159
+ /**
160
+ * Property: For any error during useUsers fetch, error handling should work
161
+ */
162
+ it("useUsers handles fetch errors", async () => {
163
+ await fc.assert(
164
+ fc.asyncProperty(fc.string({ minLength: 1 }), async (errorMessage) => {
165
+ // Reset mocks
166
+ mockUserList.mockClear();
167
+ mockConsoleError.mockClear();
168
+
169
+ // Setup mock to throw an error
170
+ const testError = new Error(errorMessage);
171
+ mockUserList.mockRejectedValue(testError);
172
+
173
+ const { error, loading, refetch } = useUsers({ autoFetch: false });
174
+
175
+ // Trigger fetch
176
+ await refetch();
177
+
178
+ // Wait for async operations
179
+ await new Promise((resolve) => setTimeout(resolve, 10));
180
+
181
+ // Error state should be set
182
+ expect(error.value).toBeInstanceOf(Error);
183
+ expect(error.value?.message).toBe(errorMessage);
184
+
185
+ // Loading should be false after error
186
+ expect(loading.value).toBe(false);
187
+
188
+ // Error should be logged
189
+ expect(mockConsoleError).toHaveBeenCalled();
190
+ expect(mockConsoleError.mock.calls[0][0]).toContain("[useUsers]");
191
+ }),
192
+ { numRuns: 50 },
193
+ );
194
+ });
195
+
196
+ /**
197
+ * Property: For any error during useProducts fetch, error handling should work
198
+ */
199
+ it("useProducts handles fetch errors", async () => {
200
+ await fc.assert(
201
+ fc.asyncProperty(fc.string({ minLength: 1 }), async (errorMessage) => {
202
+ // Reset mocks
203
+ mockProductList.mockClear();
204
+ mockConsoleError.mockClear();
205
+
206
+ // Setup mock to throw an error
207
+ const testError = new Error(errorMessage);
208
+ mockProductList.mockRejectedValue(testError);
209
+
210
+ const { error, loading, refetch } = useProducts({ autoFetch: false });
211
+
212
+ // Trigger fetch
213
+ await refetch();
214
+
215
+ // Wait for async operations
216
+ await new Promise((resolve) => setTimeout(resolve, 10));
217
+
218
+ // Error state should be set
219
+ expect(error.value).toBeInstanceOf(Error);
220
+ expect(error.value?.message).toBe(errorMessage);
221
+
222
+ // Loading should be false after error
223
+ expect(loading.value).toBe(false);
224
+
225
+ // Error should be logged
226
+ expect(mockConsoleError).toHaveBeenCalled();
227
+ expect(mockConsoleError.mock.calls[0][0]).toContain("[useProducts]");
228
+ }),
229
+ { numRuns: 50 },
230
+ );
231
+ });
232
+
233
+ /**
234
+ * Property: After an error, refetch should clear the error and try again
235
+ */
236
+ it("refetch clears previous errors", async () => {
237
+ await fc.assert(
238
+ fc.asyncProperty(
239
+ fc.string({ minLength: 1 }),
240
+ fc.array(fc.record({ id: fc.string(), name: fc.string() })),
241
+ async (errorMessage, successData) => {
242
+ // Reset mocks
243
+ mockBrandList.mockClear();
244
+ mockConsoleError.mockClear();
245
+
246
+ // First call fails
247
+ mockBrandList.mockRejectedValueOnce(new Error(errorMessage));
248
+ // Second call succeeds
249
+ mockBrandList.mockResolvedValueOnce({ data: successData, errors: null });
250
+
251
+ const { error, brands, refetch } = useBrands({ autoFetch: false });
252
+
253
+ // First fetch - should error
254
+ await refetch();
255
+ await new Promise((resolve) => setTimeout(resolve, 10));
256
+
257
+ expect(error.value).toBeInstanceOf(Error);
258
+
259
+ // Second fetch - should succeed and clear error
260
+ await refetch();
261
+ await new Promise((resolve) => setTimeout(resolve, 10));
262
+
263
+ expect(error.value).toBeNull();
264
+ expect(brands.value).toEqual(successData);
265
+ },
266
+ ),
267
+ { numRuns: 30 },
268
+ );
269
+ });
270
+ });
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Property-Based Tests for Data Hook Filters
3
+ *
4
+ * Feature: htlkg-modular-architecture, Property 6: Data hook filters are applied
5
+ * Validates: Requirements 11.4
6
+ *
7
+ * Tests that data hooks correctly apply filter options to GraphQL queries.
8
+ */
9
+
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
11
+ import * as fc from "fast-check";
12
+ import { useBrands } from "./useBrands";
13
+ import { useAccounts } from "./useAccounts";
14
+ import { useUsers } from "./useUsers";
15
+ import { useProducts } from "./useProducts";
16
+ import type { Brand, Account, Product } from "@htlkg/core/types";
17
+
18
+ // Create separate mock functions for each model
19
+ const mockBrandList = vi.fn();
20
+ const mockAccountList = vi.fn();
21
+ const mockUserList = vi.fn();
22
+ const mockProductList = vi.fn();
23
+
24
+ vi.mock("../client", () => ({
25
+ generateClient: vi.fn(() => ({
26
+ models: {
27
+ Brand: { list: mockBrandList },
28
+ Account: { list: mockAccountList },
29
+ User: { list: mockUserList },
30
+ Product: { list: mockProductList },
31
+ },
32
+ })),
33
+ }));
34
+
35
+ describe("Data Hook Filters - Property 6: Data hook filters are applied", () => {
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+ mockBrandList.mockResolvedValue({ data: [], errors: null });
39
+ mockAccountList.mockResolvedValue({ data: [], errors: null });
40
+ mockUserList.mockResolvedValue({ data: [], errors: null });
41
+ mockProductList.mockResolvedValue({ data: [], errors: null });
42
+ });
43
+
44
+ /**
45
+ * Property: For any filter options, the hook should pass those filters
46
+ * to the GraphQL query
47
+ */
48
+ it("useBrands applies filter options to GraphQL query", async () => {
49
+ await fc.assert(
50
+ fc.asyncProperty(
51
+ fc.record({
52
+ accountId: fc.option(fc.string({ minLength: 1 }), { nil: undefined }),
53
+ activeOnly: fc.boolean(),
54
+ limit: fc.option(fc.integer({ min: 1, max: 100 }), { nil: undefined }),
55
+ }),
56
+ async (options) => {
57
+ // Reset mock before each property test iteration
58
+ mockBrandList.mockClear();
59
+
60
+ // Call hook with filter options
61
+ const { refetch } = useBrands({ ...options, autoFetch: false });
62
+ await refetch();
63
+
64
+ // Verify the mock was called
65
+ expect(mockBrandList).toHaveBeenCalledTimes(1);
66
+
67
+ const callArgs = mockBrandList.mock.calls[0][0];
68
+
69
+ // Check that limit is passed correctly
70
+ if (options.limit !== undefined) {
71
+ expect(callArgs.limit).toBe(options.limit);
72
+ }
73
+
74
+ // Check that filters are built correctly
75
+ if (callArgs.filter) {
76
+ if (options.accountId) {
77
+ expect(callArgs.filter.accountId).toEqual({ eq: options.accountId });
78
+ }
79
+
80
+ if (options.activeOnly) {
81
+ expect(callArgs.filter.status).toEqual({ eq: "active" });
82
+ }
83
+ }
84
+ },
85
+ ),
86
+ { numRuns: 50 },
87
+ );
88
+ });
89
+
90
+ /**
91
+ * Property: For any filter, the hook should only return items matching the filter
92
+ */
93
+ it("useBrands returns only filtered brands", async () => {
94
+ await fc.assert(
95
+ fc.asyncProperty(
96
+ fc.array(
97
+ fc.record({
98
+ id: fc.string(),
99
+ name: fc.string(),
100
+ accountId: fc.string(),
101
+ status: fc.constantFrom("active", "inactive", "maintenance", "suspended"),
102
+ timezone: fc.string(),
103
+ settings: fc.constant({}),
104
+ }),
105
+ { minLength: 0, maxLength: 20 },
106
+ ),
107
+ fc.constantFrom("active", "inactive", "maintenance", "suspended"),
108
+ async (mockBrands, filterStatus) => {
109
+ // Reset mock before each property test iteration
110
+ mockBrandList.mockClear();
111
+ mockBrandList.mockResolvedValue({ data: mockBrands, errors: null });
112
+
113
+ // Call hook with activeOnly filter
114
+ const { brands, refetch } = useBrands({
115
+ activeOnly: filterStatus === "active",
116
+ autoFetch: false,
117
+ });
118
+
119
+ await refetch();
120
+
121
+ // Wait for the hook to update
122
+ await new Promise((resolve) => setTimeout(resolve, 10));
123
+
124
+ // If activeOnly is true, verify all returned brands are active
125
+ if (filterStatus === "active") {
126
+ // The hook should have requested active brands
127
+ const callArgs = mockBrandList.mock.calls[0][0];
128
+ if (callArgs.filter) {
129
+ expect(callArgs.filter.status).toEqual({ eq: "active" });
130
+ }
131
+ }
132
+ },
133
+ ),
134
+ { numRuns: 50 },
135
+ );
136
+ });
137
+
138
+ /**
139
+ * Property: For any accountId filter, only brands from that account should be requested
140
+ */
141
+ it("useBrands filters by accountId", async () => {
142
+ await fc.assert(
143
+ fc.asyncProperty(
144
+ fc.string({ minLength: 1 }),
145
+ async (accountId) => {
146
+ // Reset mock before each property test iteration
147
+ mockBrandList.mockClear();
148
+
149
+ const { refetch } = useBrands({ accountId, autoFetch: false });
150
+ await refetch();
151
+
152
+ expect(mockBrandList).toHaveBeenCalledTimes(1);
153
+ const callArgs = mockBrandList.mock.calls[0][0];
154
+
155
+ // Verify accountId filter is applied
156
+ expect(callArgs.filter).toBeDefined();
157
+ expect(callArgs.filter.accountId).toEqual({ eq: accountId });
158
+ },
159
+ ),
160
+ { numRuns: 50 },
161
+ );
162
+ });
163
+
164
+ /**
165
+ * Property: For any filter options on useAccounts, filters should be passed to query
166
+ */
167
+ it("useAccounts applies filter options to GraphQL query", async () => {
168
+ await fc.assert(
169
+ fc.asyncProperty(
170
+ fc.record({
171
+ filter: fc.option(
172
+ fc.record({
173
+ status: fc.record({ eq: fc.string() }),
174
+ }),
175
+ { nil: undefined },
176
+ ),
177
+ limit: fc.option(fc.integer({ min: 1, max: 100 }), { nil: undefined }),
178
+ }),
179
+ async (options) => {
180
+ // Reset mock before each property test iteration
181
+ mockAccountList.mockClear();
182
+
183
+ const { refetch } = useAccounts({ ...options, autoFetch: false });
184
+ await refetch();
185
+
186
+ expect(mockAccountList).toHaveBeenCalledTimes(1);
187
+ const callArgs = mockAccountList.mock.calls[0][0];
188
+
189
+ // Check that limit is passed correctly
190
+ if (options.limit !== undefined) {
191
+ expect(callArgs.limit).toBe(options.limit);
192
+ }
193
+
194
+ // Check that filter is passed correctly
195
+ if (options.filter !== undefined) {
196
+ expect(callArgs.filter).toEqual(options.filter);
197
+ }
198
+ },
199
+ ),
200
+ { numRuns: 50 },
201
+ );
202
+ });
203
+
204
+ /**
205
+ * Property: For any brandId or accountId filter on useUsers, filters should be applied
206
+ */
207
+ it("useUsers applies brandId and accountId filters", async () => {
208
+ await fc.assert(
209
+ fc.asyncProperty(
210
+ fc.record({
211
+ brandId: fc.option(fc.string({ minLength: 1 }), { nil: undefined }),
212
+ accountId: fc.option(fc.string({ minLength: 1 }), { nil: undefined }),
213
+ }),
214
+ async (options) => {
215
+ // Reset mock before each property test iteration
216
+ mockUserList.mockClear();
217
+
218
+ const { refetch } = useUsers({ ...options, autoFetch: false });
219
+ await refetch();
220
+
221
+ expect(mockUserList).toHaveBeenCalledTimes(1);
222
+ const callArgs = mockUserList.mock.calls[0][0];
223
+
224
+ // Check that filters are built correctly
225
+ if (callArgs.filter) {
226
+ if (options.brandId) {
227
+ expect(callArgs.filter.brandIds).toEqual({ contains: options.brandId });
228
+ }
229
+
230
+ if (options.accountId) {
231
+ expect(callArgs.filter.accountIds).toEqual({ contains: options.accountId });
232
+ }
233
+ }
234
+ },
235
+ ),
236
+ { numRuns: 50 },
237
+ );
238
+ });
239
+
240
+ /**
241
+ * Property: For activeOnly filter on useProducts, only active products should be requested
242
+ */
243
+ it("useProducts applies activeOnly filter", async () => {
244
+ await fc.assert(
245
+ fc.asyncProperty(fc.boolean(), async (activeOnly) => {
246
+ // Reset mock before each property test iteration
247
+ mockProductList.mockClear();
248
+
249
+ const { refetch } = useProducts({ activeOnly, autoFetch: false });
250
+ await refetch();
251
+
252
+ expect(mockProductList).toHaveBeenCalledTimes(1);
253
+ const callArgs = mockProductList.mock.calls[0][0];
254
+
255
+ // If activeOnly is true, verify filter is applied
256
+ if (activeOnly && callArgs.filter) {
257
+ expect(callArgs.filter.isActive).toEqual({ eq: true });
258
+ }
259
+ }),
260
+ { numRuns: 50 },
261
+ );
262
+ });
263
+ });