@htlkg/data 0.0.21 → 0.0.22
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.
- package/dist/hooks/index.d.ts +601 -94
- package/dist/hooks/index.js +682 -73
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +691 -82
- package/dist/index.js.map +1 -1
- package/dist/mutations/index.js +4 -4
- package/dist/mutations/index.js.map +1 -1
- package/dist/queries/index.js +5 -5
- package/dist/queries/index.js.map +1 -1
- package/package.json +11 -12
- package/src/hooks/accounts/index.ts +2 -0
- package/src/hooks/{useAccounts.ts → accounts/useAccounts.ts} +48 -5
- package/src/hooks/accounts/usePaginatedAccounts.ts +166 -0
- package/src/hooks/brands/index.ts +2 -0
- package/src/hooks/{useBrands.ts → brands/useBrands.ts} +1 -1
- package/src/hooks/brands/usePaginatedBrands.ts +206 -0
- package/src/hooks/createPaginatedDataHook.ts +359 -0
- package/src/hooks/data-hook-errors.property.test.ts +4 -4
- package/src/hooks/data-hook-filters.property.test.ts +4 -4
- package/src/hooks/data-hooks.property.test.ts +4 -4
- package/src/hooks/index.ts +96 -8
- package/src/hooks/productInstances/index.ts +1 -0
- package/src/hooks/{useProductInstances.ts → productInstances/useProductInstances.ts} +9 -6
- package/src/hooks/products/index.ts +1 -0
- package/src/hooks/{useProducts.ts → products/useProducts.ts} +4 -5
- package/src/hooks/reservations/index.ts +2 -0
- package/src/hooks/reservations/usePaginatedReservations.ts +258 -0
- package/src/hooks/{useReservations.ts → reservations/useReservations.ts} +65 -10
- package/src/hooks/users/index.ts +2 -0
- package/src/hooks/users/usePaginatedUsers.ts +213 -0
- package/src/hooks/{useUsers.ts → users/useUsers.ts} +1 -1
- package/src/mutations/accounts/accounts.test.ts +287 -0
- package/src/mutations/{accounts.ts → accounts/accounts.ts} +2 -2
- package/src/mutations/accounts/index.ts +1 -0
- package/src/mutations/brands/brands.test.ts +292 -0
- package/src/mutations/{brands.ts → brands/brands.ts} +2 -2
- package/src/mutations/brands/index.ts +1 -0
- package/src/mutations/reservations/index.ts +1 -0
- package/src/mutations/{reservations.test.ts → reservations/reservations.test.ts} +1 -1
- package/src/mutations/{reservations.ts → reservations/reservations.ts} +2 -2
- package/src/mutations/users/index.ts +1 -0
- package/src/mutations/users/users.test.ts +289 -0
- package/src/mutations/{users.ts → users/users.ts} +2 -2
- package/src/queries/accounts/accounts.test.ts +228 -0
- package/src/queries/accounts/index.ts +1 -0
- package/src/queries/brands/brands.test.ts +288 -0
- package/src/queries/brands/index.ts +1 -0
- package/src/queries/products/index.ts +1 -0
- package/src/queries/products/products.test.ts +347 -0
- package/src/queries/reservations/index.ts +1 -0
- package/src/queries/users/index.ts +1 -0
- package/src/queries/users/users.test.ts +301 -0
- /package/src/queries/{accounts.ts → accounts/accounts.ts} +0 -0
- /package/src/queries/{brands.ts → brands/brands.ts} +0 -0
- /package/src/queries/{products.ts → products/products.ts} +0 -0
- /package/src/queries/{reservations.test.ts → reservations/reservations.test.ts} +0 -0
- /package/src/queries/{reservations.ts → reservations/reservations.ts} +0 -0
- /package/src/queries/{users.ts → users/users.ts} +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated Users Hooks
|
|
3
|
+
*
|
|
4
|
+
* Vue composables for fetching users with server-side pagination.
|
|
5
|
+
* Provides separate hooks for active and deleted users to enable
|
|
6
|
+
* efficient tab-based filtering in large datasets.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Ref, ComputedRef } from "vue";
|
|
10
|
+
import type { User } from "@htlkg/core/types";
|
|
11
|
+
import {
|
|
12
|
+
createPaginatedDataHook,
|
|
13
|
+
ACTIVE_FILTER,
|
|
14
|
+
DELETED_FILTER,
|
|
15
|
+
type PaginatedHookOptions,
|
|
16
|
+
type PaginationState,
|
|
17
|
+
} from "../createPaginatedDataHook";
|
|
18
|
+
|
|
19
|
+
/** Extended User type with computed fields */
|
|
20
|
+
export type UserWithRelations = User & {
|
|
21
|
+
accountName?: string;
|
|
22
|
+
brandNames?: string[];
|
|
23
|
+
deletedAt?: string;
|
|
24
|
+
deletedBy?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface UsePaginatedUsersOptions extends PaginatedHookOptions {
|
|
28
|
+
/** Filter by brand ID */
|
|
29
|
+
brandId?: string;
|
|
30
|
+
/** Filter by account ID */
|
|
31
|
+
accountId?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UsePaginatedUsersReturn {
|
|
35
|
+
/** Reactive array of users */
|
|
36
|
+
users: Ref<UserWithRelations[]>;
|
|
37
|
+
/** Loading state (true during any fetch) */
|
|
38
|
+
loading: Ref<boolean>;
|
|
39
|
+
/** Loading state for initial fetch */
|
|
40
|
+
initialLoading: Ref<boolean>;
|
|
41
|
+
/** Loading state for loadMore */
|
|
42
|
+
loadingMore: Ref<boolean>;
|
|
43
|
+
/** Error state */
|
|
44
|
+
error: Ref<Error | null>;
|
|
45
|
+
/** Pagination state */
|
|
46
|
+
pagination: Ref<PaginationState>;
|
|
47
|
+
/** Whether there are more items to load */
|
|
48
|
+
hasMore: ComputedRef<boolean>;
|
|
49
|
+
/** Load next page of data */
|
|
50
|
+
loadMore: () => Promise<void>;
|
|
51
|
+
/** Refetch data (resets to first page) */
|
|
52
|
+
refetch: () => Promise<void>;
|
|
53
|
+
/** Reset data and pagination state */
|
|
54
|
+
reset: () => void;
|
|
55
|
+
/** Update search filter and refetch from server (searches ALL data) */
|
|
56
|
+
setSearchFilter: (filter: any) => Promise<void>;
|
|
57
|
+
/** Current search filter */
|
|
58
|
+
searchFilter: Ref<any>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build additional filter from hook options
|
|
63
|
+
*/
|
|
64
|
+
function buildFilter(options: UsePaginatedUsersOptions): any {
|
|
65
|
+
const conditions: any[] = [];
|
|
66
|
+
|
|
67
|
+
if (options.brandId) {
|
|
68
|
+
conditions.push({ brandIds: { contains: options.brandId } });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (options.accountId) {
|
|
72
|
+
conditions.push({ accountId: { eq: options.accountId } });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
76
|
+
conditions.push(options.filter);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (conditions.length === 0) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (conditions.length === 1) {
|
|
83
|
+
return conditions[0];
|
|
84
|
+
}
|
|
85
|
+
return { and: conditions };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Transform user data
|
|
90
|
+
*/
|
|
91
|
+
function transformUser(user: any): UserWithRelations {
|
|
92
|
+
return {
|
|
93
|
+
...user,
|
|
94
|
+
accountName: user.account?.name || "",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Selection set for user queries
|
|
100
|
+
*/
|
|
101
|
+
const USER_SELECTION_SET = [
|
|
102
|
+
"id",
|
|
103
|
+
"email",
|
|
104
|
+
"accountId",
|
|
105
|
+
"brandIds",
|
|
106
|
+
"roles",
|
|
107
|
+
"status",
|
|
108
|
+
"lastLogin",
|
|
109
|
+
"createdAt",
|
|
110
|
+
"deletedAt",
|
|
111
|
+
"deletedBy",
|
|
112
|
+
"account.name",
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Internal hook for active users
|
|
117
|
+
*/
|
|
118
|
+
const useActiveUsersInternal = createPaginatedDataHook<UserWithRelations, UsePaginatedUsersOptions>({
|
|
119
|
+
model: "User",
|
|
120
|
+
dataPropertyName: "users",
|
|
121
|
+
defaultPageSize: 25,
|
|
122
|
+
selectionSet: USER_SELECTION_SET,
|
|
123
|
+
transform: transformUser,
|
|
124
|
+
buildFilter,
|
|
125
|
+
baseFilter: ACTIVE_FILTER,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Internal hook for deleted users
|
|
130
|
+
*/
|
|
131
|
+
const useDeletedUsersInternal = createPaginatedDataHook<UserWithRelations, UsePaginatedUsersOptions>({
|
|
132
|
+
model: "User",
|
|
133
|
+
dataPropertyName: "users",
|
|
134
|
+
defaultPageSize: 25,
|
|
135
|
+
selectionSet: USER_SELECTION_SET,
|
|
136
|
+
transform: transformUser,
|
|
137
|
+
buildFilter,
|
|
138
|
+
baseFilter: DELETED_FILTER,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Composable for fetching active (non-deleted) users with pagination
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* import { useActiveUsers } from '@htlkg/data/hooks';
|
|
147
|
+
*
|
|
148
|
+
* const { users, loading, hasMore, loadMore, refetch } = useActiveUsers({
|
|
149
|
+
* pageSize: 25,
|
|
150
|
+
* });
|
|
151
|
+
*
|
|
152
|
+
* // Load more when user scrolls or clicks "Load More"
|
|
153
|
+
* async function onLoadMore() {
|
|
154
|
+
* await loadMore();
|
|
155
|
+
* }
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @example With filters
|
|
159
|
+
* ```typescript
|
|
160
|
+
* const { users, loading } = useActiveUsers({
|
|
161
|
+
* accountId: 'account-123',
|
|
162
|
+
* brandId: 'brand-456',
|
|
163
|
+
* pageSize: 50
|
|
164
|
+
* });
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export function useActiveUsers(options: UsePaginatedUsersOptions = {}): UsePaginatedUsersReturn {
|
|
168
|
+
const result = useActiveUsersInternal(options);
|
|
169
|
+
return {
|
|
170
|
+
users: result.users as Ref<UserWithRelations[]>,
|
|
171
|
+
loading: result.loading,
|
|
172
|
+
initialLoading: result.initialLoading,
|
|
173
|
+
loadingMore: result.loadingMore,
|
|
174
|
+
error: result.error,
|
|
175
|
+
pagination: result.pagination,
|
|
176
|
+
hasMore: result.hasMore,
|
|
177
|
+
loadMore: result.loadMore,
|
|
178
|
+
refetch: result.refetch,
|
|
179
|
+
reset: result.reset,
|
|
180
|
+
setSearchFilter: result.setSearchFilter,
|
|
181
|
+
searchFilter: result.searchFilter,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Composable for fetching deleted users with pagination
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* import { useDeletedUsers } from '@htlkg/data/hooks';
|
|
191
|
+
*
|
|
192
|
+
* const { users, loading, hasMore, loadMore, refetch } = useDeletedUsers({
|
|
193
|
+
* pageSize: 25,
|
|
194
|
+
* });
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function useDeletedUsers(options: UsePaginatedUsersOptions = {}): UsePaginatedUsersReturn {
|
|
198
|
+
const result = useDeletedUsersInternal(options);
|
|
199
|
+
return {
|
|
200
|
+
users: result.users as Ref<UserWithRelations[]>,
|
|
201
|
+
loading: result.loading,
|
|
202
|
+
initialLoading: result.initialLoading,
|
|
203
|
+
loadingMore: result.loadingMore,
|
|
204
|
+
error: result.error,
|
|
205
|
+
pagination: result.pagination,
|
|
206
|
+
hasMore: result.hasMore,
|
|
207
|
+
loadMore: result.loadMore,
|
|
208
|
+
refetch: result.refetch,
|
|
209
|
+
reset: result.reset,
|
|
210
|
+
setSearchFilter: result.setSearchFilter,
|
|
211
|
+
searchFilter: result.searchFilter,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { Ref } from "vue";
|
|
9
9
|
import type { User } from "@htlkg/core/types";
|
|
10
|
-
import { createDataHook, type BaseHookOptions } from "
|
|
10
|
+
import { createDataHook, type BaseHookOptions } from "../createDataHook";
|
|
11
11
|
|
|
12
12
|
export interface UseUsersOptions extends BaseHookOptions {
|
|
13
13
|
/** Filter criteria for users */
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Mutation Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for account CRUD operations including soft delete and restoration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
createAccount,
|
|
10
|
+
updateAccount,
|
|
11
|
+
softDeleteAccount,
|
|
12
|
+
restoreAccount,
|
|
13
|
+
deleteAccount,
|
|
14
|
+
} from "./accounts";
|
|
15
|
+
|
|
16
|
+
// Mock the systemSettings query functions
|
|
17
|
+
vi.mock("../../queries/systemSettings", () => ({
|
|
18
|
+
checkRestoreEligibility: vi.fn((deletedAt: string | null, retentionDays: number) => {
|
|
19
|
+
if (!deletedAt) {
|
|
20
|
+
return { canRestore: false, daysRemaining: 0, daysExpired: 0 };
|
|
21
|
+
}
|
|
22
|
+
const deletedDate = new Date(deletedAt);
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const diffMs = now.getTime() - deletedDate.getTime();
|
|
25
|
+
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
|
26
|
+
|
|
27
|
+
if (diffDays > retentionDays) {
|
|
28
|
+
return {
|
|
29
|
+
canRestore: false,
|
|
30
|
+
daysRemaining: 0,
|
|
31
|
+
daysExpired: Math.floor(diffDays - retentionDays),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
canRestore: true,
|
|
37
|
+
daysRemaining: Math.ceil(retentionDays - diffDays),
|
|
38
|
+
daysExpired: 0,
|
|
39
|
+
};
|
|
40
|
+
}),
|
|
41
|
+
DEFAULT_SOFT_DELETE_RETENTION_DAYS: 30,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
describe("Account Mutations", () => {
|
|
45
|
+
describe("createAccount", () => {
|
|
46
|
+
const validInput = {
|
|
47
|
+
name: "Test Account",
|
|
48
|
+
subscription: { plan: "premium" },
|
|
49
|
+
status: "active" as const,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
it("should create an account with valid input", async () => {
|
|
53
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
54
|
+
data: { id: "account-001", ...validInput },
|
|
55
|
+
errors: null,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const mockClient = {
|
|
59
|
+
models: {
|
|
60
|
+
Account: { create: mockCreate },
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = await createAccount(mockClient, validInput);
|
|
65
|
+
|
|
66
|
+
expect(result).toBeTruthy();
|
|
67
|
+
expect(result?.id).toBe("account-001");
|
|
68
|
+
expect(mockCreate).toHaveBeenCalledWith(validInput);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should return null when GraphQL returns errors", async () => {
|
|
72
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
73
|
+
data: null,
|
|
74
|
+
errors: [{ message: "Validation error" }],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const mockClient = {
|
|
78
|
+
models: {
|
|
79
|
+
Account: { create: mockCreate },
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const result = await createAccount(mockClient, validInput);
|
|
84
|
+
|
|
85
|
+
expect(result).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should throw on unexpected error", async () => {
|
|
89
|
+
const mockCreate = vi.fn().mockRejectedValue(new Error("Network error"));
|
|
90
|
+
|
|
91
|
+
const mockClient = {
|
|
92
|
+
models: {
|
|
93
|
+
Account: { create: mockCreate },
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await expect(createAccount(mockClient, validInput)).rejects.toThrow("Network error");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("updateAccount", () => {
|
|
102
|
+
it("should update an account", async () => {
|
|
103
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
104
|
+
data: { id: "account-001", name: "Updated Account" },
|
|
105
|
+
errors: null,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const mockClient = {
|
|
109
|
+
models: {
|
|
110
|
+
Account: { update: mockUpdate },
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const result = await updateAccount(mockClient, {
|
|
115
|
+
id: "account-001",
|
|
116
|
+
name: "Updated Account",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(result).toBeTruthy();
|
|
120
|
+
expect(result?.name).toBe("Updated Account");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should return null on GraphQL error", async () => {
|
|
124
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
125
|
+
data: null,
|
|
126
|
+
errors: [{ message: "Update failed" }],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const mockClient = {
|
|
130
|
+
models: {
|
|
131
|
+
Account: { update: mockUpdate },
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const result = await updateAccount(mockClient, { id: "account-001" });
|
|
136
|
+
|
|
137
|
+
expect(result).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("softDeleteAccount", () => {
|
|
142
|
+
it("should set deletedAt and deletedBy", async () => {
|
|
143
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
144
|
+
data: { id: "account-001" },
|
|
145
|
+
errors: null,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const mockClient = {
|
|
149
|
+
models: {
|
|
150
|
+
Account: { update: mockUpdate },
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result = await softDeleteAccount(mockClient, "account-001", "admin@example.com");
|
|
155
|
+
|
|
156
|
+
expect(result).toBe(true);
|
|
157
|
+
const callArgs = mockUpdate.mock.calls[0][0];
|
|
158
|
+
expect(callArgs.id).toBe("account-001");
|
|
159
|
+
expect(callArgs.status).toBe("deleted");
|
|
160
|
+
expect(callArgs.deletedAt).toBeDefined();
|
|
161
|
+
expect(callArgs.deletedBy).toBe("admin@example.com");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should return false on GraphQL error", async () => {
|
|
165
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
166
|
+
data: null,
|
|
167
|
+
errors: [{ message: "Error" }],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const mockClient = {
|
|
171
|
+
models: {
|
|
172
|
+
Account: { update: mockUpdate },
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const result = await softDeleteAccount(mockClient, "account-001", "admin@example.com");
|
|
177
|
+
|
|
178
|
+
expect(result).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("restoreAccount", () => {
|
|
183
|
+
it("should restore a recently deleted account", async () => {
|
|
184
|
+
const recentDeletedAt = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString();
|
|
185
|
+
|
|
186
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
187
|
+
data: { id: "account-001", deletedAt: recentDeletedAt },
|
|
188
|
+
errors: null,
|
|
189
|
+
});
|
|
190
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
191
|
+
data: { id: "account-001" },
|
|
192
|
+
errors: null,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const mockClient = {
|
|
196
|
+
models: {
|
|
197
|
+
Account: { get: mockGet, update: mockUpdate },
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const result = await restoreAccount(mockClient, "account-001");
|
|
202
|
+
|
|
203
|
+
expect(result.success).toBe(true);
|
|
204
|
+
expect(mockUpdate).toHaveBeenCalledWith({
|
|
205
|
+
id: "account-001",
|
|
206
|
+
status: "active",
|
|
207
|
+
deletedAt: null,
|
|
208
|
+
deletedBy: null,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should reject restoration after retention period expires", async () => {
|
|
213
|
+
const oldDeletedAt = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString();
|
|
214
|
+
|
|
215
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
216
|
+
data: { id: "account-001", deletedAt: oldDeletedAt },
|
|
217
|
+
errors: null,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const mockClient = {
|
|
221
|
+
models: {
|
|
222
|
+
Account: { get: mockGet, update: vi.fn() },
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const result = await restoreAccount(mockClient, "account-001", 30);
|
|
227
|
+
|
|
228
|
+
expect(result.success).toBe(false);
|
|
229
|
+
expect(result.error).toContain("Retention period");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should return error when account not found", async () => {
|
|
233
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
234
|
+
data: null,
|
|
235
|
+
errors: [{ message: "Not found" }],
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const mockClient = {
|
|
239
|
+
models: {
|
|
240
|
+
Account: { get: mockGet, update: vi.fn() },
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const result = await restoreAccount(mockClient, "nonexistent");
|
|
245
|
+
|
|
246
|
+
expect(result.success).toBe(false);
|
|
247
|
+
expect(result.error).toBe("Account not found");
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("deleteAccount", () => {
|
|
252
|
+
it("should permanently delete an account", async () => {
|
|
253
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
254
|
+
data: { id: "account-001" },
|
|
255
|
+
errors: null,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const mockClient = {
|
|
259
|
+
models: {
|
|
260
|
+
Account: { delete: mockDelete },
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const result = await deleteAccount(mockClient, "account-001");
|
|
265
|
+
|
|
266
|
+
expect(result).toBe(true);
|
|
267
|
+
expect(mockDelete).toHaveBeenCalledWith({ id: "account-001" });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should return false on GraphQL error", async () => {
|
|
271
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
272
|
+
data: null,
|
|
273
|
+
errors: [{ message: "Cannot delete" }],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const mockClient = {
|
|
277
|
+
models: {
|
|
278
|
+
Account: { delete: mockDelete },
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const result = await deleteAccount(mockClient, "account-001");
|
|
283
|
+
|
|
284
|
+
expect(result).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -8,8 +8,8 @@ import type { Account } from "@htlkg/core/types";
|
|
|
8
8
|
import {
|
|
9
9
|
checkRestoreEligibility,
|
|
10
10
|
DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
11
|
-
} from "
|
|
12
|
-
import type { CreateAuditFields, UpdateWithSoftDeleteFields } from "
|
|
11
|
+
} from "../../queries/systemSettings";
|
|
12
|
+
import type { CreateAuditFields, UpdateWithSoftDeleteFields } from "../common";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Input type for creating an account
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./accounts";
|