@jskit-ai/http-web 0.1.1
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/fixtures/crud-list-date-filters/App.vue +107 -0
- package/fixtures/crud-list-date-filters/index.html +12 -0
- package/fixtures/crud-list-date-filters/main.js +24 -0
- package/fixtures/crud-list-date-filters/vite.config.mjs +18 -0
- package/package.json +150 -0
- package/src/client/bulkActions.js +47 -0
- package/src/client/components/CrudAddEditScreen.vue +186 -0
- package/src/client/components/CrudDeleteAction.vue +93 -0
- package/src/client/components/CrudListBulkActionSurface.vue +126 -0
- package/src/client/components/CrudListDateFilterControl.vue +188 -0
- package/src/client/components/CrudListFilterSurface.vue +388 -0
- package/src/client/components/CrudListRecordActionMenu.vue +120 -0
- package/src/client/components/CrudListScreen.vue +501 -0
- package/src/client/components/CrudViewScreen.vue +197 -0
- package/src/client/composables/crud/crudBindingSupport.js +75 -0
- package/src/client/composables/crud/crudHttpClientSupport.js +57 -0
- package/src/client/composables/crud/crudJsonApiTransportSupport.js +145 -0
- package/src/client/composables/crud/crudLookupFieldLabelSupport.js +151 -0
- package/src/client/composables/crud/crudLookupFieldRuntime.js +251 -0
- package/src/client/composables/crud/crudSchemaFormHelpers.js +464 -0
- package/src/client/composables/internal/crudListFilterLookupSupport.js +161 -0
- package/src/client/composables/internal/crudListParentTitleSupport.js +171 -0
- package/src/client/composables/internal/useOperationScope.js +144 -0
- package/src/client/composables/records/useAddEdit.js +224 -0
- package/src/client/composables/records/useCrudAddEdit.js +227 -0
- package/src/client/composables/records/useCrudList.js +101 -0
- package/src/client/composables/records/useCrudView.js +47 -0
- package/src/client/composables/records/useList.js +500 -0
- package/src/client/composables/records/useView.js +164 -0
- package/src/client/composables/runtime/addEditUiRuntime.js +130 -0
- package/src/client/composables/runtime/listUiRuntime.js +123 -0
- package/src/client/composables/runtime/modelStateHelpers.js +90 -0
- package/src/client/composables/runtime/operationAdapters.js +34 -0
- package/src/client/composables/runtime/operationUiHelpers.js +125 -0
- package/src/client/composables/runtime/operationValidationHelpers.js +51 -0
- package/src/client/composables/runtime/useAddEditCore.js +112 -0
- package/src/client/composables/runtime/useCommandCore.js +123 -0
- package/src/client/composables/runtime/useEndpointResource.js +199 -0
- package/src/client/composables/runtime/useFieldErrorBag.js +61 -0
- package/src/client/composables/runtime/useListCore.js +112 -0
- package/src/client/composables/runtime/useUiFeedback.js +98 -0
- package/src/client/composables/runtime/useViewCore.js +93 -0
- package/src/client/composables/runtime/viewUiRuntime.js +96 -0
- package/src/client/composables/support/errorMessageHelpers.js +66 -0
- package/src/client/composables/support/listQueryParamSupport.js +473 -0
- package/src/client/composables/support/listSearchSupport.js +70 -0
- package/src/client/composables/support/refValueHelpers.js +19 -0
- package/src/client/composables/support/requestQueryRuntimeSupport.js +141 -0
- package/src/client/composables/support/resourceLoadStateHelpers.js +208 -0
- package/src/client/composables/support/routeTemplateHelpers.js +285 -0
- package/src/client/composables/support/scopeHelpers.js +145 -0
- package/src/client/composables/useAccess.js +109 -0
- package/src/client/composables/useCommand.js +118 -0
- package/src/client/composables/useCrudAddEditScreen.js +88 -0
- package/src/client/composables/useCrudDeleteAction.js +160 -0
- package/src/client/composables/useCrudListBulkActions.js +147 -0
- package/src/client/composables/useCrudListFilterLookups.js +146 -0
- package/src/client/composables/useCrudListFilters.js +339 -0
- package/src/client/composables/useCrudListParentTitle.js +151 -0
- package/src/client/composables/useCrudListRowActions.js +144 -0
- package/src/client/composables/useCrudListScreen.js +237 -0
- package/src/client/composables/useCrudViewScreen.js +77 -0
- package/src/client/composables/usePagedCollection.js +181 -0
- package/src/client/composables/useRealtimeQueryInvalidation.js +156 -0
- package/src/client/composables/useScopeRuntime.js +135 -0
- package/src/client/filters.js +15 -0
- package/src/client/index.js +24 -0
- package/src/client/lib/httpClient.js +47 -0
- package/src/client/lib/permissions.js +34 -0
- package/src/client/rowActions.js +47 -0
- package/src/client/support/contractGuards.js +34 -0
- package/src/client/support/crudListDateFilterSupport.js +91 -0
- package/test/addEditUiRuntime.test.js +110 -0
- package/test/crudBindingSupport.test.js +110 -0
- package/test/crudHttpClientSupport.test.js +77 -0
- package/test/crudJsonApiTransportSupport.test.js +179 -0
- package/test/crudListBulkActionSurface.test.js +27 -0
- package/test/crudListDateFilterSupport.test.js +92 -0
- package/test/crudListDateFilterSurface.browser.test.js +182 -0
- package/test/crudListFilterSurface.test.js +90 -0
- package/test/crudLookupFieldRuntime.test.js +264 -0
- package/test/crudScreenComponents.test.js +110 -0
- package/test/errorIntentContract.test.js +31 -0
- package/test/errorMessageHelpers.test.js +28 -0
- package/test/exportsContract.test.js +68 -0
- package/test/listQueryParamSupport.test.js +223 -0
- package/test/listUiRuntime.test.js +102 -0
- package/test/operationValidationHelpers.test.js +64 -0
- package/test/packageBoundary.test.js +31 -0
- package/test/permissions.test.js +35 -0
- package/test/refValueHelpers.test.js +14 -0
- package/test/requestTransportOptions.test.js +457 -0
- package/test/resourceLoadStateHelpers.test.js +189 -0
- package/test/routeTemplateHelpers.test.js +85 -0
- package/test/scopeHelpers.test.js +57 -0
- package/test/useAddEditCore.test.js +124 -0
- package/test/useAddEditRequestQueryParams.test.js +190 -0
- package/test/useCrudAddEdit.test.js +415 -0
- package/test/useCrudDeleteAction.test.js +139 -0
- package/test/useCrudListBulkActions.test.js +65 -0
- package/test/useCrudListFilterLookups.test.js +114 -0
- package/test/useCrudListFilters.test.js +293 -0
- package/test/useCrudListParentTitle.test.js +262 -0
- package/test/useCrudListRowActions.test.js +77 -0
- package/test/useListRouteInitialization.test.js +337 -0
- package/test/useListSearchSupport.test.js +61 -0
- package/test/usePagedCollection.test.js +53 -0
- package/test/useViewRequestQueryParams.test.js +20 -0
- package/test/viewCoreLoading.test.js +44 -0
- package/test/viewUiRuntime.test.js +95 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
|
|
4
|
+
import { createSSRApp, h } from "vue";
|
|
5
|
+
import { renderToString } from "vue/server-renderer";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildEndpointReadRequestOptions,
|
|
9
|
+
buildEndpointWriteRequestOptions,
|
|
10
|
+
useEndpointResource
|
|
11
|
+
} from "../src/client/composables/runtime/useEndpointResource.js";
|
|
12
|
+
import { useCommand } from "../src/client/composables/useCommand.js";
|
|
13
|
+
import {
|
|
14
|
+
configureHttpWebClient,
|
|
15
|
+
resetHttpWebClientForTests
|
|
16
|
+
} from "../src/client/lib/httpClient.js";
|
|
17
|
+
import { buildListRequestOptions } from "../src/client/composables/runtime/useListCore.js";
|
|
18
|
+
import {
|
|
19
|
+
resolveOperationRealtimeOptions
|
|
20
|
+
} from "../src/client/composables/useRealtimeQueryInvalidation.js";
|
|
21
|
+
|
|
22
|
+
test("endpoint read request options include transport only when provided", () => {
|
|
23
|
+
assert.deepEqual(
|
|
24
|
+
buildEndpointReadRequestOptions({
|
|
25
|
+
method: "get"
|
|
26
|
+
}),
|
|
27
|
+
{
|
|
28
|
+
method: "GET"
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
assert.deepEqual(
|
|
33
|
+
buildEndpointReadRequestOptions({
|
|
34
|
+
method: "get",
|
|
35
|
+
transport: {
|
|
36
|
+
kind: "jsonapi-resource",
|
|
37
|
+
responseType: "user-settings",
|
|
38
|
+
responseKind: "record"
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
{
|
|
42
|
+
method: "GET",
|
|
43
|
+
transport: {
|
|
44
|
+
kind: "jsonapi-resource",
|
|
45
|
+
responseType: "user-settings",
|
|
46
|
+
responseKind: "record"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("endpoint write request options preserve body/options and append transport", () => {
|
|
53
|
+
assert.deepEqual(
|
|
54
|
+
buildEndpointWriteRequestOptions({
|
|
55
|
+
method: "patch",
|
|
56
|
+
body: {
|
|
57
|
+
theme: "dark"
|
|
58
|
+
},
|
|
59
|
+
options: {
|
|
60
|
+
headers: {
|
|
61
|
+
"x-demo": "1"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
transport: {
|
|
65
|
+
kind: "jsonapi-resource",
|
|
66
|
+
requestType: "user-preferences",
|
|
67
|
+
responseType: "user-settings",
|
|
68
|
+
responseKind: "record"
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
{
|
|
72
|
+
method: "PATCH",
|
|
73
|
+
headers: {
|
|
74
|
+
"x-demo": "1"
|
|
75
|
+
},
|
|
76
|
+
body: {
|
|
77
|
+
theme: "dark"
|
|
78
|
+
},
|
|
79
|
+
transport: {
|
|
80
|
+
kind: "jsonapi-resource",
|
|
81
|
+
requestType: "user-preferences",
|
|
82
|
+
responseType: "user-settings",
|
|
83
|
+
responseKind: "record"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("list request options stay GET and carry transport when provided", () => {
|
|
90
|
+
assert.deepEqual(
|
|
91
|
+
buildListRequestOptions({
|
|
92
|
+
requestOptions: {
|
|
93
|
+
headers: {
|
|
94
|
+
"x-demo": "1"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
pageParam: "cursor_2",
|
|
98
|
+
transport: {
|
|
99
|
+
kind: "jsonapi-resource",
|
|
100
|
+
responseType: "contacts",
|
|
101
|
+
responseKind: "collection"
|
|
102
|
+
}
|
|
103
|
+
}),
|
|
104
|
+
{
|
|
105
|
+
method: "GET",
|
|
106
|
+
headers: {
|
|
107
|
+
"x-demo": "1"
|
|
108
|
+
},
|
|
109
|
+
query: {
|
|
110
|
+
limit: 20,
|
|
111
|
+
cursor: "cursor_2"
|
|
112
|
+
},
|
|
113
|
+
transport: {
|
|
114
|
+
kind: "jsonapi-resource",
|
|
115
|
+
responseType: "contacts",
|
|
116
|
+
responseKind: "collection"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("list request options preserve explicit limit values", () => {
|
|
123
|
+
assert.deepEqual(
|
|
124
|
+
buildListRequestOptions({
|
|
125
|
+
requestOptions: {
|
|
126
|
+
query: {
|
|
127
|
+
limit: 50
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
{
|
|
132
|
+
method: "GET",
|
|
133
|
+
query: {
|
|
134
|
+
limit: 50
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
assert.deepEqual(
|
|
140
|
+
buildListRequestOptions({
|
|
141
|
+
requestOptions: {
|
|
142
|
+
query: {
|
|
143
|
+
"page[limit]": "75"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}),
|
|
147
|
+
{
|
|
148
|
+
method: "GET",
|
|
149
|
+
query: {
|
|
150
|
+
"page[limit]": "75"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("operation realtime options use fallback events unless explicitly overridden", () => {
|
|
157
|
+
const fallbackRealtime = {
|
|
158
|
+
events: ["contacts.record.changed"]
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
assert.deepEqual(
|
|
162
|
+
resolveOperationRealtimeOptions({
|
|
163
|
+
fallbackRealtime
|
|
164
|
+
}),
|
|
165
|
+
{
|
|
166
|
+
events: ["contacts.record.changed"]
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
assert.deepEqual(
|
|
171
|
+
resolveOperationRealtimeOptions({
|
|
172
|
+
realtime: {
|
|
173
|
+
enabled: false
|
|
174
|
+
},
|
|
175
|
+
fallbackRealtime
|
|
176
|
+
}),
|
|
177
|
+
{
|
|
178
|
+
events: ["contacts.record.changed"],
|
|
179
|
+
enabled: false
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
assert.deepEqual(
|
|
184
|
+
resolveOperationRealtimeOptions({
|
|
185
|
+
realtime: {
|
|
186
|
+
event: "contacts.custom.changed"
|
|
187
|
+
},
|
|
188
|
+
fallbackRealtime
|
|
189
|
+
}),
|
|
190
|
+
{
|
|
191
|
+
event: "contacts.custom.changed"
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
assert.equal(
|
|
196
|
+
resolveOperationRealtimeOptions({
|
|
197
|
+
realtime: false,
|
|
198
|
+
fallbackRealtime
|
|
199
|
+
}),
|
|
200
|
+
null
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("endpoint resources invalidate their query key from configured realtime events", async () => {
|
|
205
|
+
const queryClient = new QueryClient();
|
|
206
|
+
const invalidations = [];
|
|
207
|
+
const socketHandlers = new Map();
|
|
208
|
+
const socket = {
|
|
209
|
+
on(eventName, handler) {
|
|
210
|
+
socketHandlers.set(eventName, handler);
|
|
211
|
+
},
|
|
212
|
+
off(eventName, handler) {
|
|
213
|
+
if (socketHandlers.get(eventName) === handler) {
|
|
214
|
+
socketHandlers.delete(eventName);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
queryClient.invalidateQueries = async (options = {}) => {
|
|
219
|
+
invalidations.push(options);
|
|
220
|
+
};
|
|
221
|
+
const app = createSSRApp({
|
|
222
|
+
setup() {
|
|
223
|
+
useEndpointResource({
|
|
224
|
+
queryKey: ["today-workout-detail", "/api/today/workouts/2026-05-06"],
|
|
225
|
+
path: "/api/today/workouts/2026-05-06",
|
|
226
|
+
client: {
|
|
227
|
+
async request() {
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
realtime: {
|
|
232
|
+
event: "workout_set_logs.record.changed"
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return () => h("div");
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
app.use(VueQueryPlugin, {
|
|
239
|
+
queryClient
|
|
240
|
+
});
|
|
241
|
+
app.provide("jskit.realtime.runtime.client.socket", socket);
|
|
242
|
+
await renderToString(app);
|
|
243
|
+
|
|
244
|
+
const handler = socketHandlers.get("workout_set_logs.record.changed");
|
|
245
|
+
assert.equal(typeof handler, "function");
|
|
246
|
+
handler({
|
|
247
|
+
entityId: "42"
|
|
248
|
+
});
|
|
249
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
250
|
+
|
|
251
|
+
assert.deepEqual(invalidations, [
|
|
252
|
+
{
|
|
253
|
+
queryKey: ["today-workout-detail", "/api/today/workouts/2026-05-06"]
|
|
254
|
+
}
|
|
255
|
+
]);
|
|
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 http-web HTTP client by default", async () => {
|
|
297
|
+
const queryClient = new QueryClient();
|
|
298
|
+
const calls = [];
|
|
299
|
+
let resource = null;
|
|
300
|
+
configureHttpWebClient({
|
|
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
|
+
resetHttpWebClientForTests();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("commands use the configured http-web HTTP client by default", async () => {
|
|
352
|
+
const queryClient = new QueryClient();
|
|
353
|
+
const calls = [];
|
|
354
|
+
let command = null;
|
|
355
|
+
configureHttpWebClient({
|
|
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
|
+
resetHttpWebClientForTests();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("commands use public CRUD resource access without a CSRF session preflight", async () => {
|
|
408
|
+
const queryClient = new QueryClient();
|
|
409
|
+
const calls = [];
|
|
410
|
+
let command = null;
|
|
411
|
+
configureHttpWebClient({
|
|
412
|
+
fetchImpl: async (url, options) => {
|
|
413
|
+
calls.push([url, options]);
|
|
414
|
+
return {
|
|
415
|
+
ok: true,
|
|
416
|
+
status: 204,
|
|
417
|
+
headers: {
|
|
418
|
+
get() {
|
|
419
|
+
return "";
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
async json() {
|
|
423
|
+
return {};
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const app = createSSRApp({
|
|
431
|
+
setup() {
|
|
432
|
+
command = useCommand({
|
|
433
|
+
access: "always",
|
|
434
|
+
apiSuffix: "/books/1",
|
|
435
|
+
writeMethod: "DELETE",
|
|
436
|
+
resource: {
|
|
437
|
+
apiAccess: "public"
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return () => h("div");
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
app.use(VueQueryPlugin, {
|
|
444
|
+
queryClient
|
|
445
|
+
});
|
|
446
|
+
await renderToString(app);
|
|
447
|
+
|
|
448
|
+
await command.run();
|
|
449
|
+
|
|
450
|
+
assert.equal(calls.length, 1);
|
|
451
|
+
assert.equal(calls[0][0], "/api/books/1");
|
|
452
|
+
assert.equal(calls[0][1].method, "DELETE");
|
|
453
|
+
assert.equal(Object.hasOwn(calls[0][1], "csrf"), false);
|
|
454
|
+
} finally {
|
|
455
|
+
resetHttpWebClientForTests();
|
|
456
|
+
}
|
|
457
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import {
|
|
5
|
+
buildRequestRecoveryMeta,
|
|
6
|
+
hasResolvedQueryData,
|
|
7
|
+
mergeQueryMeta,
|
|
8
|
+
mergeRequestRecoveryQueryMeta
|
|
9
|
+
} from "../src/client/composables/support/resourceLoadStateHelpers.js";
|
|
10
|
+
|
|
11
|
+
test("hasResolvedQueryData returns true when the query succeeded", () => {
|
|
12
|
+
const query = {
|
|
13
|
+
isSuccess: ref(true)
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
assert.equal(hasResolvedQueryData({
|
|
17
|
+
query,
|
|
18
|
+
data: ref(null)
|
|
19
|
+
}), true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("hasResolvedQueryData returns true when data payload is available", () => {
|
|
23
|
+
const query = {
|
|
24
|
+
isSuccess: ref(false)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
assert.equal(hasResolvedQueryData({
|
|
28
|
+
query,
|
|
29
|
+
data: ref({
|
|
30
|
+
id: 2971
|
|
31
|
+
})
|
|
32
|
+
}), true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("hasResolvedQueryData returns false when query and payload are unresolved", () => {
|
|
36
|
+
const query = {
|
|
37
|
+
isSuccess: ref(false)
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
assert.equal(hasResolvedQueryData({
|
|
41
|
+
query,
|
|
42
|
+
data: ref(null)
|
|
43
|
+
}), false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("mergeQueryMeta preserves caller metadata while adding JSKIT refresh policy", () => {
|
|
47
|
+
assert.deepEqual(
|
|
48
|
+
mergeQueryMeta(
|
|
49
|
+
{
|
|
50
|
+
staleTime: 1000,
|
|
51
|
+
meta: {
|
|
52
|
+
owner: "contacts",
|
|
53
|
+
jskit: {
|
|
54
|
+
feature: "list"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
jskit: {
|
|
60
|
+
refreshOnPull: true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
),
|
|
64
|
+
{
|
|
65
|
+
staleTime: 1000,
|
|
66
|
+
meta: {
|
|
67
|
+
owner: "contacts",
|
|
68
|
+
jskit: {
|
|
69
|
+
feature: "list",
|
|
70
|
+
refreshOnPull: true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
);
|
|
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
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
extractRouteParamNames,
|
|
5
|
+
resolveScopedRoutePathname,
|
|
6
|
+
resolveRouteParamNamesInOrder
|
|
7
|
+
} from "../src/client/composables/support/routeTemplateHelpers.js";
|
|
8
|
+
|
|
9
|
+
test("extractRouteParamNames reads dynamic params from route templates", () => {
|
|
10
|
+
assert.deepEqual(
|
|
11
|
+
extractRouteParamNames("/w/:workspaceSlug/admin/contacts/:contactId/addresses/:addressId"),
|
|
12
|
+
["workspaceSlug", "contactId", "addressId"]
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("resolveRouteParamNamesInOrder prefers matched route templates", () => {
|
|
17
|
+
const route = {
|
|
18
|
+
matched: [
|
|
19
|
+
{ path: "/w/:workspaceSlug/admin" },
|
|
20
|
+
{ path: "/contacts/:contactId/addresses/:addressId" }
|
|
21
|
+
],
|
|
22
|
+
params: {
|
|
23
|
+
workspaceSlug: "acme",
|
|
24
|
+
contactId: "7",
|
|
25
|
+
addressId: "42"
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
assert.deepEqual(resolveRouteParamNamesInOrder(route), ["workspaceSlug", "contactId", "addressId"]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("resolveScopedRoutePathname supports at/before/after anchors", () => {
|
|
33
|
+
const currentPathname = "/w/dogandgroom/admin/contacts/541841/pets/715528/edit/advanced";
|
|
34
|
+
const params = {
|
|
35
|
+
workspaceSlug: "dogandgroom",
|
|
36
|
+
contactId: "541841",
|
|
37
|
+
petId: "715528"
|
|
38
|
+
};
|
|
39
|
+
const orderedParamNames = ["workspaceSlug", "contactId", "petId"];
|
|
40
|
+
|
|
41
|
+
assert.equal(
|
|
42
|
+
resolveScopedRoutePathname({
|
|
43
|
+
currentPathname,
|
|
44
|
+
params,
|
|
45
|
+
orderedParamNames,
|
|
46
|
+
anchorParamName: "contactId",
|
|
47
|
+
anchorMode: "at"
|
|
48
|
+
}),
|
|
49
|
+
"/w/dogandgroom/admin/contacts/541841"
|
|
50
|
+
);
|
|
51
|
+
assert.equal(
|
|
52
|
+
resolveScopedRoutePathname({
|
|
53
|
+
currentPathname,
|
|
54
|
+
params,
|
|
55
|
+
orderedParamNames,
|
|
56
|
+
anchorParamName: "petId",
|
|
57
|
+
anchorMode: "before"
|
|
58
|
+
}),
|
|
59
|
+
"/w/dogandgroom/admin/contacts/541841/pets"
|
|
60
|
+
);
|
|
61
|
+
assert.equal(
|
|
62
|
+
resolveScopedRoutePathname({
|
|
63
|
+
currentPathname,
|
|
64
|
+
params,
|
|
65
|
+
orderedParamNames,
|
|
66
|
+
anchorParamName: "petId",
|
|
67
|
+
anchorMode: "after"
|
|
68
|
+
}),
|
|
69
|
+
"/w/dogandgroom/admin/contacts/541841/pets/715528/edit"
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("resolveScopedRoutePathname falls back to direct value matching", () => {
|
|
74
|
+
assert.equal(
|
|
75
|
+
resolveScopedRoutePathname({
|
|
76
|
+
currentPathname: "/contacts/abc%20123/notes",
|
|
77
|
+
params: {},
|
|
78
|
+
orderedParamNames: [],
|
|
79
|
+
anchorParamName: "contactId",
|
|
80
|
+
anchorParamValue: "abc 123",
|
|
81
|
+
anchorMode: "at"
|
|
82
|
+
}),
|
|
83
|
+
"/contacts/abc%20123"
|
|
84
|
+
);
|
|
85
|
+
});
|