Eric Bower
·
2025-06-06
mdw.test.ts
1import {
2 createApi,
3 createKey,
4 mdw,
5 put,
6 safe,
7 takeEvery,
8 takeLatest,
9 waitFor,
10} from "../index.js";
11import type { ApiCtx, Next, ThunkCtx } from "../index.js";
12import {
13 createSchema,
14 createStore,
15 slice,
16 updateStore,
17 waitForLoader,
18} from "../store/index.js";
19import { assertLike, expect, test } from "../test.js";
20
21interface User {
22 id: string;
23 name: string;
24 email: string;
25}
26
27const emptyUser: User = { id: "", name: "", email: "" };
28const mockUser: User = { id: "1", name: "test", email: "test@test.com" };
29const mockUser2: User = { id: "2", name: "two", email: "two@test.com" };
30
31// deno-lint-ignore no-explicit-any
32const jsonBlob = (data: any) => {
33 return JSON.stringify(data);
34};
35
36const testStore = () => {
37 const [schema, initialState] = createSchema({
38 users: slice.table<User>({ empty: emptyUser }),
39 loaders: slice.loaders(),
40 cache: slice.table({ empty: {} }),
41 });
42 const store = createStore({ initialState });
43 return { schema, store };
44};
45
46test("basic", () => {
47 const { store, schema } = testStore();
48 const query = createApi<ApiCtx>();
49 query.use(mdw.api({ schema }));
50 query.use(query.routes());
51 query.use(function* fetchApi(ctx, next) {
52 if (`${ctx.req().url}`.startsWith("/users/")) {
53 ctx.json = { ok: true, value: mockUser2 };
54 yield* next();
55 return;
56 }
57 const data = {
58 users: [mockUser],
59 };
60 ctx.json = { ok: true, value: data };
61 yield* next();
62 });
63
64 const fetchUsers = query.create(
65 "/users",
66 { supervisor: takeEvery },
67 function* processUsers(ctx: ApiCtx<unknown, { users: User[] }>, next) {
68 yield* next();
69 if (!ctx.json.ok) return;
70 const { users } = ctx.json.value;
71
72 yield* updateStore((state) => {
73 users.forEach((u) => {
74 state.users[u.id] = u;
75 });
76 });
77 },
78 );
79
80 const fetchUser = query.create<{ id: string }>(
81 "/users/:id",
82 {
83 supervisor: takeLatest,
84 },
85 function* processUser(ctx, next) {
86 ctx.request = ctx.req({ method: "POST" });
87 yield* next();
88 if (!ctx.json.ok) return;
89 const curUser = ctx.json.value;
90 yield* updateStore((state) => {
91 state.users[curUser.id] = curUser;
92 });
93 },
94 );
95
96 store.run(query.bootup);
97
98 store.dispatch(fetchUsers());
99 expect(store.getState().users).toEqual({ [mockUser.id]: mockUser });
100 store.dispatch(fetchUser({ id: "2" }));
101 expect(store.getState().users).toEqual({
102 [mockUser.id]: mockUser,
103 [mockUser2.id]: mockUser2,
104 });
105});
106
107test("with loader", () => {
108 const { schema, store } = testStore();
109 const api = createApi<ApiCtx>();
110 api.use(mdw.api({ schema }));
111 api.use(api.routes());
112 api.use(function* fetchApi(ctx, next) {
113 ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
114 const data = { users: [mockUser] };
115 ctx.json = { ok: true, value: data };
116 yield* next();
117 });
118
119 const fetchUsers = api.create(
120 "/users",
121 { supervisor: takeEvery },
122 function* processUsers(ctx: ApiCtx<unknown, { users: User[] }>, next) {
123 yield* next();
124 if (!ctx.json.ok) return;
125
126 const { value } = ctx.json;
127
128 yield* updateStore((state) => {
129 value.users.forEach((u) => {
130 state.users[u.id] = u;
131 });
132 });
133 },
134 );
135
136 store.run(api.bootup);
137
138 store.dispatch(fetchUsers());
139 assertLike(store.getState(), {
140 users: { [mockUser.id]: mockUser },
141 loaders: {
142 "/users": {
143 status: "success",
144 },
145 },
146 });
147});
148
149test("with item loader", () => {
150 const { store, schema } = testStore();
151 const api = createApi<ApiCtx>();
152 api.use(mdw.api({ schema }));
153 api.use(api.routes());
154 api.use(function* fetchApi(ctx, next) {
155 ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
156 const data = { users: [mockUser] };
157 ctx.json = { ok: true, value: data };
158 yield* next();
159 });
160
161 const fetchUser = api.create<{ id: string }>(
162 "/users/:id",
163 { supervisor: takeEvery },
164 function* processUsers(ctx: ApiCtx<unknown, { users: User[] }>, next) {
165 yield* next();
166 if (!ctx.json.ok) return;
167
168 const { value } = ctx.json;
169 yield* updateStore((state) => {
170 value.users.forEach((u) => {
171 state.users[u.id] = u;
172 });
173 });
174 },
175 );
176
177 store.run(api.bootup);
178
179 const action = fetchUser({ id: mockUser.id });
180 store.dispatch(action);
181 assertLike(store.getState(), {
182 users: { [mockUser.id]: mockUser },
183 loaders: {
184 "/users/:id": {
185 status: "success",
186 },
187 [action.payload.key]: {
188 status: "success",
189 },
190 },
191 });
192});
193
194test("with POST", () => {
195 const { store, schema } = testStore();
196 const query = createApi();
197 query.use(mdw.queryCtx);
198 query.use(mdw.api({ schema }));
199 query.use(query.routes());
200 query.use(function* fetchApi(ctx, next) {
201 const request = ctx.req();
202 expect(request).toEqual({
203 url: "/users",
204 headers: {},
205 method: "POST",
206 body: JSON.stringify({ email: "test@test.com" }),
207 });
208
209 const data = {
210 users: [mockUser],
211 };
212 ctx.response = new Response(jsonBlob(data), { status: 200 });
213 yield* next();
214 });
215
216 const createUser = query.create<{ email: string }>(
217 "/users [POST]",
218 { supervisor: takeEvery },
219 function* processUsers(
220 ctx: ApiCtx<{ email: string }, { users: User[] }>,
221 next,
222 ) {
223 ctx.request = ctx.req({
224 method: "POST",
225 body: JSON.stringify({ email: ctx.payload.email }),
226 });
227
228 yield* next();
229
230 if (!ctx.json.ok) return;
231
232 const { users } = ctx.json.value;
233 yield* updateStore((state) => {
234 users.forEach((u) => {
235 state.users[u.id] = u;
236 });
237 });
238 },
239 );
240
241 store.run(query.bootup);
242 store.dispatch(createUser({ email: mockUser.email }));
243});
244
245test("simpleCache", () => {
246 const { store, schema } = testStore();
247 const api = createApi<ApiCtx>();
248 api.use(mdw.api({ schema }));
249 api.use(api.routes());
250 api.use(function* fetchApi(ctx, next) {
251 const data = { users: [mockUser] };
252 ctx.response = new Response(jsonBlob(data));
253 ctx.json = { ok: true, value: data };
254 yield* next();
255 });
256
257 const fetchUsers = api.get("/users", { supervisor: takeEvery }, api.cache());
258 store.run(api.bootup);
259
260 const action = fetchUsers();
261 store.dispatch(action);
262 assertLike(store.getState(), {
263 data: {
264 [action.payload.key]: { users: [mockUser] },
265 },
266 loaders: {
267 [`${fetchUsers}`]: {
268 status: "success",
269 },
270 },
271 });
272});
273
274test("overriding default loader behavior", () => {
275 const { store, schema } = testStore();
276 const api = createApi<ApiCtx>();
277 api.use(mdw.api({ schema }));
278 api.use(api.routes());
279 api.use(function* fetchApi(ctx, next) {
280 const data = { users: [mockUser] };
281 ctx.response = new Response(jsonBlob(data));
282 ctx.json = { ok: true, value: data };
283 yield* next();
284 });
285
286 const fetchUsers = api.create(
287 "/users",
288 { supervisor: takeEvery },
289 function* (ctx: ApiCtx<unknown, { users: User[] }>, next) {
290 yield* next();
291
292 if (!ctx.json.ok) {
293 return;
294 }
295 const { value } = ctx.json;
296 ctx.loader = { message: "yes", meta: { wow: true } };
297 yield* updateStore((state) => {
298 value.users.forEach((u) => {
299 state.users[u.id] = u;
300 });
301 });
302 },
303 );
304
305 store.run(api.bootup);
306
307 store.dispatch(fetchUsers());
308 assertLike(store.getState(), {
309 users: { [mockUser.id]: mockUser },
310 loaders: {
311 [`${fetchUsers}`]: {
312 status: "success",
313 message: "yes",
314 meta: { wow: true },
315 },
316 },
317 });
318});
319
320test("mdw.api() - error handler", () => {
321 let err = false;
322 console.error = (msg: string) => {
323 if (err) return;
324 expect(msg).toBe("Error: something happened. Check the endpoint [/users]");
325 err = true;
326 };
327
328 const { schema, store } = testStore();
329 const query = createApi<ApiCtx>();
330 query.use(mdw.api({ schema }));
331 query.use(query.routes());
332 query.use(function* () {
333 throw new Error("something happened");
334 });
335
336 const fetchUsers = query.create("/users", { supervisor: takeEvery });
337
338 store.run(query.bootup);
339 store.dispatch(fetchUsers());
340});
341
342test("createApi with own key", async () => {
343 const { schema, store } = testStore();
344 const query = createApi();
345 query.use(mdw.api({ schema }));
346 query.use(query.routes());
347 query.use(mdw.customKey);
348 query.use(function* fetchApi(ctx, next) {
349 const data = {
350 users: [{ ...mockUser, ...ctx.action.payload.options }],
351 };
352 ctx.response = new Response(jsonBlob(data), { status: 200 });
353 yield* next();
354 });
355
356 const theTestKey = `some-custom-key-${Math.ceil(Math.random() * 1000)}`;
357
358 const createUserCustomKey = query.post<{ email: string }>(
359 "/users",
360 { supervisor: takeEvery },
361 function* processUsers(ctx: ApiCtx, next) {
362 ctx.cache = true;
363 ctx.key = theTestKey; // or some calculated key //
364 yield* next();
365 const buff = yield* safe(() => {
366 if (!ctx.response) throw new Error("no response");
367 return ctx.response.arrayBuffer();
368 });
369 if (!buff.ok) {
370 throw buff.error;
371 }
372
373 const result = new TextDecoder("utf-8").decode(buff.value);
374 const { users } = JSON.parse(result);
375 if (!users) return;
376 const curUsers = (users as User[]).reduce<Record<string, User>>(
377 (acc, u) => {
378 acc[u.id] = u;
379 return acc;
380 },
381 {},
382 );
383 ctx.response = new Response();
384 ctx.json = {
385 ok: true,
386 value: curUsers,
387 };
388 },
389 );
390 const newUEmail = `${mockUser.email}.org`;
391
392 store.run(query.bootup);
393
394 store.dispatch(createUserCustomKey({ email: newUEmail }));
395
396 await store.run(waitForLoader(schema.loaders, createUserCustomKey));
397
398 const expectedKey = theTestKey
399 ? `/users [POST]|${theTestKey}`
400 : createKey("/users [POST]", { email: newUEmail });
401
402 const s = store.getState();
403 expect(schema.cache.selectById(s, { id: expectedKey })).toEqual({
404 "1": { id: "1", name: "test", email: newUEmail },
405 });
406
407 expect(expectedKey.split("|")[1]).toEqual(theTestKey);
408});
409
410test("createApi with custom key but no payload", async () => {
411 const { store, schema } = testStore();
412 const query = createApi();
413 query.use(mdw.api({ schema }));
414 query.use(query.routes());
415 query.use(mdw.customKey);
416 query.use(function* fetchApi(ctx, next) {
417 const data = {
418 users: [mockUser],
419 };
420 ctx.response = new Response(jsonBlob(data), { status: 200 });
421 yield* next();
422 });
423
424 const theTestKey = `some-custom-key-${Math.ceil(Math.random() * 1000)}`;
425
426 const getUsers = query.get(
427 "/users",
428 { supervisor: takeEvery },
429 function* processUsers(ctx: ApiCtx, next) {
430 ctx.cache = true;
431 ctx.key = theTestKey; // or some calculated key //
432 yield* next();
433 const buff = yield* safe(() => {
434 if (!ctx.response) throw new Error("no response");
435 return ctx.response?.arrayBuffer();
436 });
437 if (!buff.ok) {
438 throw buff.error;
439 }
440
441 const result = new TextDecoder("utf-8").decode(buff.value);
442 const { users } = JSON.parse(result);
443 if (!users) return;
444 const curUsers = (users as User[]).reduce<Record<string, User>>(
445 (acc, u) => {
446 acc[u.id] = u;
447 return acc;
448 },
449 {},
450 );
451 ctx.response = new Response();
452 ctx.json = {
453 ok: true,
454 value: curUsers,
455 };
456 },
457 );
458
459 store.run(query.bootup);
460 store.dispatch(getUsers());
461
462 await store.run(waitForLoader(schema.loaders, getUsers));
463
464 const expectedKey = theTestKey
465 ? `/users [GET]|${theTestKey}`
466 : createKey("/users [GET]", null);
467
468 const s = store.getState();
469 expect(schema.cache.selectById(s, { id: expectedKey })).toEqual({
470 "1": mockUser,
471 });
472
473 expect(expectedKey.split("|")[1]).toBe(theTestKey);
474});
475
476test("errorHandler", () => {
477 let a = 0;
478 const query = createApi<ApiCtx>();
479 query.use(function* errorHandler<Ctx extends ThunkCtx = ThunkCtx>(
480 ctx: Ctx,
481 next: Next,
482 ) {
483 try {
484 a = 1;
485 yield* next();
486 a = 2;
487 } catch (err) {
488 const errorMessage = err instanceof Error ? err.message : "Unknown error";
489 console.error(
490 `Error: ${errorMessage}. Check the endpoint [${ctx.name}]`,
491 ctx,
492 );
493 }
494 });
495 query.use(mdw.queryCtx);
496 query.use(query.routes());
497 query.use(function* fetchApi(ctx, next) {
498 if (`${ctx.req().url}`.startsWith("/users/")) {
499 ctx.json = { ok: true, value: mockUser2 };
500 yield* next();
501 return;
502 }
503 const data = {
504 users: [mockUser],
505 };
506 ctx.json = { ok: true, value: data };
507 yield* next();
508 });
509
510 const fetchUsers = query.create(
511 "/users",
512 { supervisor: takeEvery },
513 function* processUsers(_: ApiCtx<unknown, { users: User[] }>, next) {
514 // throw new Error("some error");
515 yield* next();
516 },
517 );
518
519 const store = createStore({
520 initialState: {
521 users: {},
522 },
523 });
524 store.run(query.bootup);
525 store.dispatch(fetchUsers());
526 expect(store.getState()).toEqual({
527 users: {},
528 });
529 expect(a).toEqual(2);
530});
531
532test("stub predicate", async () => {
533 let actual: { ok: boolean } = { ok: false };
534 const { store, schema } = testStore();
535 const api = createApi();
536 api.use(function* (ctx, next) {
537 ctx.stub = true;
538 yield* next();
539 });
540
541 api.use(mdw.api({ schema }));
542 api.use(api.routes());
543 api.use(mdw.fetch({ baseUrl: "http://nowhere.com" }));
544
545 const stub = mdw.predicate((ctx) => ctx.stub === true);
546
547 const fetchUsers = api.get("/users", { supervisor: takeEvery }, [
548 function* (ctx, next) {
549 yield* next();
550 actual = ctx.json;
551 yield* put({ type: "DONE" });
552 },
553 stub(function* (ctx, next) {
554 ctx.response = new Response(JSON.stringify({ frodo: "shire" }));
555 yield* next();
556 }),
557 ]);
558
559 store.run(api.bootup);
560 store.dispatch(fetchUsers());
561
562 await store.run(waitFor(() => actual.ok));
563
564 expect(actual).toEqual({
565 ok: true,
566 value: { frodo: "shire" },
567 });
568});