@jskit-ai/users-web 0.1.105 → 0.1.107

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.
@@ -3,7 +3,7 @@ import { HOME_COG_OUTLET } from "./src/shared/toolsOutletContracts.js";
3
3
  export default Object.freeze({
4
4
  packageVersion: 1,
5
5
  packageId: "@jskit-ai/users-web",
6
- version: "0.1.105",
6
+ version: "0.1.107",
7
7
  kind: "runtime",
8
8
  description: "Users web module: account/profile UI plus shared users web widgets.",
9
9
  dependsOn: [
@@ -278,12 +278,12 @@ export default Object.freeze({
278
278
  dependencies: {
279
279
  runtime: {
280
280
  "@mdi/js": "^7.4.47",
281
- "@jskit-ai/http-runtime": "0.1.89",
282
- "@jskit-ai/realtime": "0.1.89",
283
- "@jskit-ai/kernel": "0.1.90",
284
- "@jskit-ai/shell-web": "0.1.89",
285
- "@jskit-ai/uploads-image-web": "0.1.68",
286
- "@jskit-ai/users-core": "0.1.100"
281
+ "@jskit-ai/http-runtime": "0.1.91",
282
+ "@jskit-ai/realtime": "0.1.91",
283
+ "@jskit-ai/kernel": "0.1.92",
284
+ "@jskit-ai/shell-web": "0.1.91",
285
+ "@jskit-ai/uploads-image-web": "0.1.70",
286
+ "@jskit-ai/users-core": "0.1.102"
287
287
  },
288
288
  dev: {}
289
289
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.1.105",
3
+ "version": "0.1.107",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -44,12 +44,12 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@mdi/js": "^7.4.47",
47
- "@jskit-ai/http-runtime": "0.1.89",
48
- "@jskit-ai/kernel": "0.1.90",
49
- "@jskit-ai/realtime": "0.1.89",
50
- "@jskit-ai/shell-web": "0.1.89",
51
- "@jskit-ai/uploads-image-web": "0.1.68",
52
- "@jskit-ai/users-core": "0.1.100"
47
+ "@jskit-ai/http-runtime": "0.1.91",
48
+ "@jskit-ai/kernel": "0.1.92",
49
+ "@jskit-ai/realtime": "0.1.91",
50
+ "@jskit-ai/shell-web": "0.1.91",
51
+ "@jskit-ai/uploads-image-web": "0.1.70",
52
+ "@jskit-ai/users-core": "0.1.102"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@tanstack/vue-query": "^5.90.5",
@@ -29,7 +29,10 @@ function useAddEdit({
29
29
  readMethod = "GET",
30
30
  readEnabled = true,
31
31
  writeMethod = "PATCH",
32
+ client = null,
32
33
  transport = null,
34
+ requestRecovery = null,
35
+ requestRecoveryLabel = "Resource",
33
36
  placementSource = "users-web.add-edit",
34
37
  fallbackLoadError = "Unable to load resource.",
35
38
  fallbackSaveError = "Unable to save resource.",
@@ -117,10 +120,13 @@ function useAddEdit({
117
120
  queryKey: requestQueryRuntime.queryKey,
118
121
  path: operationScope.apiPath,
119
122
  enabled: queryCanRun,
123
+ client,
120
124
  readMethod,
121
125
  writeMethod,
122
126
  readQuery: requestQueryRuntime.requestQuery,
123
127
  transport,
128
+ requestRecovery,
129
+ requestRecoveryLabel,
124
130
  fallbackLoadError,
125
131
  fallbackSaveError: String(fallbackSaveError || effectiveMessages.saveError || "Unable to save resource.")
126
132
  });
@@ -43,9 +43,12 @@ function useList({
43
43
  initialPageParam = null,
44
44
  getNextPageParam,
45
45
  selectItems,
46
+ client = null,
46
47
  transport = null,
47
48
  requestOptions,
48
49
  queryOptions,
50
+ requestRecovery,
51
+ requestRecoveryLabel = "List",
49
52
  realtime = null,
50
53
  adapter = null,
51
54
  recordIdParam = "recordId",
@@ -222,12 +225,15 @@ function useList({
222
225
  queryKey: listQueryKey,
223
226
  path: listPath,
224
227
  enabled: operationScope.queryCanRun(canView),
228
+ client,
225
229
  transport,
226
230
  initialPageParam,
227
231
  getNextPageParam,
228
232
  selectItems,
229
233
  requestOptions: listRequestOptions,
230
234
  queryOptions,
235
+ requestRecovery,
236
+ requestRecoveryLabel,
231
237
  fallbackLoadError
232
238
  });
233
239
  const routeSyncHydrated = ref(routeSyncConfig.enabled !== true);
@@ -20,7 +20,10 @@ function useView({
20
20
  viewPermissions = [],
21
21
  readMethod = "GET",
22
22
  readEnabled = true,
23
+ client = null,
23
24
  transport = null,
25
+ requestRecovery = null,
26
+ requestRecoveryLabel = "Resource",
24
27
  placementSource = "users-web.view",
25
28
  fallbackLoadError = "Unable to load resource.",
26
29
  notFoundStatuses = [404],
@@ -101,9 +104,12 @@ function useView({
101
104
  queryKey: requestQueryRuntime.queryKey,
102
105
  path: operationScope.apiPath,
103
106
  enabled: operationScope.queryCanRun(canView),
107
+ client,
104
108
  readMethod,
105
109
  readQuery: requestQueryRuntime.requestQuery,
106
110
  transport,
111
+ requestRecovery,
112
+ requestRecoveryLabel,
107
113
  refreshOnPull: true,
108
114
  fallbackLoadError
109
115
  });
@@ -1,13 +1,14 @@
1
1
  import { computed, unref } from "vue";
2
2
  import { useMutation, useQuery } from "@tanstack/vue-query";
3
- import { usersWebHttpClient } from "../../lib/httpClient.js";
3
+ import { getUsersWebHttpClient } from "../../lib/httpClient.js";
4
4
  import { asPlainObject } from "../support/scopeHelpers.js";
5
5
  import { resolveEnabledRef, resolveTextRef } from "../support/refValueHelpers.js";
6
6
  import { toQueryErrorMessage } from "../support/errorMessageHelpers.js";
7
7
  import { useOperationRealtime } from "../useRealtimeQueryInvalidation.js";
8
8
  import {
9
9
  hasResolvedQueryData,
10
- mergeQueryMeta
10
+ mergeQueryMeta,
11
+ mergeRequestRecoveryQueryMeta
11
12
  } from "../support/resourceLoadStateHelpers.js";
12
13
 
13
14
  function buildEndpointReadRequestOptions({
@@ -66,7 +67,7 @@ function useEndpointResource({
66
67
  queryKey,
67
68
  path = "",
68
69
  enabled = true,
69
- client = usersWebHttpClient,
70
+ client = null,
70
71
  readMethod = "GET",
71
72
  writeMethod = "PATCH",
72
73
  readQuery = null,
@@ -75,10 +76,13 @@ function useEndpointResource({
75
76
  realtime = null,
76
77
  queryOptions = null,
77
78
  mutationOptions = null,
79
+ requestRecovery = null,
80
+ requestRecoveryLabel = "Request",
78
81
  fallbackLoadError = "Unable to load resource.",
79
82
  fallbackSaveError = "Unable to save resource."
80
83
  } = {}) {
81
- if (!client || typeof client.request !== "function") {
84
+ const activeClient = client || getUsersWebHttpClient();
85
+ if (!activeClient || typeof activeClient.request !== "function") {
82
86
  throw new TypeError("useEndpointResource requires a client with request().");
83
87
  }
84
88
 
@@ -93,7 +97,7 @@ function useEndpointResource({
93
97
  throw new Error("Resource path is required.");
94
98
  }
95
99
 
96
- return client.request(requestPath, {
100
+ return activeClient.request(requestPath, {
97
101
  ...buildEndpointReadRequestOptions({
98
102
  method: readMethod,
99
103
  query: readQuery,
@@ -102,13 +106,20 @@ function useEndpointResource({
102
106
  });
103
107
  },
104
108
  enabled: queryEnabled,
105
- ...(refreshOnPull
106
- ? mergeQueryMeta(asPlainObject(queryOptions), {
107
- jskit: {
108
- refreshOnPull: true
109
- }
110
- })
111
- : asPlainObject(queryOptions))
109
+ ...mergeRequestRecoveryQueryMeta(
110
+ refreshOnPull
111
+ ? mergeQueryMeta(asPlainObject(queryOptions), {
112
+ jskit: {
113
+ refreshOnPull: true
114
+ }
115
+ })
116
+ : asPlainObject(queryOptions),
117
+ requestRecovery,
118
+ {
119
+ label: requestRecoveryLabel,
120
+ method: readMethod
121
+ }
122
+ )
112
123
  });
113
124
  const realtimeBinding = useOperationRealtime({
114
125
  realtime,
@@ -127,7 +138,7 @@ function useEndpointResource({
127
138
  const method = String(options.method || writeMethod || "PATCH").toUpperCase();
128
139
  const hasBody = Object.hasOwn(options, "body");
129
140
  const body = hasBody ? options.body : options.payload;
130
- return client.request(
141
+ return activeClient.request(
131
142
  requestPath,
132
143
  buildEndpointWriteRequestOptions({
133
144
  method,
@@ -1,8 +1,9 @@
1
1
  import { computed, unref } from "vue";
2
- import { usersWebHttpClient } from "../../lib/httpClient.js";
2
+ import { getUsersWebHttpClient } from "../../lib/httpClient.js";
3
3
  import { asPlainObject } from "../support/scopeHelpers.js";
4
4
  import { resolveEnabledRef, resolveTextRef } from "../support/refValueHelpers.js";
5
5
  import { usePagedCollection } from "../usePagedCollection.js";
6
+ import { mergeRequestRecoveryQueryMeta } from "../support/resourceLoadStateHelpers.js";
6
7
 
7
8
  const DEFAULT_LIST_LIMIT = 20;
8
9
 
@@ -47,20 +48,27 @@ function resolveRequestOptionsObject(value = null) {
47
48
  return asPlainObject(source);
48
49
  }
49
50
 
51
+ function resolveListRequestMethod(requestOptions = null) {
52
+ return String(resolveRequestOptionsObject(requestOptions).method || "GET").toUpperCase();
53
+ }
54
+
50
55
  function useListCore({
51
56
  queryKey,
52
57
  path = "",
53
58
  enabled = true,
54
- client = usersWebHttpClient,
59
+ client = null,
55
60
  transport = null,
56
61
  initialPageParam = null,
57
62
  getNextPageParam,
58
63
  selectItems,
59
64
  requestOptions = null,
60
65
  queryOptions = null,
66
+ requestRecovery = null,
67
+ requestRecoveryLabel = "List",
61
68
  fallbackLoadError = "Unable to load list."
62
69
  } = {}) {
63
- if (!client || typeof client.request !== "function") {
70
+ const activeClient = client || getUsersWebHttpClient();
71
+ if (!activeClient || typeof activeClient.request !== "function") {
64
72
  throw new TypeError("useListCore requires a client with request().");
65
73
  }
66
74
 
@@ -77,7 +85,7 @@ function useListCore({
77
85
  throw new Error("List path is required.");
78
86
  }
79
87
 
80
- return client.request(
88
+ return activeClient.request(
81
89
  requestPath,
82
90
  buildListRequestOptions({
83
91
  requestOptions,
@@ -88,7 +96,10 @@ function useListCore({
88
96
  },
89
97
  getNextPageParam,
90
98
  selectItems,
91
- queryOptions,
99
+ queryOptions: mergeRequestRecoveryQueryMeta(queryOptions, requestRecovery, {
100
+ label: requestRecoveryLabel,
101
+ method: resolveListRequestMethod(requestOptions)
102
+ }),
92
103
  fallbackLoadError
93
104
  });
94
105
 
@@ -36,7 +36,194 @@ function mergeQueryMeta(queryOptions = null, meta = {}) {
36
36
  };
37
37
  }
38
38
 
39
+ function normalizeText(value = "") {
40
+ return String(value || "").trim();
41
+ }
42
+
43
+ function firstNormalizedText(...values) {
44
+ for (const value of values) {
45
+ const normalized = normalizeText(value);
46
+ if (normalized) {
47
+ return normalized;
48
+ }
49
+ }
50
+ return "";
51
+ }
52
+
53
+ function firstNonNegativeFiniteNumber(...values) {
54
+ for (const value of values) {
55
+ const numberValue = Number(value);
56
+ if (Number.isFinite(numberValue)) {
57
+ return Math.max(0, numberValue);
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+
63
+ function normalizePlainObject(value = null) {
64
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
65
+ }
66
+
67
+ function normalizeRequestRecoveryInput(requestRecovery = null) {
68
+ if (typeof requestRecovery === "string") {
69
+ return {
70
+ label: requestRecovery
71
+ };
72
+ }
73
+ return normalizePlainObject(requestRecovery);
74
+ }
75
+
76
+ function normalizeRequestRecoveryMethod(value = "") {
77
+ return normalizeText(value).toUpperCase();
78
+ }
79
+
80
+ function isRequestRecoveryMetaDisabled(sourceJskitMeta = {}, sourceMeta = {}) {
81
+ return Boolean(
82
+ sourceJskitMeta.requestRecovery === false ||
83
+ sourceMeta.jskitRequestRecovery === false ||
84
+ sourceMeta.requestRecovery === false
85
+ );
86
+ }
87
+
88
+ function hasUsableMetaValue(source = {}, key = "") {
89
+ return Object.hasOwn(source, key) && source[key] !== undefined && source[key] !== null && source[key] !== "";
90
+ }
91
+
92
+ function buildRequestRecoveryMeta(requestRecovery = null, defaults = {}) {
93
+ if (requestRecovery === false) {
94
+ return {
95
+ jskit: {
96
+ requestRecovery: false
97
+ }
98
+ };
99
+ }
100
+
101
+ const fallback = normalizePlainObject(defaults);
102
+ const source = normalizeRequestRecoveryInput(requestRecovery);
103
+ if (source.requestRecovery === false || source.enabled === false) {
104
+ return {
105
+ jskit: {
106
+ requestRecovery: false
107
+ }
108
+ };
109
+ }
110
+
111
+ const label = firstNormalizedText(
112
+ source.requestRecoveryLabel,
113
+ source.label,
114
+ fallback.requestRecoveryLabel,
115
+ fallback.label
116
+ );
117
+ const recoverySource = firstNormalizedText(
118
+ source.requestRecoverySource,
119
+ source.source,
120
+ fallback.requestRecoverySource,
121
+ fallback.source
122
+ );
123
+ const dedupeKey = firstNormalizedText(
124
+ source.requestRecoveryDedupeKey,
125
+ source.dedupeKey,
126
+ fallback.requestRecoveryDedupeKey,
127
+ fallback.dedupeKey
128
+ );
129
+ const method = normalizeRequestRecoveryMethod(
130
+ firstNormalizedText(
131
+ source.requestRecoveryMethod,
132
+ source.method,
133
+ fallback.requestRecoveryMethod,
134
+ fallback.method
135
+ )
136
+ );
137
+ const dedupeWindowMs = firstNonNegativeFiniteNumber(
138
+ source.requestRecoveryDedupeWindowMs,
139
+ source.dedupeWindowMs,
140
+ fallback.requestRecoveryDedupeWindowMs,
141
+ fallback.dedupeWindowMs
142
+ );
143
+
144
+ const jskit = {};
145
+ if (label) {
146
+ jskit.requestRecoveryLabel = label;
147
+ }
148
+ if (recoverySource) {
149
+ jskit.requestRecoverySource = recoverySource;
150
+ }
151
+ if (dedupeKey) {
152
+ jskit.requestRecoveryDedupeKey = dedupeKey;
153
+ }
154
+ if (method) {
155
+ jskit.requestRecoveryMethod = method;
156
+ }
157
+ if (dedupeWindowMs !== null) {
158
+ jskit.requestRecoveryDedupeWindowMs = dedupeWindowMs;
159
+ }
160
+
161
+ return Object.keys(jskit).length > 0 ? { jskit } : {};
162
+ }
163
+
164
+ function hasRequestRecoveryMetaValue(sourceJskitMeta = {}, sourceMeta = {}, key = "") {
165
+ const suffix = String(key || "").trim();
166
+ if (!suffix) {
167
+ return false;
168
+ }
169
+
170
+ const jskitKey = `requestRecovery${suffix}`;
171
+ const legacyJskitKey = `jskitRequestRecovery${suffix}`;
172
+ const legacyKey = `requestRecovery${suffix}`;
173
+
174
+ return Boolean(
175
+ hasUsableMetaValue(sourceJskitMeta, jskitKey) ||
176
+ hasUsableMetaValue(sourceMeta, legacyJskitKey) ||
177
+ hasUsableMetaValue(sourceMeta, legacyKey)
178
+ );
179
+ }
180
+
181
+ function resolveRequestRecoveryDefaults(queryOptions = null, defaults = {}) {
182
+ const sourceOptions = normalizePlainObject(queryOptions);
183
+ const sourceMeta = normalizePlainObject(sourceOptions.meta);
184
+ const sourceJskitMeta = normalizePlainObject(sourceMeta.jskit);
185
+ const fallback = normalizePlainObject(defaults);
186
+ if (isRequestRecoveryMetaDisabled(sourceJskitMeta, sourceMeta)) {
187
+ return {};
188
+ }
189
+
190
+ const hasExplicitLabel = hasRequestRecoveryMetaValue(sourceJskitMeta, sourceMeta, "Label");
191
+ const hasExplicitSource = hasRequestRecoveryMetaValue(sourceJskitMeta, sourceMeta, "Source");
192
+ const hasExplicitDedupeKey = hasRequestRecoveryMetaValue(sourceJskitMeta, sourceMeta, "DedupeKey");
193
+ const hasExplicitDedupeWindowMs = hasRequestRecoveryMetaValue(sourceJskitMeta, sourceMeta, "DedupeWindowMs");
194
+ const hasExplicitMethod = hasRequestRecoveryMetaValue(sourceJskitMeta, sourceMeta, "Method");
195
+
196
+ return {
197
+ ...fallback,
198
+ requestRecoveryLabel: hasExplicitLabel ? "" : fallback.requestRecoveryLabel,
199
+ label: hasExplicitLabel ? "" : fallback.label,
200
+ requestRecoverySource: hasExplicitSource ? "" : fallback.requestRecoverySource,
201
+ source: hasExplicitSource ? "" : fallback.source,
202
+ requestRecoveryDedupeKey: hasExplicitDedupeKey ? "" : fallback.requestRecoveryDedupeKey,
203
+ dedupeKey: hasExplicitDedupeKey ? "" : fallback.dedupeKey,
204
+ requestRecoveryDedupeWindowMs: hasExplicitDedupeWindowMs ? "" : fallback.requestRecoveryDedupeWindowMs,
205
+ dedupeWindowMs: hasExplicitDedupeWindowMs ? "" : fallback.dedupeWindowMs,
206
+ requestRecoveryMethod: hasExplicitMethod ? "" : fallback.requestRecoveryMethod,
207
+ method: hasExplicitMethod ? "" : fallback.method
208
+ };
209
+ }
210
+
211
+ function mergeRequestRecoveryQueryMeta(queryOptions = null, requestRecovery = null, defaults = {}) {
212
+ const sourceOptions = normalizePlainObject(queryOptions);
213
+ const effectiveDefaults =
214
+ requestRecovery == null
215
+ ? resolveRequestRecoveryDefaults(sourceOptions, defaults)
216
+ : normalizePlainObject(defaults);
217
+ const meta = buildRequestRecoveryMeta(requestRecovery, effectiveDefaults);
218
+ if (Object.keys(meta).length < 1) {
219
+ return sourceOptions;
220
+ }
221
+ return mergeQueryMeta(sourceOptions, meta);
222
+ }
223
+
39
224
  export {
225
+ buildRequestRecoveryMeta,
40
226
  hasResolvedQueryData,
41
- mergeQueryMeta
227
+ mergeQueryMeta,
228
+ mergeRequestRecoveryQueryMeta
42
229
  };
@@ -17,6 +17,7 @@ function useCommand({
17
17
  apiSuffix = "",
18
18
  runPermissions = [],
19
19
  writeMethod = "POST",
20
+ client = null,
20
21
  transport = null,
21
22
  placementSource = "users-web.command",
22
23
  fallbackRunError = "Unable to complete action.",
@@ -52,6 +53,7 @@ function useCommand({
52
53
  const resource = useEndpointResource({
53
54
  path: operationScope.apiPath,
54
55
  enabled: false,
56
+ client,
55
57
  writeMethod,
56
58
  transport,
57
59
  fallbackSaveError: fallbackRunError
@@ -31,6 +31,7 @@ function useCrudListScreen({
31
31
  listFilters = {},
32
32
  listBulkActions = [],
33
33
  routeQueryBlacklist = Object.freeze(["include", "cursor", "limit"]),
34
+ requestRecoveryLabel = "Records",
34
35
  fallbackLoadError = "Unable to load records."
35
36
  } = {}) {
36
37
  const filterRuntime = useCrudListFilters(listFilters);
@@ -62,6 +63,7 @@ function useCrudListScreen({
62
63
  queryParamBlacklist: routeQueryBlacklist
63
64
  },
64
65
  placementSource: `ui-generator.${normalizedResourceNamespace}.list`,
66
+ requestRecoveryLabel,
65
67
  fallbackLoadError,
66
68
  recordIdParam,
67
69
  recordIdSelector,
@@ -12,6 +12,7 @@ function useCrudViewScreen({
12
12
  listUrlTemplate = "",
13
13
  editUrlTemplate = "",
14
14
  recordChangedEvent = "",
15
+ requestRecoveryLabel = "Record",
15
16
  fallbackLoadError = "Unable to load record.",
16
17
  notFoundMessage = "Record not found."
17
18
  } = {}) {
@@ -31,6 +32,7 @@ function useCrudViewScreen({
31
32
  String(workspaceSlug || "")
32
33
  ],
33
34
  placementSource: `ui-generator.${normalizedResourceNamespace}.view`,
35
+ requestRecoveryLabel,
34
36
  fallbackLoadError,
35
37
  notFoundMessage,
36
38
  listUrlTemplate,
@@ -1,10 +1,47 @@
1
1
  import { createTransientRetryHttpClient } from "@jskit-ai/http-runtime/client";
2
2
 
3
- const usersWebHttpClient = createTransientRetryHttpClient({
4
- credentials: "include",
5
- csrf: {
6
- sessionPath: "/api/session"
7
- }
8
- });
9
-
10
- export { usersWebHttpClient };
3
+ function normalizeOptions(value = null) {
4
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
5
+ }
6
+
7
+ function createUsersWebHttpClient(options = {}) {
8
+ const source = normalizeOptions(options);
9
+ const sourceCsrf = normalizeOptions(source.csrf);
10
+
11
+ return createTransientRetryHttpClient({
12
+ ...source,
13
+ credentials: source.credentials ?? "include",
14
+ csrf: {
15
+ sessionPath: "/api/session",
16
+ ...sourceCsrf
17
+ }
18
+ });
19
+ }
20
+
21
+ let usersWebHttpClient = createUsersWebHttpClient();
22
+
23
+ function configureUsersWebHttpClient(optionsOrClient = {}) {
24
+ const source = normalizeOptions(optionsOrClient);
25
+ usersWebHttpClient =
26
+ typeof source.request === "function"
27
+ ? source
28
+ : createUsersWebHttpClient(source);
29
+ return usersWebHttpClient;
30
+ }
31
+
32
+ function getUsersWebHttpClient() {
33
+ return usersWebHttpClient;
34
+ }
35
+
36
+ function resetUsersWebHttpClientForTests() {
37
+ usersWebHttpClient = createUsersWebHttpClient();
38
+ return usersWebHttpClient;
39
+ }
40
+
41
+ export {
42
+ configureUsersWebHttpClient,
43
+ createUsersWebHttpClient,
44
+ getUsersWebHttpClient,
45
+ resetUsersWebHttpClientForTests,
46
+ usersWebHttpClient
47
+ };
@@ -9,6 +9,11 @@ import {
9
9
  buildEndpointWriteRequestOptions,
10
10
  useEndpointResource
11
11
  } from "../src/client/composables/runtime/useEndpointResource.js";
12
+ import { useCommand } from "../src/client/composables/useCommand.js";
13
+ import {
14
+ configureUsersWebHttpClient,
15
+ resetUsersWebHttpClientForTests
16
+ } from "../src/client/lib/httpClient.js";
12
17
  import { buildListRequestOptions } from "../src/client/composables/runtime/useListCore.js";
13
18
  import {
14
19
  resolveOperationRealtimeOptions
@@ -249,3 +254,152 @@ test("endpoint resources invalidate their query key from configured realtime eve
249
254
  }
250
255
  ]);
251
256
  });
257
+
258
+ test("endpoint resource reads attach request recovery metadata to query options", async () => {
259
+ const queryClient = new QueryClient();
260
+ const queryKey = ["project-access", "beepollen"];
261
+ const app = createSSRApp({
262
+ setup() {
263
+ useEndpointResource({
264
+ queryKey,
265
+ path: "/api/project-access",
266
+ client: {
267
+ async request() {
268
+ return {};
269
+ }
270
+ },
271
+ requestRecovery: {
272
+ label: "Project access",
273
+ source: "project-access.panel",
274
+ dedupeKey: "project-access"
275
+ }
276
+ });
277
+ return () => h("div");
278
+ }
279
+ });
280
+ app.use(VueQueryPlugin, {
281
+ queryClient
282
+ });
283
+ await renderToString(app);
284
+
285
+ const query = queryClient.getQueryCache().find({
286
+ queryKey
287
+ });
288
+ assert.deepEqual(query?.meta?.jskit, {
289
+ requestRecoveryLabel: "Project access",
290
+ requestRecoverySource: "project-access.panel",
291
+ requestRecoveryDedupeKey: "project-access",
292
+ requestRecoveryMethod: "GET"
293
+ });
294
+ });
295
+
296
+ test("endpoint resources use the configured users-web HTTP client by default", async () => {
297
+ const queryClient = new QueryClient();
298
+ const calls = [];
299
+ let resource = null;
300
+ configureUsersWebHttpClient({
301
+ csrf: {
302
+ enabled: false
303
+ },
304
+ resolveRequestUrl(url) {
305
+ return url.replace(/^\/api\//u, "/api/app/beepollen/");
306
+ },
307
+ fetchImpl: async (url, options) => {
308
+ calls.push([url, options]);
309
+ return {
310
+ ok: true,
311
+ status: 200,
312
+ headers: {
313
+ get(name) {
314
+ return String(name || "").toLowerCase() === "content-type"
315
+ ? "application/json; charset=utf-8"
316
+ : "";
317
+ }
318
+ },
319
+ async json() {
320
+ return {
321
+ ok: true
322
+ };
323
+ }
324
+ };
325
+ }
326
+ });
327
+
328
+ try {
329
+ const app = createSSRApp({
330
+ setup() {
331
+ resource = useEndpointResource({
332
+ queryKey: ["vibe64-sessions"],
333
+ path: "/api/vibe64/sessions"
334
+ });
335
+ return () => h("div");
336
+ }
337
+ });
338
+ app.use(VueQueryPlugin, {
339
+ queryClient
340
+ });
341
+ await renderToString(app);
342
+
343
+ await resource.reload();
344
+
345
+ assert.equal(calls.at(-1)?.[0], "/api/app/beepollen/vibe64/sessions");
346
+ } finally {
347
+ resetUsersWebHttpClientForTests();
348
+ }
349
+ });
350
+
351
+ test("commands use the configured users-web HTTP client by default", async () => {
352
+ const queryClient = new QueryClient();
353
+ const calls = [];
354
+ let command = null;
355
+ configureUsersWebHttpClient({
356
+ csrf: {
357
+ enabled: false
358
+ },
359
+ resolveRequestUrl(url) {
360
+ return url.replace(/^\/api\//u, "/api/app/beepollen/");
361
+ },
362
+ fetchImpl: async (url, options) => {
363
+ calls.push([url, options]);
364
+ return {
365
+ ok: true,
366
+ status: 200,
367
+ headers: {
368
+ get(name) {
369
+ return String(name || "").toLowerCase() === "content-type"
370
+ ? "application/json; charset=utf-8"
371
+ : "";
372
+ }
373
+ },
374
+ async json() {
375
+ return {
376
+ ok: true
377
+ };
378
+ }
379
+ };
380
+ }
381
+ });
382
+
383
+ try {
384
+ const app = createSSRApp({
385
+ setup() {
386
+ command = useCommand({
387
+ access: "always",
388
+ apiSuffix: "/vibe64/sessions"
389
+ });
390
+ return () => h("div");
391
+ }
392
+ });
393
+ app.use(VueQueryPlugin, {
394
+ queryClient
395
+ });
396
+ await renderToString(app);
397
+
398
+ await command.run();
399
+
400
+ assert.equal(calls.at(-1)?.[0], "/api/app/beepollen/vibe64/sessions");
401
+ assert.equal(calls.at(-1)?.[1]?.method, "POST");
402
+ } finally {
403
+ resetUsersWebHttpClientForTests();
404
+ }
405
+ });
@@ -2,8 +2,10 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { ref } from "vue";
4
4
  import {
5
+ buildRequestRecoveryMeta,
5
6
  hasResolvedQueryData,
6
- mergeQueryMeta
7
+ mergeQueryMeta,
8
+ mergeRequestRecoveryQueryMeta
7
9
  } from "../src/client/composables/support/resourceLoadStateHelpers.js";
8
10
 
9
11
  test("hasResolvedQueryData returns true when the query succeeded", () => {
@@ -71,3 +73,117 @@ test("mergeQueryMeta preserves caller metadata while adding JSKIT refresh policy
71
73
  }
72
74
  );
73
75
  });
76
+
77
+ test("request recovery query metadata supports labels and disabling recovery", () => {
78
+ assert.deepEqual(
79
+ buildRequestRecoveryMeta("Project access"),
80
+ {
81
+ jskit: {
82
+ requestRecoveryLabel: "Project access"
83
+ }
84
+ }
85
+ );
86
+
87
+ assert.deepEqual(
88
+ buildRequestRecoveryMeta(false),
89
+ {
90
+ jskit: {
91
+ requestRecovery: false
92
+ }
93
+ }
94
+ );
95
+ });
96
+
97
+ test("mergeRequestRecoveryQueryMeta preserves caller metadata while adding recovery hints", () => {
98
+ assert.deepEqual(
99
+ mergeRequestRecoveryQueryMeta(
100
+ {
101
+ staleTime: 5000,
102
+ meta: {
103
+ owner: "project-access",
104
+ jskit: {
105
+ refreshOnPull: true
106
+ }
107
+ }
108
+ },
109
+ {
110
+ label: "Project access",
111
+ source: "app.project-access",
112
+ dedupeKey: "project-access",
113
+ method: "get",
114
+ dedupeWindowMs: 100
115
+ }
116
+ ),
117
+ {
118
+ staleTime: 5000,
119
+ meta: {
120
+ owner: "project-access",
121
+ jskit: {
122
+ refreshOnPull: true,
123
+ requestRecoveryLabel: "Project access",
124
+ requestRecoverySource: "app.project-access",
125
+ requestRecoveryDedupeKey: "project-access",
126
+ requestRecoveryMethod: "GET",
127
+ requestRecoveryDedupeWindowMs: 100
128
+ }
129
+ }
130
+ }
131
+ );
132
+ });
133
+
134
+ test("mergeRequestRecoveryQueryMeta does not overwrite explicit query recovery labels with defaults", () => {
135
+ assert.deepEqual(
136
+ mergeRequestRecoveryQueryMeta(
137
+ {
138
+ meta: {
139
+ jskit: {
140
+ requestRecoveryLabel: "Explicit label",
141
+ requestRecoveryMethod: "HEAD",
142
+ requestRecoveryDedupeWindowMs: 0
143
+ }
144
+ }
145
+ },
146
+ null,
147
+ {
148
+ label: "Default label",
149
+ method: "GET",
150
+ dedupeWindowMs: 5000
151
+ }
152
+ ),
153
+ {
154
+ meta: {
155
+ jskit: {
156
+ requestRecoveryLabel: "Explicit label",
157
+ requestRecoveryMethod: "HEAD",
158
+ requestRecoveryDedupeWindowMs: 0
159
+ }
160
+ }
161
+ }
162
+ );
163
+ });
164
+
165
+ test("mergeRequestRecoveryQueryMeta preserves disabled recovery metadata", () => {
166
+ assert.deepEqual(
167
+ mergeRequestRecoveryQueryMeta(
168
+ {
169
+ meta: {
170
+ jskit: {
171
+ requestRecovery: false
172
+ }
173
+ }
174
+ },
175
+ null,
176
+ {
177
+ label: "Default label",
178
+ method: "GET"
179
+ }
180
+ ),
181
+ {
182
+ meta: {
183
+ jskit: {
184
+ requestRecovery: false
185
+ }
186
+ }
187
+ }
188
+ );
189
+ });