@lovrozagar/flare 0.6.0 → 0.7.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/package.json +1 -1
- package/src/plugins/server-fn.ts +1 -1
- package/src/server/index.ts +2 -7
- package/src/server-fn/index.ts +52 -36
- package/src/server-fn/rpc.ts +41 -0
- package/src/server-fn-query.ts +4 -42
package/package.json
CHANGED
package/src/plugins/server-fn.ts
CHANGED
|
@@ -605,7 +605,7 @@ export function createServerFnPlugin(_config?: { ignorePrefix?: string }): ViteP
|
|
|
605
605
|
if (hasServerFnCalls(transformed)) {
|
|
606
606
|
transformed = replaceServerFnConfigs(transformed, (content) => {
|
|
607
607
|
const fnId = computeFnId(id, content);
|
|
608
|
-
return ` __id: "${fnId}",${content}`;
|
|
608
|
+
return ` __client: true, __id: "${fnId}",${content}`;
|
|
609
609
|
});
|
|
610
610
|
}
|
|
611
611
|
|
package/src/server/index.ts
CHANGED
|
@@ -54,7 +54,7 @@ export interface ServerBuilder<
|
|
|
54
54
|
callerData?: unknown[];
|
|
55
55
|
env: TEnv;
|
|
56
56
|
request: Request;
|
|
57
|
-
serverContext:
|
|
57
|
+
serverContext: TServerContext;
|
|
58
58
|
url: URL;
|
|
59
59
|
}) => TNewAuth | null | Promise<TNewAuth | null>,
|
|
60
60
|
) => ServerBuilder<TExcluded | "authenticateFn", TNewAuth, TServerContext, TEnv>;
|
|
@@ -72,12 +72,7 @@ export interface ServerBuilder<
|
|
|
72
72
|
: (
|
|
73
73
|
config:
|
|
74
74
|
| SecurityConfig
|
|
75
|
-
| ((ctx: {
|
|
76
|
-
env: TEnv;
|
|
77
|
-
nonce: string;
|
|
78
|
-
request: Request;
|
|
79
|
-
serverContext: Record<string, unknown>;
|
|
80
|
-
}) => SecurityConfig),
|
|
75
|
+
| ((ctx: { env: TEnv; nonce: string; request: Request; serverContext: TServerContext }) => SecurityConfig),
|
|
81
76
|
) => ServerBuilder<TExcluded | "security", TAuth, TServerContext, TEnv>;
|
|
82
77
|
serverContext: "serverContext" extends TExcluded
|
|
83
78
|
? never
|
package/src/server-fn/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { ResolvedAuth, ResolvedEnv, ResolvedServerContext } from "../route-builder/register.ts";
|
|
1
2
|
import { FORM_FN_FIELD, parseServerFnPathname, serverFnPath } from "../protocol.ts";
|
|
3
|
+
import { callServerFnOverHttp } from "./rpc.ts";
|
|
2
4
|
import {
|
|
3
5
|
isNotFoundError,
|
|
4
6
|
isRedirectResponse,
|
|
@@ -26,6 +28,8 @@ import { runValidator } from "../validation/index.ts";
|
|
|
26
28
|
export type { Validator } from "../validation/index.ts";
|
|
27
29
|
|
|
28
30
|
export interface ServerFnConfig {
|
|
31
|
+
/** Set by the client build transform: calls go over HTTP, the handler body is gone. */
|
|
32
|
+
__client?: boolean;
|
|
29
33
|
__id?: string;
|
|
30
34
|
method?: "get" | "post";
|
|
31
35
|
name: string;
|
|
@@ -36,7 +40,8 @@ export interface PiggybackedQuery {
|
|
|
36
40
|
key: unknown[];
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
/* TEnv defaults to the app's registered env (`createServer<Env>()`) */
|
|
44
|
+
export interface HandlerContext<TAuth, TInput, TEnv = ResolvedEnv> {
|
|
40
45
|
auth: TAuth;
|
|
41
46
|
env: TEnv;
|
|
42
47
|
input: TInput;
|
|
@@ -53,15 +58,17 @@ export interface HandlerContext<TAuth, TInput, TEnv = unknown> {
|
|
|
53
58
|
piggyback: (key: unknown[], data: unknown) => void;
|
|
54
59
|
request: Request;
|
|
55
60
|
revalidate: RevalidateFn;
|
|
56
|
-
serverContext
|
|
61
|
+
/* the app's `.serverContext()` result, typed through FlareRegister */
|
|
62
|
+
serverContext: ResolvedServerContext;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
export interface StreamContext<TAuth, TInput, TEnv =
|
|
65
|
+
export interface StreamContext<TAuth, TInput, TEnv = ResolvedEnv> {
|
|
60
66
|
auth: TAuth;
|
|
61
67
|
env: TEnv;
|
|
62
68
|
input: TInput;
|
|
63
69
|
request: Request;
|
|
64
|
-
serverContext
|
|
70
|
+
/* the app's `.serverContext()` result, typed through FlareRegister */
|
|
71
|
+
serverContext: ResolvedServerContext;
|
|
65
72
|
signal: AbortSignal;
|
|
66
73
|
}
|
|
67
74
|
|
|
@@ -75,18 +82,19 @@ export interface ServerFnRegistrationBase {
|
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
export interface ServerFnHandlerRegistration extends ServerFnRegistrationBase {
|
|
78
|
-
fn: (ctx: HandlerContext<unknown, unknown>) => unknown | Promise<unknown>;
|
|
85
|
+
fn: (ctx: HandlerContext<unknown, unknown, unknown>) => unknown | Promise<unknown>;
|
|
79
86
|
stream?: false;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
export interface ServerFnStreamRegistration extends ServerFnRegistrationBase {
|
|
83
|
-
fn: (ctx: StreamContext<unknown, unknown>) => AsyncGenerator<unknown>;
|
|
90
|
+
fn: (ctx: StreamContext<unknown, unknown, unknown>) => AsyncGenerator<unknown>;
|
|
84
91
|
stream: true;
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
export type ServerFnRegistration = ServerFnHandlerRegistration | ServerFnStreamRegistration;
|
|
88
95
|
|
|
89
96
|
interface BuilderState {
|
|
97
|
+
__client?: boolean;
|
|
90
98
|
__id?: string;
|
|
91
99
|
authenticate: boolean;
|
|
92
100
|
authorizeFn?: (ctx: { auth: unknown; input: unknown }) => boolean | Promise<boolean>;
|
|
@@ -95,22 +103,24 @@ interface BuilderState {
|
|
|
95
103
|
name: string;
|
|
96
104
|
}
|
|
97
105
|
|
|
98
|
-
interface ServerFnBuilderTerminal<TAuth, TInput
|
|
99
|
-
handler
|
|
100
|
-
|
|
106
|
+
interface ServerFnBuilderTerminal<TAuth, TInput> {
|
|
107
|
+
/* the output type is the handler's: callers get it without restating it */
|
|
108
|
+
handler<TOutput>(fn: (ctx: HandlerContext<TAuth, TInput>) => TOutput | Promise<TOutput>): ServerFn<TInput, TOutput>;
|
|
109
|
+
input<T>(validator: Validator<T>): ServerFnBuilderTerminal<TAuth, T>;
|
|
101
110
|
stream<TChunk>(fn: (ctx: StreamContext<TAuth, TInput>) => AsyncGenerator<TChunk>): StreamFn<TInput, TChunk>;
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
interface ServerFnBuilderAfterAuth<TAuth, TInput
|
|
113
|
+
interface ServerFnBuilderAfterAuth<TAuth, TInput> extends ServerFnBuilderTerminal<TAuth, TInput> {
|
|
105
114
|
authorize(
|
|
106
115
|
fn: (ctx: { auth: TAuth; input: TInput }) => boolean | Promise<boolean>,
|
|
107
|
-
): ServerFnBuilderTerminal<TAuth, TInput
|
|
108
|
-
input<T>(validator: Validator<T>): ServerFnBuilderAfterAuth<TAuth, T
|
|
116
|
+
): ServerFnBuilderTerminal<TAuth, TInput>;
|
|
117
|
+
input<T>(validator: Validator<T>): ServerFnBuilderAfterAuth<TAuth, T>;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
interface ServerFnBuilder<TAuth, TInput
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
interface ServerFnBuilder<TAuth, TInput> extends ServerFnBuilderAfterAuth<TAuth, TInput> {
|
|
121
|
+
/* the app's `authenticateFn` result, typed through FlareRegister */
|
|
122
|
+
authenticate(): ServerFnBuilderAfterAuth<ResolvedAuth<true>, TInput>;
|
|
123
|
+
input<T>(validator: Validator<T>): ServerFnBuilder<TAuth, T>;
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
export type ServerFn<_TInput, TOutput> = ((_input: _TInput) => Promise<TOutput>) & {
|
|
@@ -124,21 +134,27 @@ export type StreamFn<_TInput, TChunk> = ((
|
|
|
124
134
|
_registration?: ServerFnRegistration;
|
|
125
135
|
};
|
|
126
136
|
|
|
127
|
-
function createBuilderTerminal<TAuth, TInput,
|
|
128
|
-
state: BuilderState,
|
|
129
|
-
): ServerFnBuilderTerminal<TAuth, TInput, TOutput> {
|
|
137
|
+
function createBuilderTerminal<TAuth, TInput>(state: BuilderState): ServerFnBuilderTerminal<TAuth, TInput> {
|
|
130
138
|
return {
|
|
131
|
-
handler(fn) {
|
|
139
|
+
handler<TOutput>(fn: (ctx: HandlerContext<TAuth, TInput>) => TOutput | Promise<TOutput>) {
|
|
132
140
|
const registration: ServerFnHandlerRegistration = {
|
|
133
141
|
authenticate: state.authenticate,
|
|
134
142
|
authorizeFn: state.authorizeFn,
|
|
135
|
-
fn: fn as (ctx: HandlerContext<unknown, unknown>) => unknown | Promise<unknown>,
|
|
143
|
+
fn: fn as (ctx: HandlerContext<unknown, unknown, unknown>) => unknown | Promise<unknown>,
|
|
136
144
|
id: state.__id && state.__id.length > 0 ? state.__id : state.name,
|
|
137
145
|
input: state.input,
|
|
138
146
|
method: state.method,
|
|
139
147
|
name: state.name,
|
|
140
148
|
};
|
|
141
149
|
|
|
150
|
+
if (state.__client) {
|
|
151
|
+
/* browser: the server validates, authenticates, and runs the handler */
|
|
152
|
+
const clientFn: ServerFn<TInput, TOutput> = async (input: TInput): Promise<TOutput> =>
|
|
153
|
+
(await callServerFnOverHttp<TOutput>(registration, input)).data;
|
|
154
|
+
clientFn._registration = registration;
|
|
155
|
+
return clientFn;
|
|
156
|
+
}
|
|
157
|
+
|
|
142
158
|
const serverFn: ServerFn<TInput, TOutput> = async (_input: TInput): Promise<TOutput> => {
|
|
143
159
|
if (state.authenticate) {
|
|
144
160
|
throw new Error(
|
|
@@ -156,7 +172,8 @@ function createBuilderTerminal<TAuth, TInput, TOutput>(
|
|
|
156
172
|
};
|
|
157
173
|
return await fn({
|
|
158
174
|
auth: null as TAuth,
|
|
159
|
-
|
|
175
|
+
/* direct server-side invocation runs outside a request: there are no bindings */
|
|
176
|
+
env: {} as ResolvedEnv,
|
|
160
177
|
input: validated,
|
|
161
178
|
piggyback: () => {},
|
|
162
179
|
request: new Request("http://localhost"),
|
|
@@ -168,7 +185,7 @@ function createBuilderTerminal<TAuth, TInput, TOutput>(
|
|
|
168
185
|
return serverFn;
|
|
169
186
|
},
|
|
170
187
|
input<T>(validator: Validator<T>) {
|
|
171
|
-
return createBuilderTerminal<TAuth, T
|
|
188
|
+
return createBuilderTerminal<TAuth, T>({
|
|
172
189
|
...state,
|
|
173
190
|
input: validator as Validator<unknown>,
|
|
174
191
|
});
|
|
@@ -177,7 +194,7 @@ function createBuilderTerminal<TAuth, TInput, TOutput>(
|
|
|
177
194
|
const registration: ServerFnStreamRegistration = {
|
|
178
195
|
authenticate: state.authenticate,
|
|
179
196
|
authorizeFn: state.authorizeFn,
|
|
180
|
-
fn: fn as (ctx: StreamContext<unknown, unknown>) => AsyncGenerator<unknown>,
|
|
197
|
+
fn: fn as (ctx: StreamContext<unknown, unknown, unknown>) => AsyncGenerator<unknown>,
|
|
181
198
|
id: state.__id && state.__id.length > 0 ? state.__id : state.name,
|
|
182
199
|
input: state.input,
|
|
183
200
|
method: state.method,
|
|
@@ -206,7 +223,7 @@ function createBuilderTerminal<TAuth, TInput, TOutput>(
|
|
|
206
223
|
const validated = (await validatedPromise) as TInput;
|
|
207
224
|
generator = fn({
|
|
208
225
|
auth: null as TAuth,
|
|
209
|
-
env: {} as
|
|
226
|
+
env: {} as ResolvedEnv,
|
|
210
227
|
input: validated,
|
|
211
228
|
request: new Request("http://localhost"),
|
|
212
229
|
serverContext: getServerContext(),
|
|
@@ -231,19 +248,17 @@ function createBuilderTerminal<TAuth, TInput, TOutput>(
|
|
|
231
248
|
};
|
|
232
249
|
}
|
|
233
250
|
|
|
234
|
-
function createBuilderAfterAuth<TAuth, TInput,
|
|
235
|
-
state: BuilderState,
|
|
236
|
-
): ServerFnBuilderAfterAuth<TAuth, TInput, TOutput> {
|
|
251
|
+
function createBuilderAfterAuth<TAuth, TInput>(state: BuilderState): ServerFnBuilderAfterAuth<TAuth, TInput> {
|
|
237
252
|
return {
|
|
238
|
-
...createBuilderTerminal<TAuth, TInput
|
|
253
|
+
...createBuilderTerminal<TAuth, TInput>(state),
|
|
239
254
|
authorize(fn) {
|
|
240
|
-
return createBuilderTerminal<TAuth, TInput
|
|
255
|
+
return createBuilderTerminal<TAuth, TInput>({
|
|
241
256
|
...state,
|
|
242
257
|
authorizeFn: fn as (ctx: { auth: unknown; input: unknown }) => boolean | Promise<boolean>,
|
|
243
258
|
});
|
|
244
259
|
},
|
|
245
260
|
input<T>(validator: Validator<T>) {
|
|
246
|
-
return createBuilderAfterAuth<TAuth, T
|
|
261
|
+
return createBuilderAfterAuth<TAuth, T>({
|
|
247
262
|
...state,
|
|
248
263
|
input: validator as Validator<unknown>,
|
|
249
264
|
});
|
|
@@ -251,17 +266,17 @@ function createBuilderAfterAuth<TAuth, TInput, TOutput>(
|
|
|
251
266
|
};
|
|
252
267
|
}
|
|
253
268
|
|
|
254
|
-
function createBuilder<TAuth, TInput
|
|
269
|
+
function createBuilder<TAuth, TInput>(state: BuilderState): ServerFnBuilder<TAuth, TInput> {
|
|
255
270
|
return {
|
|
256
|
-
...createBuilderAfterAuth<TAuth, TInput
|
|
271
|
+
...createBuilderAfterAuth<TAuth, TInput>(state),
|
|
257
272
|
authenticate() {
|
|
258
|
-
return createBuilderAfterAuth<
|
|
273
|
+
return createBuilderAfterAuth<ResolvedAuth<true>, TInput>({
|
|
259
274
|
...state,
|
|
260
275
|
authenticate: true,
|
|
261
276
|
});
|
|
262
277
|
},
|
|
263
278
|
input<T>(validator: Validator<T>) {
|
|
264
|
-
return createBuilder<TAuth, T
|
|
279
|
+
return createBuilder<TAuth, T>({
|
|
265
280
|
...state,
|
|
266
281
|
input: validator as Validator<unknown>,
|
|
267
282
|
});
|
|
@@ -269,8 +284,9 @@ function createBuilder<TAuth, TInput, TOutput>(state: BuilderState): ServerFnBui
|
|
|
269
284
|
};
|
|
270
285
|
}
|
|
271
286
|
|
|
272
|
-
export function createServerFn(config: ServerFnConfig): ServerFnBuilder<null, void
|
|
273
|
-
return createBuilder<null, void
|
|
287
|
+
export function createServerFn(config: ServerFnConfig): ServerFnBuilder<null, void> {
|
|
288
|
+
return createBuilder<null, void>({
|
|
289
|
+
__client: config.__client,
|
|
274
290
|
__id: config.__id,
|
|
275
291
|
authenticate: false,
|
|
276
292
|
method: config.method ?? "post",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser side of a server fn call: one HTTP request to `/_flare/server-fn/{id}/{name}`.
|
|
3
|
+
* Client-safe — must not import server-context or anything that reaches node:*.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { serverFnPath } from "../protocol.ts";
|
|
7
|
+
import { serverFnGetUrl } from "./get-input.ts";
|
|
8
|
+
import { throwServerFnHttpError } from "./http-error.ts";
|
|
9
|
+
|
|
10
|
+
export interface ServerFnTarget {
|
|
11
|
+
id?: string;
|
|
12
|
+
method?: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ServerFnEnvelope<TOutput> {
|
|
17
|
+
data: TOutput;
|
|
18
|
+
queries?: Array<{ data: unknown; key: unknown[] }>;
|
|
19
|
+
revalidatedTags?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function callServerFnOverHttp<TOutput>(
|
|
23
|
+
target: ServerFnTarget,
|
|
24
|
+
input: unknown,
|
|
25
|
+
): Promise<ServerFnEnvelope<TOutput>> {
|
|
26
|
+
const url = serverFnPath(target.id ?? target.name, target.name);
|
|
27
|
+
const res =
|
|
28
|
+
target.method === "get"
|
|
29
|
+
? await fetch(serverFnGetUrl(url, input))
|
|
30
|
+
: await fetch(url, {
|
|
31
|
+
body: input !== undefined ? JSON.stringify(input) : undefined,
|
|
32
|
+
headers: { "content-type": "application/json" },
|
|
33
|
+
method: "POST",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const body: unknown = await res.json().catch(() => null);
|
|
38
|
+
throwServerFnHttpError(body, res.status, target.name);
|
|
39
|
+
}
|
|
40
|
+
return (await res.json()) as ServerFnEnvelope<TOutput>;
|
|
41
|
+
}
|
package/src/server-fn-query.ts
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
* to avoid pulling in node:async_hooks on the client.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import { throwServerFnHttpError } from "./server-fn/http-error.ts";
|
|
9
|
-
import { serverFnPath } from "./protocol.ts";
|
|
7
|
+
import { callServerFnOverHttp } from "./server-fn/rpc.ts";
|
|
10
8
|
|
|
11
9
|
export interface PiggybackedQuery {
|
|
12
10
|
data: unknown;
|
|
@@ -52,28 +50,7 @@ export function serverFnQueryOptions<TInput, TOutput>(
|
|
|
52
50
|
return serverFn(config?.input as TInput);
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
const
|
|
56
|
-
const method = reg?.method ?? "post";
|
|
57
|
-
|
|
58
|
-
const res =
|
|
59
|
-
method === "get"
|
|
60
|
-
? await fetch(serverFnGetUrl(url, config?.input))
|
|
61
|
-
: await fetch(url, {
|
|
62
|
-
body: config?.input !== undefined ? JSON.stringify(config.input) : undefined,
|
|
63
|
-
headers: { "content-type": "application/json" },
|
|
64
|
-
method: "POST",
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
const body: unknown = await res.json().catch(() => null);
|
|
69
|
-
throwServerFnHttpError(body, res.status, name);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const json = (await res.json()) as {
|
|
73
|
-
data: TOutput;
|
|
74
|
-
queries?: Array<{ data: unknown; key: unknown[] }>;
|
|
75
|
-
revalidatedTags?: string[];
|
|
76
|
-
};
|
|
53
|
+
const json = await callServerFnOverHttp<TOutput>({ id, method: reg?.method ?? "post", name }, config?.input);
|
|
77
54
|
|
|
78
55
|
if (json.queries && config?.queryClient) {
|
|
79
56
|
for (const q of json.queries) {
|
|
@@ -117,23 +94,8 @@ export function serverFnMutationOptions<TInput, TOutput>(
|
|
|
117
94
|
return serverFn(input);
|
|
118
95
|
}
|
|
119
96
|
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
body: input !== undefined ? JSON.stringify(input) : undefined,
|
|
123
|
-
headers: { "content-type": "application/json" },
|
|
124
|
-
method: "POST",
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (!res.ok) {
|
|
128
|
-
const body: unknown = await res.json().catch(() => null);
|
|
129
|
-
throwServerFnHttpError(body, res.status, name);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const json = (await res.json()) as {
|
|
133
|
-
data: TOutput;
|
|
134
|
-
queries?: Array<{ data: unknown; key: unknown[] }>;
|
|
135
|
-
revalidatedTags?: string[];
|
|
136
|
-
};
|
|
97
|
+
/* mutations always POST, whatever the fn's own method */
|
|
98
|
+
const json = await callServerFnOverHttp<TOutput>({ id, method: "post", name }, input);
|
|
137
99
|
|
|
138
100
|
if (json.queries && config?.queryClient) {
|
|
139
101
|
for (const q of json.queries) {
|