@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.
- package/package.descriptor.mjs +7 -7
- package/package.json +7 -7
- package/src/client/composables/records/useAddEdit.js +6 -0
- package/src/client/composables/records/useList.js +6 -0
- package/src/client/composables/records/useView.js +6 -0
- package/src/client/composables/runtime/useEndpointResource.js +24 -13
- package/src/client/composables/runtime/useListCore.js +16 -5
- package/src/client/composables/support/resourceLoadStateHelpers.js +188 -1
- package/src/client/composables/useCommand.js +2 -0
- package/src/client/composables/useCrudListScreen.js +2 -0
- package/src/client/composables/useCrudViewScreen.js +2 -0
- package/src/client/lib/httpClient.js +45 -8
- package/test/requestTransportOptions.test.js +154 -0
- package/test/resourceLoadStateHelpers.test.js +117 -1
package/package.descriptor.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
282
|
-
"@jskit-ai/realtime": "0.1.
|
|
283
|
-
"@jskit-ai/kernel": "0.1.
|
|
284
|
-
"@jskit-ai/shell-web": "0.1.
|
|
285
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
286
|
-
"@jskit-ai/users-core": "0.1.
|
|
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.
|
|
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.
|
|
48
|
-
"@jskit-ai/kernel": "0.1.
|
|
49
|
-
"@jskit-ai/realtime": "0.1.
|
|
50
|
-
"@jskit-ai/shell-web": "0.1.
|
|
51
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
52
|
-
"@jskit-ai/users-core": "0.1.
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
...(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
});
|