Skip to content

Commit 0f7d2de

Browse files
Varixowmertens
authored andcommitted
feat: implement q-action
1 parent 7a79cd8 commit 0f7d2de

File tree

14 files changed

+271
-146
lines changed

14 files changed

+271
-146
lines changed

packages/qwik-router/global.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
/* eslint-disable no-var */
22
// Globals used by qwik-router, for internal use only
33

4-
type RequestEventInternal =
5-
import('./middleware/request-handler/request-event').RequestEventInternal;
64
type AsyncStore = import('node:async_hooks').AsyncLocalStorage<RequestEventInternal>;
75
type SerializationStrategy = import('@qwik.dev/core/internal').SerializationStrategy;
86

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type {
2+
ActionInternal,
3+
JSONObject,
4+
RequestEvent,
5+
RequestHandler,
6+
} from '../../runtime/src/types';
7+
import { runValidators } from './loader-endpoints';
8+
import {
9+
getRequestActions,
10+
getRequestMode,
11+
RequestEvQwikSerializer,
12+
type RequestEventInternal,
13+
} from './request-event';
14+
import { measure, verifySerializable } from './resolve-request-handlers';
15+
import type { QwikSerializer } from './types';
16+
import { IsQAction, QActionId } from './user-response';
17+
import { _UNINITIALIZED, type ValueOrPromise } from '@qwik.dev/core/internal';
18+
19+
export function actionHandler(routeActions: ActionInternal[]): RequestHandler {
20+
return async (requestEvent: RequestEvent) => {
21+
const requestEv = requestEvent as RequestEventInternal;
22+
23+
const isQAction = requestEv.sharedMap.has(IsQAction);
24+
if (!isQAction) {
25+
return;
26+
}
27+
28+
if (requestEv.headersSent || requestEv.exited) {
29+
return;
30+
}
31+
const actionId = requestEv.sharedMap.get(QActionId);
32+
33+
// Execute just this action
34+
const actions = getRequestActions(requestEv);
35+
const isDev = getRequestMode(requestEv) === 'dev';
36+
const qwikSerializer = requestEv[RequestEvQwikSerializer];
37+
const method = requestEv.method;
38+
39+
if (isDev && method === 'GET') {
40+
console.warn(
41+
'Seems like you are submitting a Qwik Action via GET request. Qwik Actions should be submitted via POST request.\nMake sure your <form> has method="POST" attribute, like this: <form method="POST">'
42+
);
43+
}
44+
if (method === 'POST') {
45+
let action: ActionInternal | undefined;
46+
for (const routeAction of routeActions) {
47+
if (routeAction.__id === actionId) {
48+
action = routeAction;
49+
break;
50+
}
51+
// TODO: do we need to initialize the rest with _UNINITIALIZED?
52+
}
53+
if (!action) {
54+
const serverActionsMap = globalThis._qwikActionsMap as
55+
| Map<string, ActionInternal>
56+
| undefined;
57+
action = serverActionsMap?.get(actionId);
58+
}
59+
60+
if (!action) {
61+
requestEv.json(404, { error: 'Action not found' });
62+
return;
63+
}
64+
65+
await executeAction(action, actions, requestEv, isDev, qwikSerializer);
66+
67+
if (requestEv.request.headers.get('accept')?.includes('application/json')) {
68+
// only return the action data if the client accepts json, otherwise return the html page
69+
const data = await qwikSerializer._serialize([actions[actionId]]);
70+
requestEv.headers.set('Content-Type', 'application/json; charset=utf-8');
71+
requestEv.send(200, data);
72+
return;
73+
}
74+
}
75+
};
76+
}
77+
78+
async function executeAction(
79+
action: ActionInternal,
80+
actions: Record<string, ValueOrPromise<unknown> | undefined>,
81+
requestEv: RequestEventInternal,
82+
isDev: boolean,
83+
qwikSerializer: QwikSerializer
84+
) {
85+
const selectedActionId = action.__id;
86+
requestEv.sharedMap.set(QActionId, selectedActionId);
87+
const data = await requestEv.parseBody();
88+
if (!data || typeof data !== 'object') {
89+
throw new Error(`Expected request data for the action id ${selectedActionId} to be an object`);
90+
}
91+
const result = await runValidators(requestEv, action.__validators, data, isDev);
92+
if (!result.success) {
93+
actions[selectedActionId] = requestEv.fail(result.status ?? 500, result.error);
94+
} else {
95+
const actionResolved = isDev
96+
? await measure(requestEv, action.__qrl.getHash(), () =>
97+
action.__qrl.call(requestEv, result.data as JSONObject, requestEv)
98+
)
99+
: await action.__qrl.call(requestEv, result.data as JSONObject, requestEv);
100+
if (isDev) {
101+
verifySerializable(qwikSerializer, actionResolved, action.__qrl);
102+
}
103+
actions[selectedActionId] = actionResolved;
104+
}
105+
}

packages/qwik-router/src/middleware/request-handler/loader-endpoints.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function loaderDataHandler(routeLoaders: LoaderInternal[]): RequestHandle
5858
};
5959
}
6060

61-
export function singleLoaderHandler(routeLoaders: LoaderInternal[]): RequestHandler {
61+
export function loaderHandler(routeLoaders: LoaderInternal[]): RequestHandler {
6262
return async (requestEvent: RequestEvent) => {
6363
const requestEv = requestEvent as RequestEventInternal;
6464

packages/qwik-router/src/middleware/request-handler/qdata-endpoints.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
import type { RequestEvent } from '@qwik.dev/router';
44
import { _serialize } from 'packages/qwik/core-internal';
5-
import { RequestEvIsRewrite, RequestEvSharedActionId } from './request-event';
5+
import { RequestEvIsRewrite } from './request-event';
66
import { getPathname } from './resolve-request-handlers';
77
import { IsQData } from './user-response';
88

99
export interface QData {
1010
status: number;
1111
href: string;
12-
action?: string;
1312
redirect?: string;
1413
isRewrite?: boolean;
1514
}
@@ -32,7 +31,6 @@ export async function qDataHandler(requestEv: RequestEvent) {
3231
const qData: QData = {
3332
status,
3433
href: getPathname(requestEv.url),
35-
action: requestEv.sharedMap.get(RequestEvSharedActionId),
3634
redirect: redirectLocation ?? undefined,
3735
isRewrite: requestEv.sharedMap.get(RequestEvIsRewrite),
3836
};

packages/qwik-router/src/middleware/request-handler/request-event.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ValueOrPromise } from '@qwik.dev/core';
22
import { _deserialize, _UNINITIALIZED, type SerializationStrategy } from '@qwik.dev/core/internal';
3-
import { QDATA_KEY } from '../../runtime/src/constants';
3+
import { QACTION_KEY, QDATA_KEY } from '../../runtime/src/constants';
44
import {
55
LoadedRouteProp,
66
type ActionInternal,
@@ -16,9 +16,10 @@ import { Cookie } from './cookie';
1616
import {
1717
AbortMessage,
1818
RedirectMessage,
19-
ServerError,
2019
RewriteMessage,
20+
ServerError,
2121
} from '@qwik.dev/router/middleware/request-handler';
22+
import { executeLoader } from './loader-endpoints';
2223
import { encoder } from './resolve-request-handlers';
2324
import type {
2425
CacheControl,
@@ -32,26 +33,27 @@ import type {
3233
ServerRequestMode,
3334
} from './types';
3435
import {
36+
IsQAction,
3537
IsQData,
3638
IsQLoader,
3739
IsQLoaderData,
40+
LOADER_REGEX,
3841
OriginalQDataName,
3942
Q_LOADER_DATA_REGEX,
43+
QActionId,
4044
QDATA_JSON,
4145
QDATA_JSON_LEN,
4246
QLoaderId,
43-
SINGLE_LOADER_REGEX,
4447
} from './user-response';
45-
import { executeLoader } from './loader-endpoints';
4648

4749
const RequestEvLoaders = Symbol('RequestEvLoaders');
50+
const RequestEvActions = Symbol('RequestEvActions');
4851
const RequestEvMode = Symbol('RequestEvMode');
4952
const RequestEvRoute = Symbol('RequestEvRoute');
5053
export const RequestEvLoaderSerializationStrategyMap = Symbol(
5154
'RequestEvLoaderSerializationStrategyMap'
5255
);
5356
export const RequestRouteName = '@routeName';
54-
export const RequestEvSharedActionId = '@actionId';
5557
export const RequestEvSharedActionFormData = '@actionFormData';
5658
export const RequestEvSharedNonce = '@nonce';
5759
export const RequestEvIsRewrite = '@rewrite';
@@ -91,6 +93,12 @@ export function createRequestEvent(
9193
trimEnd(requestRecognized.trimLength);
9294
}
9395

96+
const actionMatch = url.searchParams.get(QACTION_KEY);
97+
if (actionMatch) {
98+
sharedMap.set(IsQAction, true);
99+
sharedMap.set(QActionId, actionMatch);
100+
}
101+
94102
let routeModuleIndex = -1;
95103
let writableStream: WritableStream<Uint8Array> | null = null;
96104
let requestData: Promise<JSONValue | undefined> | undefined = undefined;
@@ -174,8 +182,10 @@ export function createRequestEvent(
174182
};
175183

176184
const loaders: Record<string, ValueOrPromise<unknown> | undefined> = {};
185+
const actions: Record<string, ValueOrPromise<unknown> | undefined> = {};
177186
const requestEv: RequestEventInternal = {
178187
[RequestEvLoaders]: loaders,
188+
[RequestEvActions]: actions,
179189
[RequestEvLoaderSerializationStrategyMap]: new Map(),
180190
[RequestEvMode]: serverRequestEv.mode,
181191
get [RequestEvRoute]() {
@@ -359,6 +369,7 @@ export function createRequestEvent(
359369

360370
export interface RequestEventInternal extends RequestEvent, RequestEventLoader {
361371
[RequestEvLoaders]: Record<string, ValueOrPromise<unknown> | undefined>;
372+
[RequestEvActions]: Record<string, ValueOrPromise<unknown> | undefined>;
362373
[RequestEvLoaderSerializationStrategyMap]: Map<string, SerializationStrategy>;
363374
[RequestEvMode]: ServerRequestMode;
364375
[RequestEvRoute]: LoadedRoute | null;
@@ -388,6 +399,10 @@ export function getRequestLoaders(requestEv: RequestEventCommon) {
388399
return (requestEv as RequestEventInternal)[RequestEvLoaders];
389400
}
390401

402+
export function getRequestActions(requestEv: RequestEventCommon) {
403+
return (requestEv as RequestEventInternal)[RequestEvActions];
404+
}
405+
391406
export function getRequestLoaderSerializationStrategyMap(requestEv: RequestEventCommon) {
392407
return (requestEv as RequestEventInternal)[RequestEvLoaderSerializationStrategyMap];
393408
}
@@ -488,7 +503,7 @@ export function recognizeRequest(pathname: string) {
488503
};
489504
}
490505

491-
const loaderMatch = pathname.match(SINGLE_LOADER_REGEX);
506+
const loaderMatch = pathname.match(LOADER_REGEX);
492507
if (loaderMatch) {
493508
return {
494509
type: IsQLoader,

0 commit comments

Comments
 (0)