@htlkg/data 0.0.14 → 0.0.16
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/README.md +72 -0
- package/dist/client/index.d.ts +123 -30
- package/dist/client/index.js +75 -1
- package/dist/client/index.js.map +1 -1
- package/dist/hooks/index.d.ts +76 -2
- package/dist/hooks/index.js +224 -6
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +550 -7
- package/dist/index.js.map +1 -1
- package/dist/mutations/index.d.ts +149 -5
- package/dist/mutations/index.js +397 -0
- package/dist/mutations/index.js.map +1 -1
- package/dist/productInstances-CzT3NZKU.d.ts +98 -0
- package/dist/queries/index.d.ts +54 -2
- package/dist/queries/index.js +60 -1
- package/dist/queries/index.js.map +1 -1
- package/dist/server/index.d.ts +47 -0
- package/dist/server/index.js +59 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +5 -1
- package/src/client/index.ts +82 -3
- package/src/client/proxy.ts +170 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useProductInstances.ts +174 -0
- package/src/index.ts +11 -0
- package/src/mutations/accounts.ts +102 -1
- package/src/mutations/brands.ts +102 -1
- package/src/mutations/index.ts +23 -0
- package/src/mutations/productInstances/index.ts +14 -0
- package/src/mutations/productInstances/productInstances.integration.test.ts +621 -0
- package/src/mutations/productInstances/productInstances.test.ts +680 -0
- package/src/mutations/productInstances/productInstances.ts +280 -0
- package/src/mutations/systemSettings.ts +130 -0
- package/src/mutations/users.ts +102 -1
- package/src/queries/index.ts +9 -0
- package/src/queries/systemSettings.ts +115 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProductInstance Mutation Functions
|
|
3
|
+
*
|
|
4
|
+
* Provides mutation functions for creating, updating, and deleting product instances.
|
|
5
|
+
* Product instances represent enabled products for a specific brand with their configuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ProductInstance } from "@htlkg/core/types";
|
|
9
|
+
import { getClientUser } from "@htlkg/core/auth";
|
|
10
|
+
import { getCurrentTimestamp } from "@htlkg/core/utils";
|
|
11
|
+
import { AppError } from "@htlkg/core/errors";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get current user identifier for audit trails
|
|
15
|
+
* Uses getClientUser() and returns email or username, falling back to "system"
|
|
16
|
+
*/
|
|
17
|
+
async function getUserIdentifier(fallback = "system"): Promise<string> {
|
|
18
|
+
try {
|
|
19
|
+
const user = await getClientUser();
|
|
20
|
+
if (user) {
|
|
21
|
+
return user.email || user.username || fallback;
|
|
22
|
+
}
|
|
23
|
+
return fallback;
|
|
24
|
+
} catch {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Input type for creating a product instance
|
|
31
|
+
*/
|
|
32
|
+
export interface CreateProductInstanceInput {
|
|
33
|
+
productId: string;
|
|
34
|
+
brandId: string;
|
|
35
|
+
accountId: string;
|
|
36
|
+
productName: string;
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
config?: Record<string, any>;
|
|
39
|
+
version?: string;
|
|
40
|
+
lastUpdated?: string;
|
|
41
|
+
updatedBy?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Input type for updating a product instance
|
|
46
|
+
*/
|
|
47
|
+
export interface UpdateProductInstanceInput {
|
|
48
|
+
id: string;
|
|
49
|
+
enabled?: boolean;
|
|
50
|
+
config?: Record<string, any>;
|
|
51
|
+
version?: string;
|
|
52
|
+
lastUpdated?: string;
|
|
53
|
+
updatedBy?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a new product instance
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import { createProductInstance } from '@htlkg/data/mutations';
|
|
62
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
63
|
+
*
|
|
64
|
+
* const client = generateClient<Schema>();
|
|
65
|
+
* const instance = await createProductInstance(client, {
|
|
66
|
+
* productId: 'product-123',
|
|
67
|
+
* brandId: 'brand-456',
|
|
68
|
+
* accountId: 'account-789',
|
|
69
|
+
* enabled: true,
|
|
70
|
+
* config: { apiKey: 'xxx', maxRequests: 100 }
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export async function createProductInstance<TClient = any>(
|
|
75
|
+
client: TClient,
|
|
76
|
+
input: CreateProductInstanceInput,
|
|
77
|
+
): Promise<ProductInstance | null> {
|
|
78
|
+
try {
|
|
79
|
+
// Build input - manually construct to avoid Vue Proxy issues
|
|
80
|
+
const createInput: any = {
|
|
81
|
+
productId: input.productId,
|
|
82
|
+
productName: input.productName,
|
|
83
|
+
brandId: input.brandId,
|
|
84
|
+
accountId: input.accountId,
|
|
85
|
+
enabled: input.enabled,
|
|
86
|
+
version: input.version,
|
|
87
|
+
lastUpdated: input.lastUpdated || getCurrentTimestamp(),
|
|
88
|
+
updatedBy: input.updatedBy || await getUserIdentifier(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// AWSJSON type requires JSON STRING
|
|
92
|
+
if (input.config) {
|
|
93
|
+
// Double stringify: first to strip Vue Proxy, second to create JSON string
|
|
94
|
+
createInput.config = JSON.stringify(JSON.parse(JSON.stringify(input.config)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log("[createProductInstance] Config as string:", createInput.config);
|
|
98
|
+
console.log("[createProductInstance] Config type:", typeof createInput.config);
|
|
99
|
+
|
|
100
|
+
const { data, errors } = await (client as any).models.ProductInstance.create(createInput);
|
|
101
|
+
|
|
102
|
+
if (errors) {
|
|
103
|
+
console.error("[createProductInstance] GraphQL errors:", errors);
|
|
104
|
+
throw new AppError(
|
|
105
|
+
"Failed to create product instance",
|
|
106
|
+
"PRODUCT_INSTANCE_CREATE_ERROR",
|
|
107
|
+
500,
|
|
108
|
+
{ errors }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return data as ProductInstance;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error("[createProductInstance] Error creating product instance:", error);
|
|
115
|
+
if (error instanceof AppError) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
throw new AppError(
|
|
119
|
+
"Failed to create product instance",
|
|
120
|
+
"PRODUCT_INSTANCE_CREATE_ERROR",
|
|
121
|
+
500,
|
|
122
|
+
{ originalError: error }
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Update an existing product instance
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* import { updateProductInstance } from '@htlkg/data/mutations';
|
|
133
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
134
|
+
*
|
|
135
|
+
* const client = generateClient<Schema>();
|
|
136
|
+
* const instance = await updateProductInstance(client, {
|
|
137
|
+
* id: 'instance-123',
|
|
138
|
+
* enabled: false,
|
|
139
|
+
* config: { apiKey: 'new-key', maxRequests: 200 }
|
|
140
|
+
* });
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export async function updateProductInstance<TClient = any>(
|
|
144
|
+
client: TClient,
|
|
145
|
+
input: UpdateProductInstanceInput,
|
|
146
|
+
): Promise<ProductInstance | null> {
|
|
147
|
+
try {
|
|
148
|
+
// Add timestamp and user metadata if not provided
|
|
149
|
+
// Convert config from Vue Proxy to plain object
|
|
150
|
+
const updateInput: any = {
|
|
151
|
+
...input,
|
|
152
|
+
lastUpdated: input.lastUpdated || getCurrentTimestamp(),
|
|
153
|
+
updatedBy: input.updatedBy || await getUserIdentifier(),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// AWSJSON type requires JSON STRING
|
|
157
|
+
if (input.config) {
|
|
158
|
+
updateInput.config = JSON.stringify(JSON.parse(JSON.stringify(input.config)));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { data, errors } = await (client as any).models.ProductInstance.update(updateInput);
|
|
162
|
+
|
|
163
|
+
if (errors) {
|
|
164
|
+
console.error("[updateProductInstance] GraphQL errors:", errors);
|
|
165
|
+
throw new AppError(
|
|
166
|
+
"Failed to update product instance",
|
|
167
|
+
"PRODUCT_INSTANCE_UPDATE_ERROR",
|
|
168
|
+
500,
|
|
169
|
+
{ errors }
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return data as ProductInstance;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error("[updateProductInstance] Error updating product instance:", error);
|
|
176
|
+
if (error instanceof AppError) {
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
throw new AppError(
|
|
180
|
+
"Failed to update product instance",
|
|
181
|
+
"PRODUCT_INSTANCE_UPDATE_ERROR",
|
|
182
|
+
500,
|
|
183
|
+
{ originalError: error }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Delete a product instance
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* import { deleteProductInstance } from '@htlkg/data/mutations';
|
|
194
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
195
|
+
*
|
|
196
|
+
* const client = generateClient<Schema>();
|
|
197
|
+
* await deleteProductInstance(client, 'instance-123');
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export async function deleteProductInstance<TClient = any>(
|
|
201
|
+
client: TClient,
|
|
202
|
+
id: string,
|
|
203
|
+
): Promise<boolean> {
|
|
204
|
+
try {
|
|
205
|
+
const { errors } = await (client as any).models.ProductInstance.delete({ id });
|
|
206
|
+
|
|
207
|
+
if (errors) {
|
|
208
|
+
console.error("[deleteProductInstance] GraphQL errors:", errors);
|
|
209
|
+
throw new AppError(
|
|
210
|
+
"Failed to delete product instance",
|
|
211
|
+
"PRODUCT_INSTANCE_DELETE_ERROR",
|
|
212
|
+
500,
|
|
213
|
+
{ errors }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return true;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error("[deleteProductInstance] Error deleting product instance:", error);
|
|
220
|
+
if (error instanceof AppError) {
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
throw new AppError(
|
|
224
|
+
"Failed to delete product instance",
|
|
225
|
+
"PRODUCT_INSTANCE_DELETE_ERROR",
|
|
226
|
+
500,
|
|
227
|
+
{ originalError: error }
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Toggle the enabled status of a product instance
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* import { toggleProductInstanceEnabled } from '@htlkg/data/mutations';
|
|
238
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
239
|
+
*
|
|
240
|
+
* const client = generateClient<Schema>();
|
|
241
|
+
* const instance = await toggleProductInstanceEnabled(client, 'instance-123', true);
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export async function toggleProductInstanceEnabled<TClient = any>(
|
|
245
|
+
client: TClient,
|
|
246
|
+
id: string,
|
|
247
|
+
enabled: boolean,
|
|
248
|
+
): Promise<ProductInstance | null> {
|
|
249
|
+
try {
|
|
250
|
+
const { data, errors } = await (client as any).models.ProductInstance.update({
|
|
251
|
+
id,
|
|
252
|
+
enabled,
|
|
253
|
+
lastUpdated: getCurrentTimestamp(),
|
|
254
|
+
updatedBy: await getUserIdentifier(),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (errors) {
|
|
258
|
+
console.error("[toggleProductInstanceEnabled] GraphQL errors:", errors);
|
|
259
|
+
throw new AppError(
|
|
260
|
+
"Failed to toggle product instance",
|
|
261
|
+
"PRODUCT_INSTANCE_TOGGLE_ERROR",
|
|
262
|
+
500,
|
|
263
|
+
{ errors }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return data as ProductInstance;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error("[toggleProductInstanceEnabled] Error toggling product instance:", error);
|
|
270
|
+
if (error instanceof AppError) {
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
throw new AppError(
|
|
274
|
+
"Failed to toggle product instance",
|
|
275
|
+
"PRODUCT_INSTANCE_TOGGLE_ERROR",
|
|
276
|
+
500,
|
|
277
|
+
{ originalError: error }
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SystemSettings Mutation Functions
|
|
3
|
+
*
|
|
4
|
+
* Provides mutation functions for managing system settings.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SystemSettings } from "@htlkg/core/types";
|
|
8
|
+
import {
|
|
9
|
+
SYSTEM_SETTINGS_KEY,
|
|
10
|
+
DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
11
|
+
} from "../queries/systemSettings";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Input type for updating system settings
|
|
15
|
+
*/
|
|
16
|
+
export interface UpdateSystemSettingsInput {
|
|
17
|
+
softDeleteRetentionDays: number;
|
|
18
|
+
updatedBy: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Update or create system settings
|
|
23
|
+
* Only SUPER_ADMINS can update system settings
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { updateSystemSettings } from '@htlkg/data/mutations';
|
|
28
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
29
|
+
*
|
|
30
|
+
* const client = generateClient<Schema>();
|
|
31
|
+
* const settings = await updateSystemSettings(client, {
|
|
32
|
+
* softDeleteRetentionDays: 60,
|
|
33
|
+
* updatedBy: 'admin@example.com'
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export async function updateSystemSettings<TClient = any>(
|
|
38
|
+
client: TClient,
|
|
39
|
+
input: UpdateSystemSettingsInput,
|
|
40
|
+
): Promise<SystemSettings | null> {
|
|
41
|
+
try {
|
|
42
|
+
// Validate retention days (minimum 1 day, maximum 365 days)
|
|
43
|
+
if (input.softDeleteRetentionDays < 1 || input.softDeleteRetentionDays > 365) {
|
|
44
|
+
console.error(
|
|
45
|
+
"[updateSystemSettings] Invalid retention days. Must be between 1 and 365.",
|
|
46
|
+
);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// First, try to get existing settings
|
|
51
|
+
const { data: existing } = await (client as any).models.SystemSettings.get({
|
|
52
|
+
key: SYSTEM_SETTINGS_KEY,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const settingsData = {
|
|
56
|
+
key: SYSTEM_SETTINGS_KEY,
|
|
57
|
+
softDeleteRetentionDays: input.softDeleteRetentionDays,
|
|
58
|
+
updatedAt: new Date().toISOString(),
|
|
59
|
+
updatedBy: input.updatedBy,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let result;
|
|
63
|
+
if (existing) {
|
|
64
|
+
// Update existing settings
|
|
65
|
+
result = await (client as any).models.SystemSettings.update(settingsData);
|
|
66
|
+
} else {
|
|
67
|
+
// Create new settings
|
|
68
|
+
result = await (client as any).models.SystemSettings.create(settingsData);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (result.errors) {
|
|
72
|
+
console.error("[updateSystemSettings] GraphQL errors:", result.errors);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result.data as SystemSettings;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error("[updateSystemSettings] Error updating system settings:", error);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Initialize system settings with default values if they don't exist
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* import { initializeSystemSettings } from '@htlkg/data/mutations';
|
|
89
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
90
|
+
*
|
|
91
|
+
* const client = generateClient<Schema>();
|
|
92
|
+
* const settings = await initializeSystemSettings(client, 'system@hotelinking.com');
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export async function initializeSystemSettings<TClient = any>(
|
|
96
|
+
client: TClient,
|
|
97
|
+
initializedBy: string,
|
|
98
|
+
): Promise<SystemSettings | null> {
|
|
99
|
+
try {
|
|
100
|
+
// Check if settings already exist
|
|
101
|
+
const { data: existing } = await (client as any).models.SystemSettings.get({
|
|
102
|
+
key: SYSTEM_SETTINGS_KEY,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (existing) {
|
|
106
|
+
return existing as SystemSettings;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Create default settings
|
|
110
|
+
const { data, errors } = await (client as any).models.SystemSettings.create({
|
|
111
|
+
key: SYSTEM_SETTINGS_KEY,
|
|
112
|
+
softDeleteRetentionDays: DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
113
|
+
updatedAt: new Date().toISOString(),
|
|
114
|
+
updatedBy: initializedBy,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (errors) {
|
|
118
|
+
console.error("[initializeSystemSettings] GraphQL errors:", errors);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return data as SystemSettings;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(
|
|
125
|
+
"[initializeSystemSettings] Error initializing system settings:",
|
|
126
|
+
error,
|
|
127
|
+
);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/mutations/users.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { User } from "@htlkg/core/types";
|
|
8
|
+
import {
|
|
9
|
+
checkRestoreEligibility,
|
|
10
|
+
DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
11
|
+
} from "../queries/systemSettings";
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Input type for creating a user
|
|
@@ -105,7 +109,104 @@ export async function updateUser<TClient = any>(
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
/**
|
|
108
|
-
*
|
|
112
|
+
* Soft delete a user (sets status to "deleted" instead of removing)
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* import { softDeleteUser } from '@htlkg/data/mutations';
|
|
117
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
118
|
+
*
|
|
119
|
+
* const client = generateClient<Schema>();
|
|
120
|
+
* await softDeleteUser(client, 'user-123', 'admin@example.com');
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export async function softDeleteUser<TClient = any>(
|
|
124
|
+
client: TClient,
|
|
125
|
+
id: string,
|
|
126
|
+
deletedBy: string,
|
|
127
|
+
): Promise<boolean> {
|
|
128
|
+
try {
|
|
129
|
+
const { errors } = await (client as any).models.User.update({
|
|
130
|
+
id,
|
|
131
|
+
status: "deleted",
|
|
132
|
+
deletedAt: new Date().toISOString(),
|
|
133
|
+
deletedBy,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (errors) {
|
|
137
|
+
console.error("[softDeleteUser] GraphQL errors:", errors);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return true;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error("[softDeleteUser] Error soft-deleting user:", error);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Restore a soft-deleted user (sets status back to "active")
|
|
150
|
+
* Checks retention period before allowing restoration
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* import { restoreUser } from '@htlkg/data/mutations';
|
|
155
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
156
|
+
*
|
|
157
|
+
* const client = generateClient<Schema>();
|
|
158
|
+
* await restoreUser(client, 'user-123');
|
|
159
|
+
* // Or with custom retention days:
|
|
160
|
+
* await restoreUser(client, 'user-123', 60);
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export async function restoreUser<TClient = any>(
|
|
164
|
+
client: TClient,
|
|
165
|
+
id: string,
|
|
166
|
+
retentionDays: number = DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
167
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
168
|
+
try {
|
|
169
|
+
// First, get the user to check its deletedAt timestamp
|
|
170
|
+
const { data: user, errors: getErrors } = await (
|
|
171
|
+
client as any
|
|
172
|
+
).models.User.get({ id });
|
|
173
|
+
|
|
174
|
+
if (getErrors || !user) {
|
|
175
|
+
console.error("[restoreUser] Error fetching user:", getErrors);
|
|
176
|
+
return { success: false, error: "User not found" };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check if restoration is allowed based on retention period
|
|
180
|
+
const eligibility = checkRestoreEligibility(user.deletedAt, retentionDays);
|
|
181
|
+
|
|
182
|
+
if (!eligibility.canRestore) {
|
|
183
|
+
const errorMsg = `Cannot restore user. Retention period of ${retentionDays} days has expired. Item was deleted ${eligibility.daysExpired} days ago.`;
|
|
184
|
+
console.error("[restoreUser]", errorMsg);
|
|
185
|
+
return { success: false, error: errorMsg };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { errors } = await (client as any).models.User.update({
|
|
189
|
+
id,
|
|
190
|
+
status: "active",
|
|
191
|
+
deletedAt: null,
|
|
192
|
+
deletedBy: null,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (errors) {
|
|
196
|
+
console.error("[restoreUser] GraphQL errors:", errors);
|
|
197
|
+
return { success: false, error: "Failed to restore user" };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { success: true };
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error("[restoreUser] Error restoring user:", error);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Hard delete a user (permanently removes from database)
|
|
209
|
+
* Use with caution - prefer softDeleteUser for recoverable deletion
|
|
109
210
|
*
|
|
110
211
|
* @example
|
|
111
212
|
* ```typescript
|
package/src/queries/index.ts
CHANGED
|
@@ -43,3 +43,12 @@ export {
|
|
|
43
43
|
|
|
44
44
|
// Server-side query helpers
|
|
45
45
|
export { executeServerQuery, executePublicQuery } from "./server-helpers";
|
|
46
|
+
|
|
47
|
+
// SystemSettings queries
|
|
48
|
+
export {
|
|
49
|
+
getSystemSettings,
|
|
50
|
+
getSoftDeleteRetentionDays,
|
|
51
|
+
checkRestoreEligibility,
|
|
52
|
+
SYSTEM_SETTINGS_KEY,
|
|
53
|
+
DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
54
|
+
} from "./systemSettings";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SystemSettings Query Functions
|
|
3
|
+
*
|
|
4
|
+
* Provides query functions for fetching system settings from the GraphQL API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SystemSettings } from "@htlkg/core/types";
|
|
8
|
+
|
|
9
|
+
/** Default retention days if no settings exist */
|
|
10
|
+
export const DEFAULT_SOFT_DELETE_RETENTION_DAYS = 30;
|
|
11
|
+
|
|
12
|
+
/** The key used for global system settings */
|
|
13
|
+
export const SYSTEM_SETTINGS_KEY = "GLOBAL";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get system settings
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { getSystemSettings } from '@htlkg/data/queries';
|
|
21
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
22
|
+
*
|
|
23
|
+
* const client = generateClient<Schema>();
|
|
24
|
+
* const settings = await getSystemSettings(client);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export async function getSystemSettings<TClient = any>(
|
|
28
|
+
client: TClient,
|
|
29
|
+
): Promise<SystemSettings | null> {
|
|
30
|
+
try {
|
|
31
|
+
const { data, errors } = await (client as any).models.SystemSettings.get({
|
|
32
|
+
key: SYSTEM_SETTINGS_KEY,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (errors) {
|
|
36
|
+
console.error("[getSystemSettings] GraphQL errors:", errors);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return data as SystemSettings;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("[getSystemSettings] Error fetching system settings:", error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the soft delete retention days from system settings
|
|
49
|
+
* Returns the default (30 days) if settings don't exist
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* import { getSoftDeleteRetentionDays } from '@htlkg/data/queries';
|
|
54
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
55
|
+
*
|
|
56
|
+
* const client = generateClient<Schema>();
|
|
57
|
+
* const days = await getSoftDeleteRetentionDays(client);
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export async function getSoftDeleteRetentionDays<TClient = any>(
|
|
61
|
+
client: TClient,
|
|
62
|
+
): Promise<number> {
|
|
63
|
+
const settings = await getSystemSettings(client);
|
|
64
|
+
return settings?.softDeleteRetentionDays ?? DEFAULT_SOFT_DELETE_RETENTION_DAYS;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a soft-deleted item can still be restored based on retention period
|
|
69
|
+
*
|
|
70
|
+
* @param deletedAt - The ISO date string when the item was deleted
|
|
71
|
+
* @param retentionDays - Number of days items can be restored
|
|
72
|
+
* @returns Object with canRestore flag and daysRemaining/daysExpired
|
|
73
|
+
*/
|
|
74
|
+
export function checkRestoreEligibility(
|
|
75
|
+
deletedAt: string | undefined | null,
|
|
76
|
+
retentionDays: number,
|
|
77
|
+
): {
|
|
78
|
+
canRestore: boolean;
|
|
79
|
+
daysRemaining: number;
|
|
80
|
+
daysExpired: number;
|
|
81
|
+
expiresAt: Date | null;
|
|
82
|
+
} {
|
|
83
|
+
if (!deletedAt) {
|
|
84
|
+
return {
|
|
85
|
+
canRestore: false,
|
|
86
|
+
daysRemaining: 0,
|
|
87
|
+
daysExpired: 0,
|
|
88
|
+
expiresAt: null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const deletedDate = new Date(deletedAt);
|
|
93
|
+
const expiresAt = new Date(deletedDate);
|
|
94
|
+
expiresAt.setDate(expiresAt.getDate() + retentionDays);
|
|
95
|
+
|
|
96
|
+
const now = new Date();
|
|
97
|
+
const msRemaining = expiresAt.getTime() - now.getTime();
|
|
98
|
+
const daysRemaining = Math.ceil(msRemaining / (1000 * 60 * 60 * 24));
|
|
99
|
+
|
|
100
|
+
if (daysRemaining <= 0) {
|
|
101
|
+
return {
|
|
102
|
+
canRestore: false,
|
|
103
|
+
daysRemaining: 0,
|
|
104
|
+
daysExpired: Math.abs(daysRemaining),
|
|
105
|
+
expiresAt,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
canRestore: true,
|
|
111
|
+
daysRemaining,
|
|
112
|
+
daysExpired: 0,
|
|
113
|
+
expiresAt,
|
|
114
|
+
};
|
|
115
|
+
}
|