@go-avro/avro-js 0.0.37 → 0.0.39
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/auth/AuthManager.d.ts +2 -2
- package/dist/auth/AuthManager.js +37 -32
- package/dist/auth/storage.d.ts +1 -1
- package/dist/auth/storage.js +6 -6
- package/dist/client/AvroQueryClientProvider.js +1 -1
- package/dist/client/QueryClient.d.ts +35 -17
- package/dist/client/QueryClient.js +505 -391
- package/dist/client/core/fetch.js +16 -11
- package/dist/client/core/utils.js +5 -5
- package/dist/client/core/xhr.js +28 -23
- package/dist/client/hooks/analytics.js +14 -14
- package/dist/client/hooks/avro.js +2 -2
- package/dist/client/hooks/bills.js +66 -30
- package/dist/client/hooks/catalog_items.js +57 -22
- package/dist/client/hooks/chats.js +4 -4
- package/dist/client/hooks/companies.js +96 -39
- package/dist/client/hooks/email.js +1 -1
- package/dist/client/hooks/events.js +174 -63
- package/dist/client/hooks/groups.js +37 -22
- package/dist/client/hooks/jobs.js +69 -18
- package/dist/client/hooks/labels.js +36 -21
- package/dist/client/hooks/messages.js +9 -6
- package/dist/client/hooks/months.js +42 -22
- package/dist/client/hooks/plans.js +2 -2
- package/dist/client/hooks/prepayments.js +42 -22
- package/dist/client/hooks/proposal.js +21 -5
- package/dist/client/hooks/root.js +4 -4
- package/dist/client/hooks/routes.js +77 -32
- package/dist/client/hooks/sessions.js +66 -34
- package/dist/client/hooks/skills.js +33 -18
- package/dist/client/hooks/teams.js +36 -21
- package/dist/client/hooks/timecards.js +6 -0
- package/dist/client/hooks/users.js +61 -29
- package/dist/client/hooks/waivers.js +41 -19
- package/dist/index.d.ts +38 -38
- package/dist/index.js +37 -37
- package/dist/types/api/Bill.d.ts +1 -1
- package/dist/types/api/Bill.js +1 -1
- package/dist/types/api/Job.d.ts +1 -1
- package/dist/types/api/Job.js +14 -14
- package/dist/types/api/LineItem.d.ts +3 -3
- package/dist/types/api/LineItem.js +5 -2
- package/dist/types/api/PaymentType.d.ts +1 -1
- package/dist/types/api/Prepayment.d.ts +1 -1
- package/dist/types/api/Route.d.ts +3 -3
- package/dist/types/api/Route.js +4 -2
- package/dist/types/api/RouteJob.d.ts +1 -1
- package/dist/types/api/Task.d.ts +2 -2
- package/dist/types/api/Task.js +12 -7
- package/dist/types/api/Timecard.d.ts +1 -1
- package/dist/types/api/TimecardAction.d.ts +1 -1
- package/dist/types/api/UserCompanyAssociation.d.ts +2 -2
- package/dist/types/api/UserCompanyAssociation.js +1 -1
- package/dist/types/api/_Event.d.ts +1 -1
- package/dist/types/api/_Event.js +1 -1
- package/dist/types/api.d.ts +1 -1
- package/dist/types/auth.d.ts +1 -1
- package/dist/types/client.d.ts +1 -1
- package/package.json +3 -2
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import io from
|
|
2
|
-
import { useMutation, useQueryClient } from
|
|
1
|
+
import io from "socket.io-client";
|
|
2
|
+
import { useMutation, useQueryClient, } from "@tanstack/react-query";
|
|
3
3
|
import { v4 as uuidv4 } from "uuid";
|
|
4
|
-
import { Job, LoginResponse } from
|
|
5
|
-
import { AuthState } from
|
|
6
|
-
import { StandardError } from
|
|
4
|
+
import { Job, LoginResponse, } from "../types/api";
|
|
5
|
+
import { AuthState } from "../types/auth";
|
|
6
|
+
import { StandardError } from "../types/error";
|
|
7
7
|
function isBulkEvent(c) {
|
|
8
|
-
return
|
|
8
|
+
return "invalidateKeys" in c;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* Maps socket event names to cache-update strategies.
|
|
@@ -18,75 +18,185 @@ function isBulkEvent(c) {
|
|
|
18
18
|
*/
|
|
19
19
|
const SOCKET_EVENT_CONFIG = {
|
|
20
20
|
// ── Company ──
|
|
21
|
-
create_company: {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
create_company: {
|
|
22
|
+
entityKey: "companies",
|
|
23
|
+
action: "create",
|
|
24
|
+
fetchPath: (id) => `/company/${id}`,
|
|
25
|
+
},
|
|
26
|
+
update_company: {
|
|
27
|
+
entityKey: "companies",
|
|
28
|
+
action: "update",
|
|
29
|
+
fetchPath: (id) => `/company/${id}`,
|
|
30
|
+
},
|
|
31
|
+
delete_company: { entityKey: "companies", action: "delete", fetchPath: null },
|
|
24
32
|
// ── Users (no single-entity socket events) ──
|
|
25
|
-
user_updated: { invalidateKeys: [[
|
|
26
|
-
update_users: { invalidateKeys: [[
|
|
33
|
+
user_updated: { invalidateKeys: [["users"], ["user"]] },
|
|
34
|
+
update_users: { invalidateKeys: [["users"], ["user"]] },
|
|
27
35
|
// ── Jobs ──
|
|
28
|
-
create_job: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
create_job: {
|
|
37
|
+
entityKey: "jobs",
|
|
38
|
+
action: "create",
|
|
39
|
+
fetchPath: (id) => `/job/${id}`,
|
|
40
|
+
construct: (d) => new Job(d),
|
|
41
|
+
},
|
|
42
|
+
update_job: {
|
|
43
|
+
entityKey: "jobs",
|
|
44
|
+
action: "update",
|
|
45
|
+
fetchPath: (id) => `/job/${id}`,
|
|
46
|
+
construct: (d) => new Job(d),
|
|
47
|
+
},
|
|
48
|
+
delete_job: { entityKey: "jobs", action: "delete", fetchPath: null },
|
|
49
|
+
update_jobs: { invalidateKeys: [["jobs"], ["infinite", "jobs"]] },
|
|
50
|
+
delete_jobs: { invalidateKeys: [["jobs"], ["infinite", "jobs"]] },
|
|
33
51
|
// ── Routes ──
|
|
34
|
-
create_route: {
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
create_route: {
|
|
53
|
+
entityKey: "routes",
|
|
54
|
+
action: "create",
|
|
55
|
+
fetchPath: (id) => `/route/${id}`,
|
|
56
|
+
},
|
|
57
|
+
update_route: {
|
|
58
|
+
entityKey: "routes",
|
|
59
|
+
action: "update",
|
|
60
|
+
fetchPath: (id) => `/route/${id}`,
|
|
61
|
+
},
|
|
62
|
+
delete_route: { entityKey: "routes", action: "delete", fetchPath: null },
|
|
37
63
|
// ── Events (also refetch parent job — overdueness, last_event, etc.) ──
|
|
38
|
-
create_event: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
create_event: {
|
|
65
|
+
entityKey: "events",
|
|
66
|
+
action: "create",
|
|
67
|
+
fetchPath: (id) => `/event/${id}`,
|
|
68
|
+
relatedRefetch: [
|
|
69
|
+
{
|
|
70
|
+
entityKey: "jobs",
|
|
71
|
+
idField: "job_id",
|
|
72
|
+
fetchPath: (id) => `/job/${id}`,
|
|
73
|
+
construct: (d) => new Job(d),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
update_event: {
|
|
78
|
+
entityKey: "events",
|
|
79
|
+
action: "update",
|
|
80
|
+
fetchPath: (id) => `/event/${id}`,
|
|
81
|
+
relatedRefetch: [
|
|
82
|
+
{
|
|
83
|
+
entityKey: "jobs",
|
|
84
|
+
idField: "job_id",
|
|
85
|
+
fetchPath: (id) => `/job/${id}`,
|
|
86
|
+
construct: (d) => new Job(d),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
delete_event: {
|
|
91
|
+
entityKey: "events",
|
|
92
|
+
action: "delete",
|
|
93
|
+
fetchPath: null,
|
|
94
|
+
relatedRefetch: [
|
|
95
|
+
{
|
|
96
|
+
entityKey: "jobs",
|
|
97
|
+
idField: "job_id",
|
|
98
|
+
fetchPath: (id) => `/job/${id}`,
|
|
99
|
+
construct: (d) => new Job(d),
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
update_events: { invalidateKeys: [["events"]] },
|
|
42
104
|
// ── Teams ──
|
|
43
|
-
create_team: { entityKey:
|
|
44
|
-
update_team: { entityKey:
|
|
45
|
-
delete_team: { entityKey:
|
|
46
|
-
update_teams: { invalidateKeys: [[
|
|
105
|
+
create_team: { entityKey: "teams", action: "create", fetchPath: null },
|
|
106
|
+
update_team: { entityKey: "teams", action: "update", fetchPath: null },
|
|
107
|
+
delete_team: { entityKey: "teams", action: "delete", fetchPath: null },
|
|
108
|
+
update_teams: { invalidateKeys: [["teams"]] },
|
|
47
109
|
// ── Bills ──
|
|
48
|
-
create_bill: {
|
|
49
|
-
|
|
50
|
-
|
|
110
|
+
create_bill: {
|
|
111
|
+
entityKey: "bills",
|
|
112
|
+
action: "create",
|
|
113
|
+
fetchPath: (id) => `/bill/${id}`,
|
|
114
|
+
},
|
|
115
|
+
delete_bill: { entityKey: "bills", action: "delete", fetchPath: null },
|
|
116
|
+
update_bills: { invalidateKeys: [["bills"]] },
|
|
51
117
|
// ── Sessions ──
|
|
52
|
-
create_session: { entityKey:
|
|
53
|
-
update_session: { entityKey:
|
|
118
|
+
create_session: { entityKey: "sessions", action: "create", fetchPath: null },
|
|
119
|
+
update_session: { entityKey: "sessions", action: "update", fetchPath: null },
|
|
54
120
|
// ── Catalog Items ──
|
|
55
|
-
create_catalog_item: {
|
|
56
|
-
|
|
57
|
-
|
|
121
|
+
create_catalog_item: {
|
|
122
|
+
entityKey: "catalog_items",
|
|
123
|
+
action: "create",
|
|
124
|
+
fetchPath: (id) => `/catalog_item/${id}`,
|
|
125
|
+
},
|
|
126
|
+
update_catalog_item: {
|
|
127
|
+
entityKey: "catalog_items",
|
|
128
|
+
action: "update",
|
|
129
|
+
fetchPath: (id) => `/catalog_item/${id}`,
|
|
130
|
+
},
|
|
131
|
+
delete_catalog_item: {
|
|
132
|
+
entityKey: "catalog_items",
|
|
133
|
+
action: "delete",
|
|
134
|
+
fetchPath: null,
|
|
135
|
+
},
|
|
58
136
|
// ── Groups ──
|
|
59
|
-
create_group: { entityKey:
|
|
60
|
-
update_group: { entityKey:
|
|
61
|
-
delete_group: { entityKey:
|
|
137
|
+
create_group: { entityKey: "groups", action: "create", fetchPath: null },
|
|
138
|
+
update_group: { entityKey: "groups", action: "update", fetchPath: null },
|
|
139
|
+
delete_group: { entityKey: "groups", action: "delete", fetchPath: null },
|
|
62
140
|
// ── Labels ──
|
|
63
|
-
create_label: { entityKey:
|
|
64
|
-
update_label: { entityKey:
|
|
65
|
-
delete_label: { entityKey:
|
|
141
|
+
create_label: { entityKey: "labels", action: "create", fetchPath: null },
|
|
142
|
+
update_label: { entityKey: "labels", action: "update", fetchPath: null },
|
|
143
|
+
delete_label: { entityKey: "labels", action: "delete", fetchPath: null },
|
|
66
144
|
// ── Skills ──
|
|
67
|
-
create_skill: { entityKey:
|
|
68
|
-
update_skill: { entityKey:
|
|
69
|
-
delete_skill: { entityKey:
|
|
145
|
+
create_skill: { entityKey: "skills", action: "create", fetchPath: null },
|
|
146
|
+
update_skill: { entityKey: "skills", action: "update", fetchPath: null },
|
|
147
|
+
delete_skill: { entityKey: "skills", action: "delete", fetchPath: null },
|
|
70
148
|
// ── Proposals ──
|
|
71
|
-
create_proposal: {
|
|
72
|
-
|
|
73
|
-
|
|
149
|
+
create_proposal: {
|
|
150
|
+
entityKey: "proposals",
|
|
151
|
+
action: "create",
|
|
152
|
+
fetchPath: (id) => `/proposal/${id}`,
|
|
153
|
+
},
|
|
154
|
+
update_proposal: {
|
|
155
|
+
entityKey: "proposals",
|
|
156
|
+
action: "update",
|
|
157
|
+
fetchPath: (id) => `/proposal/${id}`,
|
|
158
|
+
},
|
|
159
|
+
delete_proposal: {
|
|
160
|
+
entityKey: "proposals",
|
|
161
|
+
action: "delete",
|
|
162
|
+
fetchPath: null,
|
|
163
|
+
},
|
|
74
164
|
// ── Service Months ──
|
|
75
|
-
create_month: { entityKey:
|
|
76
|
-
update_months: { invalidateKeys: [[
|
|
77
|
-
delete_months: { invalidateKeys: [[
|
|
165
|
+
create_month: { entityKey: "months", action: "create", fetchPath: null },
|
|
166
|
+
update_months: { invalidateKeys: [["months"]] },
|
|
167
|
+
delete_months: { invalidateKeys: [["months"]] },
|
|
78
168
|
// ── Tasks (nested under jobs — target the parent job) ──
|
|
79
|
-
create_task: {
|
|
80
|
-
|
|
81
|
-
|
|
169
|
+
create_task: {
|
|
170
|
+
entityKey: "jobs",
|
|
171
|
+
action: "update",
|
|
172
|
+
fetchPath: (id) => `/job/${id}`,
|
|
173
|
+
idField: "job_id",
|
|
174
|
+
construct: (d) => new Job(d),
|
|
175
|
+
},
|
|
176
|
+
update_task: {
|
|
177
|
+
entityKey: "jobs",
|
|
178
|
+
action: "update",
|
|
179
|
+
fetchPath: (id) => `/job/${id}`,
|
|
180
|
+
idField: "job_id",
|
|
181
|
+
construct: (d) => new Job(d),
|
|
182
|
+
},
|
|
183
|
+
delete_task: {
|
|
184
|
+
entityKey: "jobs",
|
|
185
|
+
action: "update",
|
|
186
|
+
fetchPath: (id) => `/job/${id}`,
|
|
187
|
+
idField: "job_id",
|
|
188
|
+
construct: (d) => new Job(d),
|
|
189
|
+
},
|
|
82
190
|
// ── Scheduling ──
|
|
83
|
-
schedule_complete: {
|
|
191
|
+
schedule_complete: {
|
|
192
|
+
invalidateKeys: [["routes"], ["jobs"], ["infinite", "jobs"]],
|
|
193
|
+
},
|
|
84
194
|
// ── Location ──
|
|
85
|
-
location_update: { invalidateKeys: [[
|
|
195
|
+
location_update: { invalidateKeys: [["teams"]] },
|
|
86
196
|
// ── Prepayments ──
|
|
87
|
-
update_prepayments: { invalidateKeys: [[
|
|
197
|
+
update_prepayments: { invalidateKeys: [["prepayments"]] },
|
|
88
198
|
// ── Chats ──
|
|
89
|
-
new_message: { invalidateKeys: [[
|
|
199
|
+
new_message: { invalidateKeys: [["chats"], ["messages"]] },
|
|
90
200
|
};
|
|
91
201
|
/**
|
|
92
202
|
* Returns true when `query.queryKey` belongs to the given entity,
|
|
@@ -94,7 +204,7 @@ const SOCKET_EVENT_CONFIG = {
|
|
|
94
204
|
*/
|
|
95
205
|
function matchesEntityKey(query, entityKey) {
|
|
96
206
|
const k = query.queryKey;
|
|
97
|
-
return k[0] === entityKey || (k[0] ===
|
|
207
|
+
return k[0] === entityKey || (k[0] === "infinite" && k[1] === entityKey);
|
|
98
208
|
}
|
|
99
209
|
export class AvroQueryClient {
|
|
100
210
|
constructor(config) {
|
|
@@ -110,39 +220,54 @@ export class AvroQueryClient {
|
|
|
110
220
|
baseUrl: config.baseUrl,
|
|
111
221
|
authManager: config.authManager,
|
|
112
222
|
maxRetries: config.maxRetries ?? 3,
|
|
113
|
-
retryStrategy: config.retryStrategy ??
|
|
223
|
+
retryStrategy: config.retryStrategy ?? "fixed",
|
|
114
224
|
timeout: config.timeout ?? 0,
|
|
115
225
|
};
|
|
116
|
-
this.socket = io(config.baseUrl, {
|
|
117
|
-
|
|
226
|
+
this.socket = io(config.baseUrl, {
|
|
227
|
+
autoConnect: false,
|
|
228
|
+
transports: ["websocket"],
|
|
229
|
+
});
|
|
230
|
+
config.authManager.isAuthenticated().then((isAuth) => {
|
|
118
231
|
this.setAuthState(isAuth);
|
|
119
|
-
this.getCompanyId().then(id => {
|
|
120
|
-
this.companyId = id;
|
|
121
|
-
});
|
|
122
232
|
if (!this.socket.connected && isAuth === AuthState.AUTHENTICATED) {
|
|
123
|
-
|
|
124
|
-
|
|
233
|
+
// Resolve both companyId and access token before connecting so
|
|
234
|
+
// the 'connect' handler in setupSocketInvalidation can immediately
|
|
235
|
+
// join the company room.
|
|
236
|
+
Promise.all([
|
|
237
|
+
this.getCompanyId(),
|
|
238
|
+
this.config.authManager.accessToken(),
|
|
239
|
+
])
|
|
240
|
+
.then(([id, token]) => {
|
|
241
|
+
this.companyId = id;
|
|
242
|
+
console.log("Initializing socket connection with token:", token);
|
|
125
243
|
this.socket.auth = { token: token };
|
|
126
244
|
this.socket.connect();
|
|
127
|
-
})
|
|
128
|
-
|
|
245
|
+
})
|
|
246
|
+
.catch((err) => {
|
|
247
|
+
console.error("Not logged in:", err);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// Not authenticated – still cache the companyId for later.
|
|
252
|
+
this.getCompanyId().then((id) => {
|
|
253
|
+
this.companyId = id;
|
|
129
254
|
});
|
|
130
255
|
}
|
|
131
256
|
});
|
|
132
|
-
this.socket.on(
|
|
257
|
+
this.socket.on("connect", () => {
|
|
133
258
|
this.setAuthState(AuthState.AUTHENTICATED);
|
|
134
259
|
console.log(`Socket connected with ID: ${this.socket?.id}`);
|
|
135
260
|
});
|
|
136
|
-
this.socket.on(
|
|
261
|
+
this.socket.on("disconnect", (reason) => {
|
|
137
262
|
console.log(`Socket disconnected: ${reason}`);
|
|
138
263
|
});
|
|
139
|
-
this.socket.on(
|
|
264
|
+
this.socket.on("connect_error", (err) => {
|
|
140
265
|
console.error(`Socket connection error: ${err.message}`);
|
|
141
266
|
});
|
|
142
267
|
this.config.authManager.onTokenRefreshed((newAccessToken) => {
|
|
143
268
|
if (this.socket && newAccessToken) {
|
|
144
269
|
this.setAuthState(AuthState.AUTHENTICATED);
|
|
145
|
-
console.log(
|
|
270
|
+
console.log("Access token refreshed, updating socket auth...");
|
|
146
271
|
this.socket.auth = { token: newAccessToken };
|
|
147
272
|
this.socket.disconnect().connect();
|
|
148
273
|
}
|
|
@@ -157,7 +282,7 @@ export class AvroQueryClient {
|
|
|
157
282
|
}
|
|
158
283
|
emit(eventName, data) {
|
|
159
284
|
if (!this.socket?.connected) {
|
|
160
|
-
console.error(
|
|
285
|
+
console.error("Socket is not connected. Cannot emit event.");
|
|
161
286
|
return;
|
|
162
287
|
}
|
|
163
288
|
this.socket.emit(eventName, data);
|
|
@@ -191,7 +316,9 @@ export class AvroQueryClient {
|
|
|
191
316
|
const client = this; // stable reference for async closures
|
|
192
317
|
/** Full-invalidate every key that matches the entity (including 'infinite' prefix). */
|
|
193
318
|
const invalidateEntity = (entityKey) => {
|
|
194
|
-
queryClient.invalidateQueries({
|
|
319
|
+
queryClient.invalidateQueries({
|
|
320
|
+
predicate: (q) => matchesEntityKey(q, entityKey),
|
|
321
|
+
});
|
|
195
322
|
};
|
|
196
323
|
for (const [event, config] of Object.entries(SOCKET_EVENT_CONFIG)) {
|
|
197
324
|
if (isBulkEvent(config)) {
|
|
@@ -206,160 +333,39 @@ export class AvroQueryClient {
|
|
|
206
333
|
}
|
|
207
334
|
else {
|
|
208
335
|
// ── Targeted: surgical cache update ──────────────────
|
|
209
|
-
const { entityKey, action, fetchPath, idField, alsoInvalidate, relatedRefetch } = config;
|
|
336
|
+
const { entityKey, action, fetchPath, idField, alsoInvalidate, relatedRefetch, } = config;
|
|
210
337
|
const handler = async (data) => {
|
|
211
|
-
const id = data?.[idField ??
|
|
338
|
+
const id = data?.[idField ?? "id"];
|
|
212
339
|
// No id → old backend or malformed payload → full invalidation
|
|
213
|
-
if (!id || typeof id !==
|
|
340
|
+
if (!id || typeof id !== "string") {
|
|
214
341
|
invalidateEntity(entityKey);
|
|
215
342
|
alsoInvalidate?.forEach((k) => queryClient.invalidateQueries({ queryKey: k }));
|
|
216
343
|
return;
|
|
217
344
|
}
|
|
218
|
-
const entityPredicate = (q) => matchesEntityKey(q, entityKey);
|
|
219
345
|
let fetchedItem;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
staleTime: 0,
|
|
229
|
-
});
|
|
230
|
-
fetchedItem = config.construct ? config.construct(item) : item;
|
|
231
|
-
queryClient.setQueryData([entityKey, id], fetchedItem);
|
|
232
|
-
queryClient.setQueriesData({ predicate: entityPredicate, type: 'active' }, (old) => {
|
|
233
|
-
if (!old)
|
|
234
|
-
return old;
|
|
235
|
-
if (old.pages && Array.isArray(old.pages)) {
|
|
236
|
-
// Skip if already present (avoids duplicates
|
|
237
|
-
// when the creating user also gets the event)
|
|
238
|
-
if (old.pages.some((p) => p.some((x) => x?.id === id)))
|
|
239
|
-
return old;
|
|
240
|
-
return {
|
|
241
|
-
...old,
|
|
242
|
-
pages: [
|
|
243
|
-
[fetchedItem, ...(old.pages[0] || [])],
|
|
244
|
-
...old.pages.slice(1),
|
|
245
|
-
],
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
if (Array.isArray(old)) {
|
|
249
|
-
if (old.some((x) => x?.id === id))
|
|
250
|
-
return old;
|
|
251
|
-
return [...old, fetchedItem];
|
|
252
|
-
}
|
|
253
|
-
return old;
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
invalidateEntity(entityKey);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
// No individual GET endpoint → invalidate lists
|
|
262
|
-
invalidateEntity(entityKey);
|
|
263
|
-
}
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
// ─── UPDATE ─────────────────────────────────
|
|
267
|
-
case 'update': {
|
|
268
|
-
if (fetchPath) {
|
|
269
|
-
try {
|
|
270
|
-
// Fetch fresh copy (also updates the [entity, id] cache)
|
|
271
|
-
const item = await queryClient.fetchQuery({
|
|
272
|
-
queryKey: [entityKey, id],
|
|
273
|
-
queryFn: () => client.get({ path: fetchPath(id) }),
|
|
274
|
-
staleTime: 0,
|
|
275
|
-
});
|
|
276
|
-
fetchedItem = config.construct ? config.construct(item) : item;
|
|
277
|
-
queryClient.setQueryData([entityKey, id], fetchedItem);
|
|
278
|
-
// Patch it into every active list / infinite-query cache
|
|
279
|
-
queryClient.setQueriesData({ predicate: entityPredicate, type: 'active' }, (old) => {
|
|
280
|
-
if (!old)
|
|
281
|
-
return old;
|
|
282
|
-
if (old.pages && Array.isArray(old.pages)) {
|
|
283
|
-
return {
|
|
284
|
-
...old,
|
|
285
|
-
pages: old.pages.map((page) => page.map((x) => (x?.id === id ? fetchedItem : x))),
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
if (Array.isArray(old)) {
|
|
289
|
-
return old.map((x) => (x?.id === id ? fetchedItem : x));
|
|
290
|
-
}
|
|
291
|
-
return old;
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
invalidateEntity(entityKey);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
// No individual GET → invalidate everything for this entity
|
|
300
|
-
invalidateEntity(entityKey);
|
|
301
|
-
}
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
// ─── DELETE ─────────────────────────────────
|
|
305
|
-
case 'delete': {
|
|
306
|
-
// Remove the individual-item cache entry
|
|
307
|
-
queryClient.removeQueries({ queryKey: [entityKey, id], exact: true });
|
|
308
|
-
// Remove from every active list / infinite-query cache
|
|
309
|
-
queryClient.setQueriesData({ predicate: entityPredicate, type: 'active' }, (old) => {
|
|
310
|
-
if (!old)
|
|
311
|
-
return old;
|
|
312
|
-
if (old.pages && Array.isArray(old.pages)) {
|
|
313
|
-
return {
|
|
314
|
-
...old,
|
|
315
|
-
pages: old.pages.map((page) => page.filter((x) => x?.id !== id)),
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
if (Array.isArray(old)) {
|
|
319
|
-
return old.filter((x) => x?.id !== id);
|
|
320
|
-
}
|
|
321
|
-
return old;
|
|
322
|
-
});
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
346
|
+
// ─── CREATE / UPDATE / DELETE via shared _syncEntity ───
|
|
347
|
+
fetchedItem = await client._syncEntity(queryClient, {
|
|
348
|
+
action,
|
|
349
|
+
entityKey,
|
|
350
|
+
id,
|
|
351
|
+
fetchPath: fetchPath ? fetchPath(id) : undefined,
|
|
352
|
+
construct: config.construct,
|
|
353
|
+
});
|
|
326
354
|
// Invalidate any additional keys (e.g. companies → /company/list)
|
|
327
355
|
alsoInvalidate?.forEach((k) => queryClient.invalidateQueries({ queryKey: k }));
|
|
328
356
|
// Refetch & patch related entities (e.g. event → parent job)
|
|
329
357
|
if (relatedRefetch) {
|
|
330
358
|
for (const related of relatedRefetch) {
|
|
331
359
|
const relatedId = data?.[related.idField] ?? fetchedItem?.[related.idField];
|
|
332
|
-
if (!relatedId || typeof relatedId !==
|
|
360
|
+
if (!relatedId || typeof relatedId !== "string")
|
|
333
361
|
continue;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
relatedItem = related.construct(relatedItem);
|
|
342
|
-
queryClient.setQueryData([related.entityKey, relatedId], relatedItem);
|
|
343
|
-
const relatedPredicate = (q) => matchesEntityKey(q, related.entityKey);
|
|
344
|
-
queryClient.setQueriesData({ predicate: relatedPredicate, type: 'active' }, (old) => {
|
|
345
|
-
if (!old)
|
|
346
|
-
return old;
|
|
347
|
-
if (old.pages && Array.isArray(old.pages)) {
|
|
348
|
-
return {
|
|
349
|
-
...old,
|
|
350
|
-
pages: old.pages.map((page) => page.map((x) => (x?.id === relatedId ? relatedItem : x))),
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
if (Array.isArray(old)) {
|
|
354
|
-
return old.map((x) => (x?.id === relatedId ? relatedItem : x));
|
|
355
|
-
}
|
|
356
|
-
return old;
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
// Fetch failed → invalidate related entity lists
|
|
361
|
-
invalidateEntity(related.entityKey);
|
|
362
|
-
}
|
|
362
|
+
await client._syncEntity(queryClient, {
|
|
363
|
+
action: "update",
|
|
364
|
+
entityKey: related.entityKey,
|
|
365
|
+
id: relatedId,
|
|
366
|
+
fetchPath: related.fetchPath(relatedId),
|
|
367
|
+
construct: related.construct,
|
|
368
|
+
});
|
|
363
369
|
}
|
|
364
370
|
}
|
|
365
371
|
};
|
|
@@ -370,11 +376,11 @@ export class AvroQueryClient {
|
|
|
370
376
|
// Auto join/leave company room
|
|
371
377
|
const joinCompanyRoom = () => {
|
|
372
378
|
if (this.companyId) {
|
|
373
|
-
this.socket.emit(
|
|
379
|
+
this.socket.emit("join_company", { company_id: this.companyId });
|
|
374
380
|
}
|
|
375
381
|
};
|
|
376
|
-
this.socket.on(
|
|
377
|
-
handlers.push({ event:
|
|
382
|
+
this.socket.on("connect", joinCompanyRoom);
|
|
383
|
+
handlers.push({ event: "connect", handler: joinCompanyRoom });
|
|
378
384
|
// If already connected, join immediately
|
|
379
385
|
if (this.socket.connected && this.companyId) {
|
|
380
386
|
joinCompanyRoom();
|
|
@@ -385,7 +391,7 @@ export class AvroQueryClient {
|
|
|
385
391
|
}
|
|
386
392
|
// Leave company room on teardown
|
|
387
393
|
if (this.socket.connected && this.companyId) {
|
|
388
|
-
this.socket.emit(
|
|
394
|
+
this.socket.emit("leave_company", { company_id: this.companyId });
|
|
389
395
|
}
|
|
390
396
|
this._queryClient = null;
|
|
391
397
|
};
|
|
@@ -397,17 +403,17 @@ export class AvroQueryClient {
|
|
|
397
403
|
this._socketInvalidationCleanup?.();
|
|
398
404
|
this._socketInvalidationCleanup = null;
|
|
399
405
|
}
|
|
400
|
-
get({ path, cancelToken, headers, progressUpdateCallback }) {
|
|
401
|
-
return this._xhr(
|
|
406
|
+
get({ path, cancelToken, headers, progressUpdateCallback, }) {
|
|
407
|
+
return this._xhr("GET", path, null, cancelToken, headers, true, this.config.maxRetries, progressUpdateCallback);
|
|
402
408
|
}
|
|
403
|
-
post({ path, data, cancelToken, headers, progressUpdateCallback }) {
|
|
404
|
-
return this._xhr(
|
|
409
|
+
post({ path, data, cancelToken, headers, progressUpdateCallback, }) {
|
|
410
|
+
return this._xhr("POST", path, data, cancelToken, headers, false, this.config.maxRetries, progressUpdateCallback);
|
|
405
411
|
}
|
|
406
|
-
put({ path, data, cancelToken, headers, progressUpdateCallback }) {
|
|
407
|
-
return this._xhr(
|
|
412
|
+
put({ path, data, cancelToken, headers, progressUpdateCallback, }) {
|
|
413
|
+
return this._xhr("PUT", path, data, cancelToken, headers, true, this.config.maxRetries, progressUpdateCallback);
|
|
408
414
|
}
|
|
409
|
-
delete({ path, cancelToken, headers, progressUpdateCallback }) {
|
|
410
|
-
return this._xhr(
|
|
415
|
+
delete({ path, cancelToken, headers, progressUpdateCallback, }) {
|
|
416
|
+
return this._xhr("DELETE", path, null, cancelToken, headers, false, this.config.maxRetries, progressUpdateCallback);
|
|
411
417
|
}
|
|
412
418
|
loginSuccess(tokens) {
|
|
413
419
|
this.setAuthState(AuthState.AUTHENTICATED);
|
|
@@ -420,20 +426,23 @@ export class AvroQueryClient {
|
|
|
420
426
|
useLogin() {
|
|
421
427
|
const queryClient = this.getQueryClient();
|
|
422
428
|
return useMutation({
|
|
423
|
-
mutationFn: async ({ username, password, code, cancelToken }) => {
|
|
429
|
+
mutationFn: async ({ username, password, code, cancelToken, }) => {
|
|
424
430
|
const resp = await this.post({
|
|
425
|
-
path:
|
|
431
|
+
path: "/login",
|
|
426
432
|
data: JSON.stringify({ username, password, code }),
|
|
427
433
|
cancelToken,
|
|
428
|
-
headers: {
|
|
434
|
+
headers: { "Content-Type": "application/json" },
|
|
429
435
|
});
|
|
430
|
-
if (!resp || !(
|
|
436
|
+
if (!resp || !("access_token" in resp)) {
|
|
431
437
|
if (resp.msg === "TOTP required") {
|
|
432
438
|
return LoginResponse.NEEDS_TOTP;
|
|
433
439
|
}
|
|
434
|
-
throw new StandardError(401,
|
|
440
|
+
throw new StandardError(401, "Invalid login response");
|
|
435
441
|
}
|
|
436
|
-
await this.loginSuccess({
|
|
442
|
+
await this.loginSuccess({
|
|
443
|
+
access_token: resp.access_token,
|
|
444
|
+
refresh_token: resp.refresh_token,
|
|
445
|
+
});
|
|
437
446
|
return LoginResponse.SUCCESS;
|
|
438
447
|
},
|
|
439
448
|
onSettled: () => {
|
|
@@ -441,19 +450,19 @@ export class AvroQueryClient {
|
|
|
441
450
|
},
|
|
442
451
|
onError: (err) => {
|
|
443
452
|
this.config.authManager.clearCache();
|
|
444
|
-
throw new StandardError(401, err.message ||
|
|
445
|
-
}
|
|
453
|
+
throw new StandardError(401, err.message || "Login failed");
|
|
454
|
+
},
|
|
446
455
|
});
|
|
447
456
|
}
|
|
448
457
|
useRequestCode() {
|
|
449
458
|
const queryClient = this.getQueryClient();
|
|
450
459
|
return useMutation({
|
|
451
|
-
mutationFn: async ({ username, email, cancelToken }) => {
|
|
460
|
+
mutationFn: async ({ username, email, cancelToken, }) => {
|
|
452
461
|
const resp = await this.post({
|
|
453
|
-
path:
|
|
462
|
+
path: "/code",
|
|
454
463
|
data: JSON.stringify({ username, email }),
|
|
455
464
|
cancelToken,
|
|
456
|
-
headers: {
|
|
465
|
+
headers: { "Content-Type": "application/json" },
|
|
457
466
|
});
|
|
458
467
|
return resp;
|
|
459
468
|
},
|
|
@@ -461,46 +470,49 @@ export class AvroQueryClient {
|
|
|
461
470
|
queryClient.invalidateQueries();
|
|
462
471
|
},
|
|
463
472
|
onError: (err) => {
|
|
464
|
-
throw new StandardError(err.status, err.message ||
|
|
465
|
-
}
|
|
473
|
+
throw new StandardError(err.status, err.message || "Request code failed");
|
|
474
|
+
},
|
|
466
475
|
});
|
|
467
476
|
}
|
|
468
477
|
useUpdatePassword() {
|
|
469
478
|
const queryClient = this.getQueryClient();
|
|
470
479
|
return useMutation({
|
|
471
|
-
mutationFn: async ({ username, email, code, newPassword, cancelToken }) => {
|
|
480
|
+
mutationFn: async ({ username, email, code, newPassword, cancelToken, }) => {
|
|
472
481
|
await this.post({
|
|
473
482
|
path: `/user/${username ?? email}/password`,
|
|
474
483
|
data: JSON.stringify({ code, password: newPassword }),
|
|
475
484
|
cancelToken,
|
|
476
|
-
headers: {
|
|
485
|
+
headers: { "Content-Type": "application/json" },
|
|
477
486
|
});
|
|
478
487
|
},
|
|
479
488
|
onSettled: () => {
|
|
480
489
|
queryClient.invalidateQueries();
|
|
481
490
|
},
|
|
482
491
|
onError: (err) => {
|
|
483
|
-
throw new StandardError(err.status, err.message ||
|
|
484
|
-
}
|
|
492
|
+
throw new StandardError(err.status, err.message || "Update password failed");
|
|
493
|
+
},
|
|
485
494
|
});
|
|
486
495
|
}
|
|
487
496
|
useGoogleLogin() {
|
|
488
497
|
const queryClient = this.getQueryClient();
|
|
489
498
|
return useMutation({
|
|
490
|
-
mutationFn: async ({ token, cancelToken }) => {
|
|
491
|
-
const resp = await this._xhr(
|
|
492
|
-
if (!resp || !(
|
|
499
|
+
mutationFn: async ({ token, cancelToken, }) => {
|
|
500
|
+
const resp = await this._xhr("POST", `/google/authorize?token=${token}`, {}, cancelToken, { "Content-Type": "application/json" });
|
|
501
|
+
if (!resp || !("access_token" in resp)) {
|
|
493
502
|
if (resp.msg === "TOTP required") {
|
|
494
503
|
return LoginResponse.NEEDS_TOTP;
|
|
495
504
|
}
|
|
496
|
-
throw new StandardError(401,
|
|
505
|
+
throw new StandardError(401, "Invalid Google login response");
|
|
497
506
|
}
|
|
498
507
|
this.setAuthState(AuthState.AUTHENTICATED);
|
|
499
508
|
this.socket.auth = { token: resp.access_token };
|
|
500
509
|
if (!this.socket.connected) {
|
|
501
510
|
this.socket.connect();
|
|
502
511
|
}
|
|
503
|
-
await this.config.authManager.setTokens({
|
|
512
|
+
await this.config.authManager.setTokens({
|
|
513
|
+
access_token: resp.access_token,
|
|
514
|
+
refresh_token: resp.refresh_token,
|
|
515
|
+
});
|
|
504
516
|
return LoginResponse.SUCCESS;
|
|
505
517
|
},
|
|
506
518
|
onSettled: () => {
|
|
@@ -508,27 +520,30 @@ export class AvroQueryClient {
|
|
|
508
520
|
},
|
|
509
521
|
onError: (err) => {
|
|
510
522
|
this.config.authManager.clearCache();
|
|
511
|
-
throw new StandardError(err.status, err.message ||
|
|
512
|
-
}
|
|
523
|
+
throw new StandardError(err.status, err.message || "Google Login failed");
|
|
524
|
+
},
|
|
513
525
|
});
|
|
514
526
|
}
|
|
515
527
|
useAppleLogin() {
|
|
516
528
|
const queryClient = this.getQueryClient();
|
|
517
529
|
return useMutation({
|
|
518
|
-
mutationFn: async ({ token, cancelToken }) => {
|
|
519
|
-
const resp = await this._xhr(
|
|
520
|
-
if (!resp || !(
|
|
530
|
+
mutationFn: async ({ token, cancelToken, }) => {
|
|
531
|
+
const resp = await this._xhr("POST", `/apple/authorize?token=${encodeURIComponent(token)}`, {}, cancelToken, { "Content-Type": "application/json" });
|
|
532
|
+
if (!resp || !("access_token" in resp)) {
|
|
521
533
|
if (resp.msg === "TOTP required") {
|
|
522
534
|
return LoginResponse.NEEDS_TOTP;
|
|
523
535
|
}
|
|
524
|
-
throw new StandardError(401,
|
|
536
|
+
throw new StandardError(401, "Invalid Apple login response");
|
|
525
537
|
}
|
|
526
538
|
this.setAuthState(AuthState.AUTHENTICATED);
|
|
527
539
|
this.socket.auth = { token: resp.access_token };
|
|
528
540
|
if (!this.socket.connected) {
|
|
529
541
|
this.socket.connect();
|
|
530
542
|
}
|
|
531
|
-
await this.config.authManager.setTokens({
|
|
543
|
+
await this.config.authManager.setTokens({
|
|
544
|
+
access_token: resp.access_token,
|
|
545
|
+
refresh_token: resp.refresh_token,
|
|
546
|
+
});
|
|
532
547
|
return LoginResponse.SUCCESS;
|
|
533
548
|
},
|
|
534
549
|
onSettled: () => {
|
|
@@ -536,8 +551,8 @@ export class AvroQueryClient {
|
|
|
536
551
|
},
|
|
537
552
|
onError: (err) => {
|
|
538
553
|
this.config.authManager.clearCache();
|
|
539
|
-
throw new StandardError(err.status, err.message ||
|
|
540
|
-
}
|
|
554
|
+
throw new StandardError(err.status, err.message || "Apple Login failed");
|
|
555
|
+
},
|
|
541
556
|
});
|
|
542
557
|
}
|
|
543
558
|
setTokens(tokens) {
|
|
@@ -550,7 +565,15 @@ export class AvroQueryClient {
|
|
|
550
565
|
return this.config.authManager.getCache(key);
|
|
551
566
|
}
|
|
552
567
|
setCompanyId(companyId) {
|
|
568
|
+
const previousId = this.companyId;
|
|
553
569
|
this.companyId = companyId;
|
|
570
|
+
// If the socket is already connected, leave the old room and join the new one.
|
|
571
|
+
if (this.socket.connected) {
|
|
572
|
+
if (previousId && previousId !== companyId) {
|
|
573
|
+
this.socket.emit("leave_company", { company_id: previousId });
|
|
574
|
+
}
|
|
575
|
+
this.socket.emit("join_company", { company_id: companyId });
|
|
576
|
+
}
|
|
554
577
|
return this.config.authManager.setCompanyId(companyId);
|
|
555
578
|
}
|
|
556
579
|
getCompanyId() {
|
|
@@ -576,10 +599,10 @@ export class AvroQueryClient {
|
|
|
576
599
|
return () => this.offAuthStateChange(cb);
|
|
577
600
|
}
|
|
578
601
|
offAuthStateChange(cb) {
|
|
579
|
-
this.authStateListeners = this.authStateListeners.filter(c => c !== cb);
|
|
602
|
+
this.authStateListeners = this.authStateListeners.filter((c) => c !== cb);
|
|
580
603
|
}
|
|
581
604
|
setAuthState(state) {
|
|
582
|
-
this.authStateListeners.forEach(cb => cb(state));
|
|
605
|
+
this.authStateListeners.forEach((cb) => cb(state));
|
|
583
606
|
this._authState = state;
|
|
584
607
|
}
|
|
585
608
|
getAuthState() {
|
|
@@ -591,13 +614,104 @@ export class AvroQueryClient {
|
|
|
591
614
|
getQueryClient() {
|
|
592
615
|
return useQueryClient();
|
|
593
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Fetch an entity from the API, optionally construct it, and surgically
|
|
619
|
+
* update all matching React-Query caches (individual + list + infinite).
|
|
620
|
+
*
|
|
621
|
+
* Shared by socket handlers and mutation `onSuccess` callbacks so the
|
|
622
|
+
* sender gets an immediate cache sync and everyone else gets the socket
|
|
623
|
+
* update — both use the identical code path.
|
|
624
|
+
*
|
|
625
|
+
* @returns The fetched (and optionally constructed) item, or `undefined`
|
|
626
|
+
* for deletes / entities without a fetchPath.
|
|
627
|
+
*/
|
|
628
|
+
async _syncEntity(queryClient, params) {
|
|
629
|
+
const { action, entityKey, id, fetchPath, construct } = params;
|
|
630
|
+
const predicate = (q) => matchesEntityKey(q, entityKey);
|
|
631
|
+
const invalidate = () => queryClient.invalidateQueries({ predicate });
|
|
632
|
+
// ─── DELETE ─────────────────────────────────────────
|
|
633
|
+
if (action === "delete") {
|
|
634
|
+
queryClient.removeQueries({ queryKey: [entityKey, id], exact: true });
|
|
635
|
+
queryClient.setQueriesData({ predicate, type: "active" }, (old) => {
|
|
636
|
+
if (!old)
|
|
637
|
+
return old;
|
|
638
|
+
if (old.pages && Array.isArray(old.pages)) {
|
|
639
|
+
return {
|
|
640
|
+
...old,
|
|
641
|
+
pages: old.pages.map((p) => p.filter((x) => x?.id !== id)),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (Array.isArray(old)) {
|
|
645
|
+
return old.filter((x) => x?.id !== id);
|
|
646
|
+
}
|
|
647
|
+
return old;
|
|
648
|
+
});
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
// ─── CREATE / UPDATE ────────────────────────────────
|
|
652
|
+
if (!fetchPath) {
|
|
653
|
+
invalidate();
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
const raw = await queryClient.fetchQuery({
|
|
658
|
+
queryKey: [entityKey, id],
|
|
659
|
+
queryFn: () => this.get({ path: fetchPath }),
|
|
660
|
+
staleTime: 0,
|
|
661
|
+
});
|
|
662
|
+
const item = construct ? construct(raw) : raw;
|
|
663
|
+
queryClient.setQueryData([entityKey, id], item);
|
|
664
|
+
if (action === "create") {
|
|
665
|
+
queryClient.setQueriesData({ predicate, type: "active" }, (old) => {
|
|
666
|
+
if (!old)
|
|
667
|
+
return old;
|
|
668
|
+
if (old.pages && Array.isArray(old.pages)) {
|
|
669
|
+
if (old.pages.some((p) => p.some((x) => x?.id === id)))
|
|
670
|
+
return old;
|
|
671
|
+
return {
|
|
672
|
+
...old,
|
|
673
|
+
pages: [[item, ...(old.pages[0] || [])], ...old.pages.slice(1)],
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
if (Array.isArray(old)) {
|
|
677
|
+
if (old.some((x) => x?.id === id))
|
|
678
|
+
return old;
|
|
679
|
+
return [...old, item];
|
|
680
|
+
}
|
|
681
|
+
return old;
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
// UPDATE — replace in every active list / infinite-query cache
|
|
686
|
+
queryClient.setQueriesData({ predicate, type: "active" }, (old) => {
|
|
687
|
+
if (!old)
|
|
688
|
+
return old;
|
|
689
|
+
if (old.pages && Array.isArray(old.pages)) {
|
|
690
|
+
return {
|
|
691
|
+
...old,
|
|
692
|
+
pages: old.pages.map((page) => page.map((x) => (x?.id === id ? item : x))),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
if (Array.isArray(old)) {
|
|
696
|
+
return old.map((x) => (x?.id === id ? item : x));
|
|
697
|
+
}
|
|
698
|
+
return old;
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
return item;
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
invalidate();
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
594
708
|
useLogout() {
|
|
595
709
|
const queryClient = this.getQueryClient();
|
|
596
710
|
return useMutation({
|
|
597
711
|
mutationFn: async (cancelToken) => {
|
|
598
712
|
await this.post({
|
|
599
|
-
path:
|
|
600
|
-
cancelToken
|
|
713
|
+
path: "/logout",
|
|
714
|
+
cancelToken,
|
|
601
715
|
});
|
|
602
716
|
await this.config.authManager.clearCache();
|
|
603
717
|
if (this.socket && this.socket.connected) {
|
|
@@ -611,276 +725,276 @@ export class AvroQueryClient {
|
|
|
611
725
|
},
|
|
612
726
|
onError: (err) => {
|
|
613
727
|
this.clearCache();
|
|
614
|
-
console.error(
|
|
615
|
-
throw new StandardError(500,
|
|
616
|
-
}
|
|
728
|
+
console.error("Logout failed:", err);
|
|
729
|
+
throw new StandardError(500, "Logout failed");
|
|
730
|
+
},
|
|
617
731
|
});
|
|
618
732
|
}
|
|
619
733
|
fetchJobs(body = {}, companyId, cancelToken, headers = {}) {
|
|
620
734
|
const companyIdToUse = companyId ?? this.companyId;
|
|
621
|
-
if (!companyIdToUse || companyIdToUse.trim() ===
|
|
622
|
-
throw new StandardError(400,
|
|
735
|
+
if (!companyIdToUse || companyIdToUse.trim() === "") {
|
|
736
|
+
throw new StandardError(400, "Company ID is required");
|
|
623
737
|
}
|
|
624
|
-
return this._fetch(
|
|
738
|
+
return this._fetch("POST", `/company/${companyIdToUse}/jobs`, JSON.stringify(body), cancelToken, {
|
|
625
739
|
...headers,
|
|
626
|
-
|
|
740
|
+
"Content-Type": "application/json",
|
|
627
741
|
})
|
|
628
|
-
.then(response => {
|
|
742
|
+
.then((response) => {
|
|
629
743
|
if (!response || !Array.isArray(response)) {
|
|
630
|
-
throw new StandardError(400,
|
|
744
|
+
throw new StandardError(400, "Invalid jobs response");
|
|
631
745
|
}
|
|
632
746
|
return response;
|
|
633
747
|
})
|
|
634
|
-
.catch(err => {
|
|
635
|
-
console.error(
|
|
748
|
+
.catch((err) => {
|
|
749
|
+
console.error("Failed to fetch jobs:", err);
|
|
636
750
|
throw new StandardError(500, `Failed to fetch jobs: ${err.message ?? err}`);
|
|
637
751
|
});
|
|
638
752
|
}
|
|
639
753
|
fetchChats(body = {}, cancelToken, headers = {}) {
|
|
640
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
641
|
-
throw new StandardError(400,
|
|
754
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
755
|
+
throw new StandardError(400, "Company ID is required");
|
|
642
756
|
}
|
|
643
|
-
return this._fetch(
|
|
757
|
+
return this._fetch("POST", `/company/${this.companyId}/chats`, JSON.stringify(body), cancelToken, {
|
|
644
758
|
...headers,
|
|
645
|
-
|
|
759
|
+
"Content-Type": "application/json",
|
|
646
760
|
})
|
|
647
|
-
.then(response => {
|
|
761
|
+
.then((response) => {
|
|
648
762
|
if (!response || !Array.isArray(response)) {
|
|
649
|
-
throw new StandardError(400,
|
|
763
|
+
throw new StandardError(400, "Invalid chats response");
|
|
650
764
|
}
|
|
651
765
|
return response;
|
|
652
766
|
})
|
|
653
|
-
.catch(err => {
|
|
654
|
-
console.error(
|
|
655
|
-
throw new StandardError(500,
|
|
767
|
+
.catch((err) => {
|
|
768
|
+
console.error("Failed to fetch chats:", err);
|
|
769
|
+
throw new StandardError(500, "Failed to fetch chats");
|
|
656
770
|
});
|
|
657
771
|
}
|
|
658
772
|
fetchMessages(chatId, body = {}, cancelToken, headers = {}) {
|
|
659
|
-
if (!chatId || chatId.trim() ===
|
|
660
|
-
throw new StandardError(400,
|
|
773
|
+
if (!chatId || chatId.trim() === "") {
|
|
774
|
+
throw new StandardError(400, "Chat ID is required");
|
|
661
775
|
}
|
|
662
|
-
return this._fetch(
|
|
776
|
+
return this._fetch("POST", `/chat/${chatId}/messages`, JSON.stringify(body), cancelToken, {
|
|
663
777
|
...headers,
|
|
664
|
-
|
|
778
|
+
"Content-Type": "application/json",
|
|
665
779
|
})
|
|
666
|
-
.then(response => {
|
|
780
|
+
.then((response) => {
|
|
667
781
|
if (!response || !Array.isArray(response)) {
|
|
668
|
-
throw new StandardError(400,
|
|
782
|
+
throw new StandardError(400, "Invalid messages response");
|
|
669
783
|
}
|
|
670
784
|
return response;
|
|
671
785
|
})
|
|
672
|
-
.catch(err => {
|
|
673
|
-
console.error(
|
|
674
|
-
throw new StandardError(500,
|
|
786
|
+
.catch((err) => {
|
|
787
|
+
console.error("Failed to fetch messages:", err);
|
|
788
|
+
throw new StandardError(500, "Failed to fetch messages");
|
|
675
789
|
});
|
|
676
790
|
}
|
|
677
791
|
async fetchPrepayments(body = {}, cancelToken, headers = {}) {
|
|
678
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
679
|
-
throw new StandardError(400,
|
|
792
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
793
|
+
throw new StandardError(400, "Company ID is required");
|
|
680
794
|
}
|
|
681
|
-
return this._fetch(
|
|
795
|
+
return this._fetch("POST", `/company/${this.companyId}/prepayments`, JSON.stringify(body), cancelToken, {
|
|
682
796
|
...headers,
|
|
683
|
-
|
|
797
|
+
"Content-Type": "application/json",
|
|
684
798
|
})
|
|
685
|
-
.then(response => {
|
|
799
|
+
.then((response) => {
|
|
686
800
|
if (!response || !Array.isArray(response)) {
|
|
687
|
-
throw new StandardError(400,
|
|
801
|
+
throw new StandardError(400, "Invalid prepayments response");
|
|
688
802
|
}
|
|
689
803
|
return response;
|
|
690
804
|
})
|
|
691
|
-
.catch(err => {
|
|
692
|
-
console.error(
|
|
693
|
-
throw new StandardError(500,
|
|
805
|
+
.catch((err) => {
|
|
806
|
+
console.error("Failed to fetch prepayments:", err);
|
|
807
|
+
throw new StandardError(500, "Failed to fetch prepayments");
|
|
694
808
|
});
|
|
695
809
|
}
|
|
696
810
|
async fetchWaivers(body = {}, cancelToken, headers = {}) {
|
|
697
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
698
|
-
throw new StandardError(400,
|
|
811
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
812
|
+
throw new StandardError(400, "Company ID is required");
|
|
699
813
|
}
|
|
700
|
-
return this._fetch(
|
|
814
|
+
return this._fetch("POST", `/company/${this.companyId}/waivers`, JSON.stringify(body), cancelToken, {
|
|
701
815
|
...headers,
|
|
702
|
-
|
|
816
|
+
"Content-Type": "application/json",
|
|
703
817
|
})
|
|
704
|
-
.then(response => {
|
|
818
|
+
.then((response) => {
|
|
705
819
|
if (!response || !Array.isArray(response)) {
|
|
706
|
-
throw new StandardError(400,
|
|
820
|
+
throw new StandardError(400, "Invalid waivers response");
|
|
707
821
|
}
|
|
708
822
|
return response;
|
|
709
823
|
})
|
|
710
|
-
.catch(err => {
|
|
711
|
-
console.error(
|
|
712
|
-
throw new StandardError(500,
|
|
824
|
+
.catch((err) => {
|
|
825
|
+
console.error("Failed to fetch waivers:", err);
|
|
826
|
+
throw new StandardError(500, "Failed to fetch waivers");
|
|
713
827
|
});
|
|
714
828
|
}
|
|
715
829
|
async fetchEvents(body = {}, cancelToken, headers = {}) {
|
|
716
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
717
|
-
throw new StandardError(400,
|
|
830
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
831
|
+
throw new StandardError(400, "Company ID is required");
|
|
718
832
|
}
|
|
719
|
-
return this._fetch(
|
|
833
|
+
return this._fetch("POST", `/company/${this.companyId}/events`, JSON.stringify(body), cancelToken, {
|
|
720
834
|
...headers,
|
|
721
|
-
|
|
835
|
+
"Content-Type": "application/json",
|
|
722
836
|
})
|
|
723
|
-
.then(response => {
|
|
837
|
+
.then((response) => {
|
|
724
838
|
if (!response || !Array.isArray(response)) {
|
|
725
|
-
throw new StandardError(400,
|
|
839
|
+
throw new StandardError(400, "Invalid events response");
|
|
726
840
|
}
|
|
727
841
|
return response;
|
|
728
842
|
})
|
|
729
|
-
.catch(err => {
|
|
730
|
-
console.error(
|
|
731
|
-
throw new StandardError(500,
|
|
843
|
+
.catch((err) => {
|
|
844
|
+
console.error("Failed to fetch events:", err);
|
|
845
|
+
throw new StandardError(500, "Failed to fetch events");
|
|
732
846
|
});
|
|
733
847
|
}
|
|
734
848
|
fetchMonths(body = {}, cancelToken, headers = {}) {
|
|
735
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
736
|
-
throw new StandardError(400,
|
|
849
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
850
|
+
throw new StandardError(400, "Company ID is required");
|
|
737
851
|
}
|
|
738
|
-
return this._fetch(
|
|
852
|
+
return this._fetch("POST", `/company/${this.companyId}/months`, JSON.stringify(body), cancelToken, {
|
|
739
853
|
...headers,
|
|
740
|
-
|
|
854
|
+
"Content-Type": "application/json",
|
|
741
855
|
})
|
|
742
|
-
.then(response => {
|
|
856
|
+
.then((response) => {
|
|
743
857
|
if (!response || !Array.isArray(response)) {
|
|
744
|
-
throw new StandardError(400,
|
|
858
|
+
throw new StandardError(400, "Invalid months response");
|
|
745
859
|
}
|
|
746
860
|
return response;
|
|
747
861
|
})
|
|
748
|
-
.catch(err => {
|
|
749
|
-
console.error(
|
|
750
|
-
throw new StandardError(500,
|
|
862
|
+
.catch((err) => {
|
|
863
|
+
console.error("Failed to fetch months:", err);
|
|
864
|
+
throw new StandardError(500, "Failed to fetch months");
|
|
751
865
|
});
|
|
752
866
|
}
|
|
753
867
|
fetchBills(body = {}, cancelToken, headers = {}) {
|
|
754
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
755
|
-
throw new StandardError(400,
|
|
868
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
869
|
+
throw new StandardError(400, "Company ID is required");
|
|
756
870
|
}
|
|
757
|
-
return this._fetch(
|
|
871
|
+
return this._fetch("POST", `/company/${this.companyId}/bills`, JSON.stringify(body), cancelToken, {
|
|
758
872
|
...headers,
|
|
759
|
-
|
|
873
|
+
"Content-Type": "application/json",
|
|
760
874
|
})
|
|
761
|
-
.then(response => {
|
|
875
|
+
.then((response) => {
|
|
762
876
|
if (!response || !Array.isArray(response)) {
|
|
763
|
-
throw new StandardError(400,
|
|
877
|
+
throw new StandardError(400, "Invalid bills response");
|
|
764
878
|
}
|
|
765
879
|
return response;
|
|
766
880
|
})
|
|
767
|
-
.catch(err => {
|
|
768
|
-
console.error(
|
|
769
|
-
throw new StandardError(500,
|
|
881
|
+
.catch((err) => {
|
|
882
|
+
console.error("Failed to fetch bills:", err);
|
|
883
|
+
throw new StandardError(500, "Failed to fetch bills");
|
|
770
884
|
});
|
|
771
885
|
}
|
|
772
886
|
fetchRoutes(body = {}, cancelToken, headers = {}) {
|
|
773
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
774
|
-
throw new StandardError(400,
|
|
887
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
888
|
+
throw new StandardError(400, "Company ID is required");
|
|
775
889
|
}
|
|
776
|
-
return this._fetch(
|
|
890
|
+
return this._fetch("POST", `/company/${this.companyId}/routes`, JSON.stringify(body), cancelToken, {
|
|
777
891
|
...headers,
|
|
778
|
-
|
|
892
|
+
"Content-Type": "application/json",
|
|
779
893
|
})
|
|
780
|
-
.then(response => {
|
|
894
|
+
.then((response) => {
|
|
781
895
|
if (!response || !Array.isArray(response)) {
|
|
782
|
-
throw new StandardError(400,
|
|
896
|
+
throw new StandardError(400, "Invalid routes response");
|
|
783
897
|
}
|
|
784
898
|
return response;
|
|
785
899
|
})
|
|
786
|
-
.catch(err => {
|
|
787
|
-
console.error(
|
|
788
|
-
throw new StandardError(500,
|
|
900
|
+
.catch((err) => {
|
|
901
|
+
console.error("Failed to fetch routes:", err);
|
|
902
|
+
throw new StandardError(500, "Failed to fetch routes");
|
|
789
903
|
});
|
|
790
904
|
}
|
|
791
905
|
fetchTeams(body = {}, cancelToken, headers = {}) {
|
|
792
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
793
|
-
throw new StandardError(400,
|
|
906
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
907
|
+
throw new StandardError(400, "Company ID is required");
|
|
794
908
|
}
|
|
795
|
-
return this._fetch(
|
|
909
|
+
return this._fetch("POST", `/company/${this.companyId}/teams`, JSON.stringify(body), cancelToken, {
|
|
796
910
|
...headers,
|
|
797
|
-
|
|
911
|
+
"Content-Type": "application/json",
|
|
798
912
|
})
|
|
799
|
-
.then(response => {
|
|
913
|
+
.then((response) => {
|
|
800
914
|
if (!response || !Array.isArray(response)) {
|
|
801
|
-
throw new StandardError(400,
|
|
915
|
+
throw new StandardError(400, "Invalid teams response");
|
|
802
916
|
}
|
|
803
917
|
return response;
|
|
804
918
|
})
|
|
805
|
-
.catch(err => {
|
|
806
|
-
console.error(
|
|
807
|
-
throw new StandardError(500,
|
|
919
|
+
.catch((err) => {
|
|
920
|
+
console.error("Failed to fetch teams:", err);
|
|
921
|
+
throw new StandardError(500, "Failed to fetch teams");
|
|
808
922
|
});
|
|
809
923
|
}
|
|
810
924
|
fetchLabels(body = {}, cancelToken, headers = {}) {
|
|
811
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
812
|
-
throw new StandardError(400,
|
|
925
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
926
|
+
throw new StandardError(400, "Company ID is required");
|
|
813
927
|
}
|
|
814
|
-
return this._fetch(
|
|
928
|
+
return this._fetch("POST", `/company/${this.companyId}/labels`, JSON.stringify(body), cancelToken, {
|
|
815
929
|
...headers,
|
|
816
|
-
|
|
930
|
+
"Content-Type": "application/json",
|
|
817
931
|
})
|
|
818
|
-
.then(response => {
|
|
932
|
+
.then((response) => {
|
|
819
933
|
if (!response || !Array.isArray(response)) {
|
|
820
|
-
throw new StandardError(400,
|
|
934
|
+
throw new StandardError(400, "Invalid labels response");
|
|
821
935
|
}
|
|
822
936
|
return response;
|
|
823
937
|
})
|
|
824
|
-
.catch(err => {
|
|
825
|
-
console.error(
|
|
826
|
-
throw new StandardError(500,
|
|
938
|
+
.catch((err) => {
|
|
939
|
+
console.error("Failed to fetch labels:", err);
|
|
940
|
+
throw new StandardError(500, "Failed to fetch labels");
|
|
827
941
|
});
|
|
828
942
|
}
|
|
829
943
|
fetchGroups(body = {}, cancelToken, headers = {}) {
|
|
830
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
831
|
-
throw new StandardError(400,
|
|
944
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
945
|
+
throw new StandardError(400, "Company ID is required");
|
|
832
946
|
}
|
|
833
|
-
return this._fetch(
|
|
947
|
+
return this._fetch("POST", `/company/${this.companyId}/groups`, JSON.stringify(body), cancelToken, {
|
|
834
948
|
...headers,
|
|
835
|
-
|
|
949
|
+
"Content-Type": "application/json",
|
|
836
950
|
})
|
|
837
|
-
.then(response => {
|
|
951
|
+
.then((response) => {
|
|
838
952
|
if (!response || !Array.isArray(response)) {
|
|
839
|
-
throw new StandardError(400,
|
|
953
|
+
throw new StandardError(400, "Invalid groups response");
|
|
840
954
|
}
|
|
841
955
|
return response;
|
|
842
956
|
})
|
|
843
|
-
.catch(err => {
|
|
844
|
-
console.error(
|
|
845
|
-
throw new StandardError(500,
|
|
957
|
+
.catch((err) => {
|
|
958
|
+
console.error("Failed to fetch groups:", err);
|
|
959
|
+
throw new StandardError(500, "Failed to fetch groups");
|
|
846
960
|
});
|
|
847
961
|
}
|
|
848
962
|
fetchSkills(body = {}, cancelToken, headers = {}) {
|
|
849
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
850
|
-
throw new StandardError(400,
|
|
963
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
964
|
+
throw new StandardError(400, "Company ID is required");
|
|
851
965
|
}
|
|
852
|
-
return this._fetch(
|
|
966
|
+
return this._fetch("POST", `/company/${this.companyId}/skills`, JSON.stringify(body), cancelToken, {
|
|
853
967
|
...headers,
|
|
854
|
-
|
|
968
|
+
"Content-Type": "application/json",
|
|
855
969
|
})
|
|
856
|
-
.then(response => {
|
|
970
|
+
.then((response) => {
|
|
857
971
|
if (!response || !Array.isArray(response)) {
|
|
858
|
-
throw new StandardError(400,
|
|
972
|
+
throw new StandardError(400, "Invalid skills response");
|
|
859
973
|
}
|
|
860
974
|
return response;
|
|
861
975
|
})
|
|
862
|
-
.catch(err => {
|
|
863
|
-
console.error(
|
|
864
|
-
throw new StandardError(500,
|
|
976
|
+
.catch((err) => {
|
|
977
|
+
console.error("Failed to fetch skills:", err);
|
|
978
|
+
throw new StandardError(500, "Failed to fetch skills");
|
|
865
979
|
});
|
|
866
980
|
}
|
|
867
981
|
fetchSessions(body = {}, cancelToken, headers = {}) {
|
|
868
|
-
if (!this.companyId || this.companyId.trim() ===
|
|
869
|
-
throw new StandardError(400,
|
|
982
|
+
if (!this.companyId || this.companyId.trim() === "") {
|
|
983
|
+
throw new StandardError(400, "Company ID is required");
|
|
870
984
|
}
|
|
871
|
-
return this._fetch(
|
|
985
|
+
return this._fetch("POST", `/company/${this.companyId}/sessions`, JSON.stringify(body), cancelToken, {
|
|
872
986
|
...headers,
|
|
873
|
-
|
|
987
|
+
"Content-Type": "application/json",
|
|
874
988
|
})
|
|
875
|
-
.then(response => {
|
|
989
|
+
.then((response) => {
|
|
876
990
|
if (!response || !Array.isArray(response)) {
|
|
877
|
-
throw new StandardError(400,
|
|
991
|
+
throw new StandardError(400, "Invalid sessions response");
|
|
878
992
|
}
|
|
879
993
|
return response;
|
|
880
994
|
})
|
|
881
|
-
.catch(err => {
|
|
882
|
-
console.error(
|
|
883
|
-
throw new StandardError(500,
|
|
995
|
+
.catch((err) => {
|
|
996
|
+
console.error("Failed to fetch sessions:", err);
|
|
997
|
+
throw new StandardError(500, "Failed to fetch sessions");
|
|
884
998
|
});
|
|
885
999
|
}
|
|
886
1000
|
/* ── Email delivery tracking ──────────────────────────────────────── */
|
|
@@ -945,7 +1059,7 @@ export class AvroQueryClient {
|
|
|
945
1059
|
return this.post({
|
|
946
1060
|
path: `/email/${emailId}`,
|
|
947
1061
|
data: formData,
|
|
948
|
-
progressUpdateCallback
|
|
1062
|
+
progressUpdateCallback,
|
|
949
1063
|
});
|
|
950
1064
|
}
|
|
951
1065
|
catch (error) {
|
|
@@ -957,7 +1071,7 @@ export class AvroQueryClient {
|
|
|
957
1071
|
return this.post({
|
|
958
1072
|
path: `/bill/${billId}/email`,
|
|
959
1073
|
data: JSON.stringify(body),
|
|
960
|
-
headers: {
|
|
1074
|
+
headers: { "Content-Type": "application/json" },
|
|
961
1075
|
});
|
|
962
1076
|
}
|
|
963
1077
|
catch (error) {
|