@htlkg/data 0.0.15 → 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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * GraphQL Proxy Client
3
+ *
4
+ * Client-side helper for making authenticated GraphQL operations through
5
+ * the server-side proxy. This allows Vue components to perform mutations
6
+ * while keeping auth cookies httpOnly for security.
7
+ *
8
+ * @module @htlkg/data/proxy
9
+ */
10
+
11
+ /**
12
+ * Valid GraphQL operations
13
+ */
14
+ export type Operation = "create" | "update" | "delete" | "get" | "list";
15
+
16
+ /**
17
+ * Response type from GraphQL operations
18
+ */
19
+ export interface GraphQLResponse<T = any> {
20
+ data: T | null;
21
+ errors?: Array<{ message: string; [key: string]: any }>;
22
+ }
23
+
24
+ /**
25
+ * Options for proxy requests
26
+ */
27
+ export interface ProxyOptions {
28
+ /** Custom API endpoint (default: /api/graphql) */
29
+ endpoint?: string;
30
+ /** Additional fetch options */
31
+ fetchOptions?: RequestInit;
32
+ }
33
+
34
+ /**
35
+ * Default proxy endpoint
36
+ */
37
+ const DEFAULT_ENDPOINT = "/api/graphql";
38
+
39
+ /**
40
+ * Execute a GraphQL mutation through the server proxy
41
+ *
42
+ * @param model - The model name (e.g., 'User', 'Brand', 'Account')
43
+ * @param operation - The operation type ('create', 'update', 'delete')
44
+ * @param data - The operation data/input
45
+ * @param options - Optional configuration
46
+ * @returns Promise with the operation result
47
+ *
48
+ * @example Create
49
+ * ```typescript
50
+ * const result = await mutate('User', 'create', {
51
+ * email: 'user@example.com',
52
+ * accountId: '123',
53
+ * });
54
+ * ```
55
+ *
56
+ * @example Update
57
+ * ```typescript
58
+ * const result = await mutate('User', 'update', {
59
+ * id: 'user-id',
60
+ * status: 'deleted',
61
+ * deletedAt: new Date().toISOString(),
62
+ * });
63
+ * ```
64
+ *
65
+ * @example Delete
66
+ * ```typescript
67
+ * const result = await mutate('User', 'delete', { id: 'user-id' });
68
+ * ```
69
+ */
70
+ export async function mutate<T = any>(
71
+ model: string,
72
+ operation: "create" | "update" | "delete",
73
+ data: Record<string, any>,
74
+ options?: ProxyOptions,
75
+ ): Promise<GraphQLResponse<T>> {
76
+ return proxyRequest<T>(model, operation, data, options);
77
+ }
78
+
79
+ /**
80
+ * Execute a GraphQL query through the server proxy
81
+ *
82
+ * @param model - The model name (e.g., 'User', 'Brand', 'Account')
83
+ * @param operation - The operation type ('get', 'list')
84
+ * @param data - The query parameters (id for get, filter/limit for list)
85
+ * @param options - Optional configuration
86
+ * @returns Promise with the query result
87
+ *
88
+ * @example Get by ID
89
+ * ```typescript
90
+ * const result = await query('User', 'get', { id: 'user-id' });
91
+ * ```
92
+ *
93
+ * @example List with filter
94
+ * ```typescript
95
+ * const result = await query('User', 'list', {
96
+ * filter: { status: { eq: 'active' } },
97
+ * limit: 100,
98
+ * });
99
+ * ```
100
+ */
101
+ export async function query<T = any>(
102
+ model: string,
103
+ operation: "get" | "list",
104
+ data?: Record<string, any>,
105
+ options?: ProxyOptions,
106
+ ): Promise<GraphQLResponse<T>> {
107
+ return proxyRequest<T>(model, operation, data, options);
108
+ }
109
+
110
+ /**
111
+ * Internal function to make proxy requests
112
+ */
113
+ async function proxyRequest<T>(
114
+ model: string,
115
+ operation: Operation,
116
+ data?: Record<string, any>,
117
+ options?: ProxyOptions,
118
+ ): Promise<GraphQLResponse<T>> {
119
+ const endpoint = options?.endpoint ?? DEFAULT_ENDPOINT;
120
+
121
+ try {
122
+ const response = await fetch(endpoint, {
123
+ method: "POST",
124
+ headers: {
125
+ "Content-Type": "application/json",
126
+ },
127
+ credentials: "include", // Important: include cookies
128
+ ...options?.fetchOptions,
129
+ body: JSON.stringify({ model, operation, data }),
130
+ });
131
+
132
+ const result: GraphQLResponse<T> = await response.json();
133
+
134
+ // Handle auth errors
135
+ if (response.status === 401) {
136
+ console.error("[GraphQL Proxy] Unauthorized - session may have expired");
137
+ // Optionally trigger a redirect to login
138
+ // window.location.href = '/login';
139
+ }
140
+
141
+ return result;
142
+ } catch (error) {
143
+ console.error("[GraphQL Proxy] Request failed:", error);
144
+ return {
145
+ data: null,
146
+ errors: [
147
+ {
148
+ message: error instanceof Error ? error.message : "Network error",
149
+ },
150
+ ],
151
+ };
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Helper to check if a response has errors
157
+ */
158
+ export function hasErrors(response: GraphQLResponse): boolean {
159
+ return !!response.errors && response.errors.length > 0;
160
+ }
161
+
162
+ /**
163
+ * Helper to get the first error message from a response
164
+ */
165
+ export function getErrorMessage(response: GraphQLResponse): string | null {
166
+ if (!response.errors || response.errors.length === 0) {
167
+ return null;
168
+ }
169
+ return response.errors[0].message;
170
+ }
package/src/index.ts CHANGED
@@ -16,6 +16,17 @@ export {
16
16
  type AstroAmplifyConfig,
17
17
  } from "./client";
18
18
 
19
+ // Proxy exports (for authenticated client-side operations)
20
+ export {
21
+ mutate,
22
+ query,
23
+ hasErrors,
24
+ getErrorMessage,
25
+ type Operation,
26
+ type GraphQLResponse,
27
+ type ProxyOptions,
28
+ } from "./client";
29
+
19
30
  // Query exports
20
31
  export * from "./queries";
21
32
 
@@ -5,6 +5,10 @@
5
5
  */
6
6
 
7
7
  import type { Account } 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 an account
@@ -96,7 +100,104 @@ export async function updateAccount<TClient = any>(
96
100
  }
97
101
 
98
102
  /**
99
- * Delete an account
103
+ * Soft delete an account (sets status to "deleted" instead of removing)
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * import { softDeleteAccount } from '@htlkg/data/mutations';
108
+ * import { generateClient } from '@htlkg/data/client';
109
+ *
110
+ * const client = generateClient<Schema>();
111
+ * await softDeleteAccount(client, 'account-123', 'admin@example.com');
112
+ * ```
113
+ */
114
+ export async function softDeleteAccount<TClient = any>(
115
+ client: TClient,
116
+ id: string,
117
+ deletedBy: string,
118
+ ): Promise<boolean> {
119
+ try {
120
+ const { errors } = await (client as any).models.Account.update({
121
+ id,
122
+ status: "deleted",
123
+ deletedAt: new Date().toISOString(),
124
+ deletedBy,
125
+ });
126
+
127
+ if (errors) {
128
+ console.error("[softDeleteAccount] GraphQL errors:", errors);
129
+ return false;
130
+ }
131
+
132
+ return true;
133
+ } catch (error) {
134
+ console.error("[softDeleteAccount] Error soft-deleting account:", error);
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Restore a soft-deleted account (sets status back to "active")
141
+ * Checks retention period before allowing restoration
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * import { restoreAccount } from '@htlkg/data/mutations';
146
+ * import { generateClient } from '@htlkg/data/client';
147
+ *
148
+ * const client = generateClient<Schema>();
149
+ * await restoreAccount(client, 'account-123');
150
+ * // Or with custom retention days:
151
+ * await restoreAccount(client, 'account-123', 60);
152
+ * ```
153
+ */
154
+ export async function restoreAccount<TClient = any>(
155
+ client: TClient,
156
+ id: string,
157
+ retentionDays: number = DEFAULT_SOFT_DELETE_RETENTION_DAYS,
158
+ ): Promise<{ success: boolean; error?: string }> {
159
+ try {
160
+ // First, get the account to check its deletedAt timestamp
161
+ const { data: account, errors: getErrors } = await (
162
+ client as any
163
+ ).models.Account.get({ id });
164
+
165
+ if (getErrors || !account) {
166
+ console.error("[restoreAccount] Error fetching account:", getErrors);
167
+ return { success: false, error: "Account not found" };
168
+ }
169
+
170
+ // Check if restoration is allowed based on retention period
171
+ const eligibility = checkRestoreEligibility(account.deletedAt, retentionDays);
172
+
173
+ if (!eligibility.canRestore) {
174
+ const errorMsg = `Cannot restore account. Retention period of ${retentionDays} days has expired. Item was deleted ${eligibility.daysExpired} days ago.`;
175
+ console.error("[restoreAccount]", errorMsg);
176
+ return { success: false, error: errorMsg };
177
+ }
178
+
179
+ const { errors } = await (client as any).models.Account.update({
180
+ id,
181
+ status: "active",
182
+ deletedAt: null,
183
+ deletedBy: null,
184
+ });
185
+
186
+ if (errors) {
187
+ console.error("[restoreAccount] GraphQL errors:", errors);
188
+ return { success: false, error: "Failed to restore account" };
189
+ }
190
+
191
+ return { success: true };
192
+ } catch (error) {
193
+ console.error("[restoreAccount] Error restoring account:", error);
194
+ throw error;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Hard delete an account (permanently removes from database)
200
+ * Use with caution - prefer softDeleteAccount for recoverable deletion
100
201
  *
101
202
  * @example
102
203
  * ```typescript
@@ -5,6 +5,10 @@
5
5
  */
6
6
 
7
7
  import type { Brand } 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 brand
@@ -102,7 +106,104 @@ export async function updateBrand<TClient = any>(
102
106
  }
103
107
 
104
108
  /**
105
- * Delete a brand
109
+ * Soft delete a brand (sets status to "deleted" instead of removing)
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * import { softDeleteBrand } from '@htlkg/data/mutations';
114
+ * import { generateClient } from '@htlkg/data/client';
115
+ *
116
+ * const client = generateClient<Schema>();
117
+ * await softDeleteBrand(client, 'brand-123', 'admin@example.com');
118
+ * ```
119
+ */
120
+ export async function softDeleteBrand<TClient = any>(
121
+ client: TClient,
122
+ id: string,
123
+ deletedBy: string,
124
+ ): Promise<boolean> {
125
+ try {
126
+ const { errors } = await (client as any).models.Brand.update({
127
+ id,
128
+ status: "deleted",
129
+ deletedAt: new Date().toISOString(),
130
+ deletedBy,
131
+ });
132
+
133
+ if (errors) {
134
+ console.error("[softDeleteBrand] GraphQL errors:", errors);
135
+ return false;
136
+ }
137
+
138
+ return true;
139
+ } catch (error) {
140
+ console.error("[softDeleteBrand] Error soft-deleting brand:", error);
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Restore a soft-deleted brand (sets status back to "active")
147
+ * Checks retention period before allowing restoration
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * import { restoreBrand } from '@htlkg/data/mutations';
152
+ * import { generateClient } from '@htlkg/data/client';
153
+ *
154
+ * const client = generateClient<Schema>();
155
+ * await restoreBrand(client, 'brand-123');
156
+ * // Or with custom retention days:
157
+ * await restoreBrand(client, 'brand-123', 60);
158
+ * ```
159
+ */
160
+ export async function restoreBrand<TClient = any>(
161
+ client: TClient,
162
+ id: string,
163
+ retentionDays: number = DEFAULT_SOFT_DELETE_RETENTION_DAYS,
164
+ ): Promise<{ success: boolean; error?: string }> {
165
+ try {
166
+ // First, get the brand to check its deletedAt timestamp
167
+ const { data: brand, errors: getErrors } = await (
168
+ client as any
169
+ ).models.Brand.get({ id });
170
+
171
+ if (getErrors || !brand) {
172
+ console.error("[restoreBrand] Error fetching brand:", getErrors);
173
+ return { success: false, error: "Brand not found" };
174
+ }
175
+
176
+ // Check if restoration is allowed based on retention period
177
+ const eligibility = checkRestoreEligibility(brand.deletedAt, retentionDays);
178
+
179
+ if (!eligibility.canRestore) {
180
+ const errorMsg = `Cannot restore brand. Retention period of ${retentionDays} days has expired. Item was deleted ${eligibility.daysExpired} days ago.`;
181
+ console.error("[restoreBrand]", errorMsg);
182
+ return { success: false, error: errorMsg };
183
+ }
184
+
185
+ const { errors } = await (client as any).models.Brand.update({
186
+ id,
187
+ status: "active",
188
+ deletedAt: null,
189
+ deletedBy: null,
190
+ });
191
+
192
+ if (errors) {
193
+ console.error("[restoreBrand] GraphQL errors:", errors);
194
+ return { success: false, error: "Failed to restore brand" };
195
+ }
196
+
197
+ return { success: true };
198
+ } catch (error) {
199
+ console.error("[restoreBrand] Error restoring brand:", error);
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Hard delete a brand (permanently removes from database)
206
+ * Use with caution - prefer softDeleteBrand for recoverable deletion
106
207
  *
107
208
  * @example
108
209
  * ```typescript
@@ -9,6 +9,8 @@ export {
9
9
  createBrand,
10
10
  updateBrand,
11
11
  deleteBrand,
12
+ softDeleteBrand,
13
+ restoreBrand,
12
14
  type CreateBrandInput,
13
15
  type UpdateBrandInput,
14
16
  } from "./brands";
@@ -18,6 +20,8 @@ export {
18
20
  createAccount,
19
21
  updateAccount,
20
22
  deleteAccount,
23
+ softDeleteAccount,
24
+ restoreAccount,
21
25
  type CreateAccountInput,
22
26
  type UpdateAccountInput,
23
27
  } from "./accounts";
@@ -27,6 +31,8 @@ export {
27
31
  createUser,
28
32
  updateUser,
29
33
  deleteUser,
34
+ softDeleteUser,
35
+ restoreUser,
30
36
  type CreateUserInput,
31
37
  type UpdateUserInput,
32
38
  } from "./users";
@@ -40,3 +46,10 @@ export {
40
46
  type CreateProductInstanceInput,
41
47
  type UpdateProductInstanceInput,
42
48
  } from "./productInstances";
49
+
50
+ // SystemSettings mutations
51
+ export {
52
+ updateSystemSettings,
53
+ initializeSystemSettings,
54
+ type UpdateSystemSettingsInput,
55
+ } from "./systemSettings";
@@ -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
+ }
@@ -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
- * Delete a user
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