@jskit-ai/users-web 0.1.105 → 0.1.106

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.106",
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.90",
282
+ "@jskit-ai/realtime": "0.1.90",
283
+ "@jskit-ai/kernel": "0.1.91",
284
+ "@jskit-ai/shell-web": "0.1.90",
285
+ "@jskit-ai/uploads-image-web": "0.1.69",
286
+ "@jskit-ai/users-core": "0.1.101"
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.106",
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.90",
48
+ "@jskit-ai/kernel": "0.1.91",
49
+ "@jskit-ai/realtime": "0.1.90",
50
+ "@jskit-ai/shell-web": "0.1.90",
51
+ "@jskit-ai/uploads-image-web": "0.1.69",
52
+ "@jskit-ai/users-core": "0.1.101"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@tanstack/vue-query": "^5.90.5",
@@ -30,6 +30,8 @@ function useAddEdit({
30
30
  readEnabled = true,
31
31
  writeMethod = "PATCH",
32
32
  transport = null,
33
+ requestRecovery = null,
34
+ requestRecoveryLabel = "Resource",
33
35
  placementSource = "users-web.add-edit",
34
36
  fallbackLoadError = "Unable to load resource.",
35
37
  fallbackSaveError = "Unable to save resource.",
@@ -121,6 +123,8 @@ function useAddEdit({
121
123
  writeMethod,
122
124
  readQuery: requestQueryRuntime.requestQuery,
123
125
  transport,
126
+ requestRecovery,
127
+ requestRecoveryLabel,
124
128
  fallbackLoadError,
125
129
  fallbackSaveError: String(fallbackSaveError || effectiveMessages.saveError || "Unable to save resource.")
126
130
  });
@@ -46,6 +46,8 @@ function useList({
46
46
  transport = null,
47
47
  requestOptions,
48
48
  queryOptions,
49
+ requestRecovery,
50
+ requestRecoveryLabel = "List",
49
51
  realtime = null,
50
52
  adapter = null,
51
53
  recordIdParam = "recordId",
@@ -228,6 +230,8 @@ function useList({
228
230
  selectItems,
229
231
  requestOptions: listRequestOptions,
230
232
  queryOptions,
233
+ requestRecovery,
234
+ requestRecoveryLabel,
231
235
  fallbackLoadError
232
236
  });
233
237
  const routeSyncHydrated = ref(routeSyncConfig.enabled !== true);
@@ -21,6 +21,8 @@ function useView({
21
21
  readMethod = "GET",
22
22
  readEnabled = true,
23
23
  transport = null,
24
+ requestRecovery = null,
25
+ requestRecoveryLabel = "Resource",
24
26
  placementSource = "users-web.view",
25
27
  fallbackLoadError = "Unable to load resource.",
26
28
  notFoundStatuses = [404],
@@ -104,6 +106,8 @@ function useView({
104
106
  readMethod,
105
107
  readQuery: requestQueryRuntime.requestQuery,
106
108
  transport,
109
+ requestRecovery,
110
+ requestRecoveryLabel,
107
111
  refreshOnPull: true,
108
112
  fallbackLoadError
109
113
  });
@@ -7,7 +7,8 @@ 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({
@@ -75,6 +76,8 @@ 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
  } = {}) {
@@ -102,13 +105,20 @@ function useEndpointResource({
102
105
  });
103
106
  },
104
107
  enabled: queryEnabled,
105
- ...(refreshOnPull
106
- ? mergeQueryMeta(asPlainObject(queryOptions), {
107
- jskit: {
108
- refreshOnPull: true
109
- }
110
- })
111
- : asPlainObject(queryOptions))
108
+ ...mergeRequestRecoveryQueryMeta(
109
+ refreshOnPull
110
+ ? mergeQueryMeta(asPlainObject(queryOptions), {
111
+ jskit: {
112
+ refreshOnPull: true
113
+ }
114
+ })
115
+ : asPlainObject(queryOptions),
116
+ requestRecovery,
117
+ {
118
+ label: requestRecoveryLabel,
119
+ method: readMethod
120
+ }
121
+ )
112
122
  });
113
123
  const realtimeBinding = useOperationRealtime({
114
124
  realtime,
@@ -3,6 +3,7 @@ import { usersWebHttpClient } 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,6 +48,10 @@ 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 = "",
@@ -58,6 +63,8 @@ function useListCore({
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
70
  if (!client || typeof client.request !== "function") {
@@ -88,7 +95,10 @@ function useListCore({
88
95
  },
89
96
  getNextPageParam,
90
97
  selectItems,
91
- queryOptions,
98
+ queryOptions: mergeRequestRecoveryQueryMeta(queryOptions, requestRecovery, {
99
+ label: requestRecoveryLabel,
100
+ method: resolveListRequestMethod(requestOptions)
101
+ }),
92
102
  fallbackLoadError
93
103
  });
94
104
 
@@ -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
  };
@@ -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,
@@ -249,3 +249,41 @@ test("endpoint resources invalidate their query key from configured realtime eve
249
249
  }
250
250
  ]);
251
251
  });
252
+
253
+ test("endpoint resource reads attach request recovery metadata to query options", async () => {
254
+ const queryClient = new QueryClient();
255
+ const queryKey = ["project-access", "beepollen"];
256
+ const app = createSSRApp({
257
+ setup() {
258
+ useEndpointResource({
259
+ queryKey,
260
+ path: "/api/project-access",
261
+ client: {
262
+ async request() {
263
+ return {};
264
+ }
265
+ },
266
+ requestRecovery: {
267
+ label: "Project access",
268
+ source: "project-access.panel",
269
+ dedupeKey: "project-access"
270
+ }
271
+ });
272
+ return () => h("div");
273
+ }
274
+ });
275
+ app.use(VueQueryPlugin, {
276
+ queryClient
277
+ });
278
+ await renderToString(app);
279
+
280
+ const query = queryClient.getQueryCache().find({
281
+ queryKey
282
+ });
283
+ assert.deepEqual(query?.meta?.jskit, {
284
+ requestRecoveryLabel: "Project access",
285
+ requestRecoverySource: "project-access.panel",
286
+ requestRecoveryDedupeKey: "project-access",
287
+ requestRecoveryMethod: "GET"
288
+ });
289
+ });
@@ -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
+ });