appflare 0.0.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/cli/README.md +101 -0
- package/cli/core/build.ts +136 -0
- package/cli/core/config.ts +29 -0
- package/cli/core/discover-handlers.ts +61 -0
- package/cli/core/handlers.ts +5 -0
- package/cli/core/index.ts +157 -0
- package/cli/generators/generate-api-client/client.ts +93 -0
- package/cli/generators/generate-api-client/index.ts +529 -0
- package/cli/generators/generate-api-client/types.ts +59 -0
- package/cli/generators/generate-api-client/utils.ts +18 -0
- package/cli/generators/generate-api-client.ts +1 -0
- package/cli/generators/generate-db-handlers.ts +138 -0
- package/cli/generators/generate-hono-server.ts +238 -0
- package/cli/generators/generate-websocket-durable-object.ts +537 -0
- package/cli/index.ts +157 -0
- package/cli/schema/schema-static-types.ts +252 -0
- package/cli/schema/schema.ts +105 -0
- package/cli/utils/tsc.ts +53 -0
- package/cli/utils/utils.ts +126 -0
- package/cli/utils/zod-utils.ts +115 -0
- package/index.ts +2 -0
- package/lib/README.md +43 -0
- package/lib/db.ts +9 -0
- package/lib/values.ts +23 -0
- package/package.json +28 -0
- package/react/README.md +67 -0
- package/react/hooks/useMutation.ts +89 -0
- package/react/hooks/usePaginatedQuery.ts +213 -0
- package/react/hooks/useQuery.ts +106 -0
- package/react/index.ts +3 -0
- package/react/shared/queryShared.ts +169 -0
- package/server/README.md +153 -0
- package/server/database/builders.ts +83 -0
- package/server/database/context.ts +265 -0
- package/server/database/populate.ts +160 -0
- package/server/database/query-builder.ts +101 -0
- package/server/database/query-utils.ts +25 -0
- package/server/db.ts +2 -0
- package/server/types/schema-refs.ts +66 -0
- package/server/types/types.ts +419 -0
- package/server/utils/id-utils.ts +123 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DiscoveredHandler,
|
|
3
|
+
groupBy,
|
|
4
|
+
pascalCase,
|
|
5
|
+
toImportPathFromGeneratedSrc,
|
|
6
|
+
} from "../../utils/utils";
|
|
7
|
+
import {
|
|
8
|
+
generateMutationsClientLines,
|
|
9
|
+
generateQueriesClientLines,
|
|
10
|
+
} from "./client";
|
|
11
|
+
import {
|
|
12
|
+
generateMutationsTypeLines,
|
|
13
|
+
generateQueriesTypeLines,
|
|
14
|
+
generateTypeBlocks,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
const HEADER_TEMPLATE = `/* eslint-disable */
|
|
18
|
+
/**
|
|
19
|
+
* This file is auto-generated by appflare/handler-build.ts.
|
|
20
|
+
* Do not edit directly.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fetch from "better-fetch";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
AnyValidator,
|
|
28
|
+
InferQueryArgs,
|
|
29
|
+
MutationDefinition,
|
|
30
|
+
QueryArgsShape,
|
|
31
|
+
QueryDefinition,
|
|
32
|
+
} from "./schema-types";
|
|
33
|
+
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const TYPE_DEFINITIONS_TEMPLATE = `
|
|
37
|
+
type AnyArgsShape = Record<string, AnyValidator>;
|
|
38
|
+
|
|
39
|
+
type AnyHandlerDefinition = QueryDefinition<AnyArgsShape, unknown> | MutationDefinition<AnyArgsShape, unknown>;
|
|
40
|
+
|
|
41
|
+
type Simplify<T> = { [K in keyof T]: T[K] };
|
|
42
|
+
|
|
43
|
+
type OptionalKeys<TArgs extends QueryArgsShape> = {
|
|
44
|
+
[K in keyof TArgs]: TArgs[K] extends z.ZodOptional<any> | z.ZodDefault<any> ? K : never;
|
|
45
|
+
}[keyof TArgs];
|
|
46
|
+
|
|
47
|
+
type RequiredKeys<TArgs extends QueryArgsShape> = Exclude<keyof TArgs, OptionalKeys<TArgs>>;
|
|
48
|
+
|
|
49
|
+
type HandlerArgsFromShape<TArgs extends QueryArgsShape> = Simplify<
|
|
50
|
+
Partial<Pick<InferQueryArgs<TArgs>, OptionalKeys<TArgs>>> &
|
|
51
|
+
Pick<InferQueryArgs<TArgs>, RequiredKeys<TArgs>>
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
type HandlerArgs<THandler extends AnyHandlerDefinition> =
|
|
55
|
+
THandler extends { args: infer TArgs extends QueryArgsShape }
|
|
56
|
+
? HandlerArgsFromShape<TArgs>
|
|
57
|
+
: never;
|
|
58
|
+
|
|
59
|
+
type HandlerResult<THandler extends AnyHandlerDefinition> = THandler extends {
|
|
60
|
+
handler: (...args: any[]) => Promise<infer TResult>;
|
|
61
|
+
}
|
|
62
|
+
? TResult
|
|
63
|
+
: never;
|
|
64
|
+
|
|
65
|
+
type HandlerInvoker<TArgs, TResult> = (args: TArgs, init?: RequestInit) => Promise<TResult>;
|
|
66
|
+
|
|
67
|
+
type HandlerArgsShape<THandler extends AnyHandlerDefinition> =
|
|
68
|
+
THandler extends { args: infer TArgs extends QueryArgsShape }
|
|
69
|
+
? TArgs
|
|
70
|
+
: never;
|
|
71
|
+
|
|
72
|
+
type HandlerSchemaFromShape<TArgs extends QueryArgsShape> = z.ZodObject<{
|
|
73
|
+
[K in keyof TArgs]: z.ZodType<InferQueryArgs<TArgs>[K]>;
|
|
74
|
+
}>;
|
|
75
|
+
|
|
76
|
+
type HandlerSchema<THandler extends AnyHandlerDefinition> = HandlerSchemaFromShape<
|
|
77
|
+
HandlerArgsShape<THandler>
|
|
78
|
+
>;
|
|
79
|
+
|
|
80
|
+
type WebSocketFactory = (url: string, protocols?: string | string[]) => WebSocket;
|
|
81
|
+
|
|
82
|
+
export type RealtimeMessage<TResult> = {
|
|
83
|
+
type?: string;
|
|
84
|
+
data?: TResult[];
|
|
85
|
+
table?: string;
|
|
86
|
+
where?: unknown;
|
|
87
|
+
[key: string]: unknown;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type HandlerWebsocketOptions<TResult> = {
|
|
91
|
+
baseUrl?: string;
|
|
92
|
+
table?: string;
|
|
93
|
+
handler?: { file: string; name: string };
|
|
94
|
+
handlerFile?: string;
|
|
95
|
+
handlerName?: string;
|
|
96
|
+
where?: Record<string, unknown>;
|
|
97
|
+
orderBy?: Record<string, unknown>;
|
|
98
|
+
take?: number;
|
|
99
|
+
skip?: number;
|
|
100
|
+
path?: string;
|
|
101
|
+
protocols?: string | string[];
|
|
102
|
+
signal?: AbortSignal;
|
|
103
|
+
websocketImpl?: WebSocketFactory;
|
|
104
|
+
onOpen?: (event: any) => void;
|
|
105
|
+
onClose?: (event: any) => void;
|
|
106
|
+
onError?: (event: any) => void;
|
|
107
|
+
onMessage?: (message: RealtimeMessage<TResult>, raw: any) => void;
|
|
108
|
+
onData?: (data: TResult[], message: RealtimeMessage<TResult>) => void;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type HandlerMetadata<THandler extends AnyHandlerDefinition> = {
|
|
112
|
+
schema: HandlerSchema<THandler>;
|
|
113
|
+
websocket: HandlerWebsocket<
|
|
114
|
+
HandlerArgs<THandler>,
|
|
115
|
+
HandlerResult<THandler>
|
|
116
|
+
>;
|
|
117
|
+
path: string;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type HandlerWebsocket<TArgs, TResult> = (
|
|
121
|
+
args?: TArgs,
|
|
122
|
+
options?: HandlerWebsocketOptions<TResult>
|
|
123
|
+
) => WebSocket;
|
|
124
|
+
|
|
125
|
+
export type AppflareHandler<THandler extends AnyHandlerDefinition> = HandlerInvoker<
|
|
126
|
+
HandlerArgs<THandler>,
|
|
127
|
+
HandlerResult<THandler>
|
|
128
|
+
> &
|
|
129
|
+
HandlerMetadata<THandler>;
|
|
130
|
+
|
|
131
|
+
type RequestExecutor = (
|
|
132
|
+
input: RequestInfo | URL,
|
|
133
|
+
init?: RequestInit
|
|
134
|
+
) => Promise<Response>;
|
|
135
|
+
|
|
136
|
+
const defaultFetcher: RequestExecutor = (input, init) => fetch(input, init);
|
|
137
|
+
|
|
138
|
+
const defaultWebSocketFactory: WebSocketFactory = (url, protocols) => {
|
|
139
|
+
if (typeof WebSocket === "undefined") {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"WebSocket is not available in this environment. Provide options.realtime.websocketImpl to create websockets."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return new WebSocket(url, protocols);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type ResolvedRealtimeConfig = {
|
|
148
|
+
baseUrl?: string;
|
|
149
|
+
path: string;
|
|
150
|
+
websocketImpl?: WebSocketFactory;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
type RealtimeConfig = {
|
|
154
|
+
baseUrl?: string;
|
|
155
|
+
path?: string;
|
|
156
|
+
websocketImpl?: WebSocketFactory;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
const CLIENT_TYPES_TEMPLATE = `
|
|
162
|
+
export type QueriesClient = {{queriesTypeDef}};
|
|
163
|
+
|
|
164
|
+
export type MutationsClient = {{mutationsTypeDef}};
|
|
165
|
+
|
|
166
|
+
export type AppflareApiClient = {
|
|
167
|
+
queries: QueriesClient;
|
|
168
|
+
mutations: MutationsClient;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export type AppflareApiOptions = {
|
|
172
|
+
baseUrl?: string;
|
|
173
|
+
fetcher?: RequestExecutor;
|
|
174
|
+
realtime?: RealtimeConfig;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export function createAppflareApi(options: AppflareApiOptions = {}): AppflareApiClient {
|
|
178
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
179
|
+
const request = options.fetcher ?? defaultFetcher;
|
|
180
|
+
const realtime = resolveRealtimeConfig(baseUrl, options.realtime);
|
|
181
|
+
const queries: QueriesClient = {{queriesInit}};
|
|
182
|
+
const mutations: MutationsClient = {{mutationsInit}};
|
|
183
|
+
return { queries, mutations };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
const UTILITY_FUNCTIONS_TEMPLATE_PART1 = `
|
|
189
|
+
function withHandlerMetadata<THandler extends AnyHandlerDefinition>(
|
|
190
|
+
invoke: HandlerInvoker<HandlerArgs<THandler>, HandlerResult<THandler>>,
|
|
191
|
+
meta: HandlerMetadata<THandler>
|
|
192
|
+
): AppflareHandler<THandler> {
|
|
193
|
+
const fn = invoke as AppflareHandler<THandler>;
|
|
194
|
+
fn.schema = meta.schema;
|
|
195
|
+
fn.websocket = meta.websocket;
|
|
196
|
+
fn.path = meta.path;
|
|
197
|
+
return fn;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveRealtimeConfig(
|
|
201
|
+
baseUrl: string,
|
|
202
|
+
realtime?: RealtimeConfig
|
|
203
|
+
): ResolvedRealtimeConfig {
|
|
204
|
+
return {
|
|
205
|
+
baseUrl: normalizeWsBaseUrl(realtime?.baseUrl ?? baseUrl),
|
|
206
|
+
path: realtime?.path ?? "/ws",
|
|
207
|
+
websocketImpl: realtime?.websocketImpl ?? defaultWebSocketFactory,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createHandlerSchema<TArgs extends QueryArgsShape>(
|
|
212
|
+
args: TArgs
|
|
213
|
+
): HandlerSchemaFromShape<TArgs> {
|
|
214
|
+
return z.object(args as any as Record<string, z.ZodTypeAny>) as HandlerSchemaFromShape<TArgs>;
|
|
215
|
+
}
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
const UTILITY_FUNCTIONS_TEMPLATE_PART2 = `
|
|
219
|
+
function createHandlerWebsocket<TArgs, TResult>(
|
|
220
|
+
realtime: ResolvedRealtimeConfig,
|
|
221
|
+
defaults: { defaultTable: string; defaultHandler: { file: string; name: string } }
|
|
222
|
+
): HandlerWebsocket<TArgs, TResult> {
|
|
223
|
+
return (args, options) => {
|
|
224
|
+
const baseUrl = normalizeWsBaseUrl(options?.baseUrl ?? realtime.baseUrl);
|
|
225
|
+
if (!baseUrl) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
"Missing realtime baseUrl. Provide createAppflareApi({ realtime: { baseUrl } }) or handler websocket options.baseUrl."
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const params = new URLSearchParams();
|
|
231
|
+
const tableParam = options?.table ?? defaults.defaultTable;
|
|
232
|
+
const normalizedTable = tableParam.endsWith("s")
|
|
233
|
+
? tableParam
|
|
234
|
+
: tableParam + "s";
|
|
235
|
+
params.set("table", normalizedTable);
|
|
236
|
+
|
|
237
|
+
const handlerRef =
|
|
238
|
+
options?.handler ??
|
|
239
|
+
(options?.handlerFile && options?.handlerName
|
|
240
|
+
? { file: options.handlerFile, name: options.handlerName }
|
|
241
|
+
: defaults.defaultHandler);
|
|
242
|
+
if (handlerRef) {
|
|
243
|
+
params.set("handler", JSON.stringify(handlerRef));
|
|
244
|
+
}
|
|
245
|
+
const where = options?.where ?? (isPlainObject(args) ? (args as Record<string, unknown>) : undefined);
|
|
246
|
+
if (where && Object.keys(where).length > 0) {
|
|
247
|
+
params.set("where", JSON.stringify(where));
|
|
248
|
+
}
|
|
249
|
+
if (options?.orderBy) params.set("orderBy", JSON.stringify(options.orderBy));
|
|
250
|
+
if (options?.take !== undefined) params.set("take", String(options.take));
|
|
251
|
+
if (options?.skip !== undefined) params.set("skip", String(options.skip));
|
|
252
|
+
|
|
253
|
+
const path = options?.path ?? realtime.path;
|
|
254
|
+
const url = buildRealtimeUrl(baseUrl, path, params);
|
|
255
|
+
const websocketFactory = options?.websocketImpl ?? realtime.websocketImpl ?? defaultWebSocketFactory;
|
|
256
|
+
const socket = websocketFactory(url, options?.protocols);
|
|
257
|
+
|
|
258
|
+
if (options?.onOpen) socket.addEventListener("open", options.onOpen as any);
|
|
259
|
+
if (options?.onClose) socket.addEventListener("close", options.onClose as any);
|
|
260
|
+
if (options?.onError) socket.addEventListener("error", options.onError as any);
|
|
261
|
+
|
|
262
|
+
const onMessage = (event: any) => {
|
|
263
|
+
const message = parseRealtimeMessage<TResult>(event?.data ?? event);
|
|
264
|
+
if (options?.onMessage) options.onMessage(message, event);
|
|
265
|
+
if (message?.type === "data" && Array.isArray((message as any).data)) {
|
|
266
|
+
options?.onData?.((message as any).data as TResult[], message);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
socket.addEventListener("message", onMessage as any);
|
|
271
|
+
|
|
272
|
+
if (options?.signal) {
|
|
273
|
+
const abortHandler = () => socket.close(1000, "aborted");
|
|
274
|
+
if (options.signal.aborted) abortHandler();
|
|
275
|
+
else options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return socket;
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
`;
|
|
282
|
+
|
|
283
|
+
const UTILITY_FUNCTIONS_TEMPLATE_PART3 = `
|
|
284
|
+
function normalizeBaseUrl(baseUrl?: string): string {
|
|
285
|
+
if (!baseUrl) {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizeWsBaseUrl(baseUrl?: string): string | undefined {
|
|
292
|
+
if (!baseUrl) return undefined;
|
|
293
|
+
if (baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://")) {
|
|
294
|
+
return baseUrl.replace(/\\/+$/, "");
|
|
295
|
+
}
|
|
296
|
+
if (baseUrl.startsWith("https://")) return baseUrl.replace(/^https/, "wss").replace(/\\/$/, "");
|
|
297
|
+
if (baseUrl.startsWith("http://")) return baseUrl.replace(/^http/, "ws").replace(/\\/$/, "");
|
|
298
|
+
return baseUrl.replace(/\\/$/, "");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildUrl(baseUrl: string, path: string): string {
|
|
302
|
+
if (!baseUrl) {
|
|
303
|
+
return path;
|
|
304
|
+
}
|
|
305
|
+
const normalizedPath = path.startsWith("/") ? path : "/" + path;
|
|
306
|
+
return baseUrl + normalizedPath;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function buildRealtimeUrl(
|
|
310
|
+
baseUrl: string,
|
|
311
|
+
path: string,
|
|
312
|
+
params: URLSearchParams
|
|
313
|
+
): string {
|
|
314
|
+
const normalizedBase = baseUrl.replace(/\\/+$/, "");
|
|
315
|
+
const normalizedPath = path.startsWith("/") ? path : "/" + path;
|
|
316
|
+
const query = params.toString();
|
|
317
|
+
return query ? normalizedBase + normalizedPath + "?" + query : normalizedBase + normalizedPath;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildQueryUrl(
|
|
321
|
+
baseUrl: string,
|
|
322
|
+
path: string,
|
|
323
|
+
params: Record<string, unknown> | undefined
|
|
324
|
+
): string {
|
|
325
|
+
const url = buildUrl(baseUrl, path);
|
|
326
|
+
const query = serializeQueryParams(params);
|
|
327
|
+
return query ? url + "?" + query : url;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function serializeQueryParams(
|
|
331
|
+
params: Record<string, unknown> | undefined
|
|
332
|
+
): string {
|
|
333
|
+
if (!params) {
|
|
334
|
+
return "";
|
|
335
|
+
}
|
|
336
|
+
const searchParams = new URLSearchParams();
|
|
337
|
+
for (const [key, value] of Object.entries(params)) {
|
|
338
|
+
if (value === undefined || value === null) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (Array.isArray(value)) {
|
|
342
|
+
for (const entry of value) {
|
|
343
|
+
searchParams.append(key, serializeQueryValue(entry));
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
searchParams.append(key, serializeQueryValue(value));
|
|
348
|
+
}
|
|
349
|
+
return searchParams.toString();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function serializeQueryValue(value: unknown): string {
|
|
353
|
+
if (value instanceof Date) {
|
|
354
|
+
return value.toISOString();
|
|
355
|
+
}
|
|
356
|
+
if (typeof value === "object") {
|
|
357
|
+
return JSON.stringify(value);
|
|
358
|
+
}
|
|
359
|
+
return String(value);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
363
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function parseRealtimeMessage<TResult>(value: unknown): RealtimeMessage<TResult> {
|
|
367
|
+
if (typeof value === "string") {
|
|
368
|
+
try {
|
|
369
|
+
return JSON.parse(value) as RealtimeMessage<TResult>;
|
|
370
|
+
} catch {
|
|
371
|
+
return { type: "message", raw: value } as any;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return value as RealtimeMessage<TResult>;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function ensureJsonHeaders(headers?: HeadersInit): HeadersInit {
|
|
378
|
+
if (!headers) {
|
|
379
|
+
return { "content-type": "application/json" };
|
|
380
|
+
}
|
|
381
|
+
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
382
|
+
const next = new Headers(headers);
|
|
383
|
+
if (!next.has("content-type")) {
|
|
384
|
+
next.set("content-type", "application/json");
|
|
385
|
+
}
|
|
386
|
+
return next;
|
|
387
|
+
}
|
|
388
|
+
if (Array.isArray(headers)) {
|
|
389
|
+
const entries = headers.slice();
|
|
390
|
+
const hasContentType = entries.some(
|
|
391
|
+
([key]) => key.toLowerCase() === "content-type"
|
|
392
|
+
);
|
|
393
|
+
if (!hasContentType) {
|
|
394
|
+
entries.push(["content-type", "application/json"]);
|
|
395
|
+
}
|
|
396
|
+
return entries;
|
|
397
|
+
}
|
|
398
|
+
if (typeof headers === "object") {
|
|
399
|
+
const record = { ...(headers as Record<string, string>) };
|
|
400
|
+
if (!hasHeader(record, "content-type")) {
|
|
401
|
+
record["content-type"] = "application/json";
|
|
402
|
+
}
|
|
403
|
+
return record;
|
|
404
|
+
}
|
|
405
|
+
return { "content-type": "application/json" };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function hasHeader(record: Record<string, string>, name: string): boolean {
|
|
409
|
+
const needle = name.toLowerCase();
|
|
410
|
+
return Object.keys(record).some((key) => key.toLowerCase() === needle);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function parseJson<TResult>(response: Response): Promise<TResult> {
|
|
414
|
+
if (!response.ok) {
|
|
415
|
+
throw new Error("Request failed with status " + response.status);
|
|
416
|
+
}
|
|
417
|
+
return (await response.json()) as TResult;
|
|
418
|
+
}
|
|
419
|
+
`;
|
|
420
|
+
|
|
421
|
+
const UTILITY_FUNCTIONS_TEMPLATE =
|
|
422
|
+
UTILITY_FUNCTIONS_TEMPLATE_PART1 +
|
|
423
|
+
UTILITY_FUNCTIONS_TEMPLATE_PART2 +
|
|
424
|
+
UTILITY_FUNCTIONS_TEMPLATE_PART3;
|
|
425
|
+
|
|
426
|
+
function generateImports(params: {
|
|
427
|
+
handlers: DiscoveredHandler[];
|
|
428
|
+
outDirAbs: string;
|
|
429
|
+
}): { importLines: string[]; importAliasBySource: Map<string, string> } {
|
|
430
|
+
const handlerImportsGrouped = groupBy(
|
|
431
|
+
params.handlers,
|
|
432
|
+
(h) => h.sourceFileAbs
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const importLines: string[] = [];
|
|
436
|
+
const importAliasBySource = new Map<string, string>();
|
|
437
|
+
for (const [fileAbs, list] of Array.from(handlerImportsGrouped.entries())) {
|
|
438
|
+
const alias = `__appflare_${pascalCase(list[0].fileName)}`;
|
|
439
|
+
importAliasBySource.set(fileAbs, alias);
|
|
440
|
+
const importPath = toImportPathFromGeneratedSrc(params.outDirAbs, fileAbs);
|
|
441
|
+
importLines.push(
|
|
442
|
+
`import * as ${alias} from ${JSON.stringify(importPath)};`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
return { importLines, importAliasBySource };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function generateGroupedHandlers(handlers: DiscoveredHandler[]): {
|
|
449
|
+
queriesByFile: Map<string, DiscoveredHandler[]>;
|
|
450
|
+
mutationsByFile: Map<string, DiscoveredHandler[]>;
|
|
451
|
+
} {
|
|
452
|
+
const queries = handlers.filter((h) => h.kind === "query");
|
|
453
|
+
const mutations = handlers.filter((h) => h.kind === "mutation");
|
|
454
|
+
|
|
455
|
+
const queriesByFile = groupBy(queries, (h) => h.fileName);
|
|
456
|
+
const mutationsByFile = groupBy(mutations, (h) => h.fileName);
|
|
457
|
+
|
|
458
|
+
return { queriesByFile, mutationsByFile };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function generateTypeDefs(
|
|
462
|
+
queriesByFile: Map<string, DiscoveredHandler[]>,
|
|
463
|
+
mutationsByFile: Map<string, DiscoveredHandler[]>
|
|
464
|
+
): { queriesTypeDef: string; mutationsTypeDef: string } {
|
|
465
|
+
const queriesTypeLines = generateQueriesTypeLines(queriesByFile);
|
|
466
|
+
const mutationsTypeLines = generateMutationsTypeLines(mutationsByFile);
|
|
467
|
+
|
|
468
|
+
const queriesTypeDef =
|
|
469
|
+
queriesByFile.size === 0 ? "{}" : `{\n${queriesTypeLines}\n}`;
|
|
470
|
+
const mutationsTypeDef =
|
|
471
|
+
mutationsByFile.size === 0 ? "{}" : `{\n${mutationsTypeLines}\n}`;
|
|
472
|
+
|
|
473
|
+
return { queriesTypeDef, mutationsTypeDef };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function generateClientInits(
|
|
477
|
+
queriesByFile: Map<string, DiscoveredHandler[]>,
|
|
478
|
+
mutationsByFile: Map<string, DiscoveredHandler[]>,
|
|
479
|
+
importAliasBySource: Map<string, string>
|
|
480
|
+
): { queriesInit: string; mutationsInit: string } {
|
|
481
|
+
const queriesClientLines = generateQueriesClientLines(
|
|
482
|
+
queriesByFile,
|
|
483
|
+
importAliasBySource
|
|
484
|
+
);
|
|
485
|
+
const mutationsClientLines = generateMutationsClientLines(
|
|
486
|
+
mutationsByFile,
|
|
487
|
+
importAliasBySource
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const queriesInit =
|
|
491
|
+
queriesByFile.size === 0 ? "{}" : `{\n${queriesClientLines}\n\t}`;
|
|
492
|
+
const mutationsInit =
|
|
493
|
+
mutationsByFile.size === 0 ? "{}" : `{\n${mutationsClientLines}\n\t}`;
|
|
494
|
+
|
|
495
|
+
return { queriesInit, mutationsInit };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function generateApiClient(params: {
|
|
499
|
+
handlers: DiscoveredHandler[];
|
|
500
|
+
outDirAbs: string;
|
|
501
|
+
}): string {
|
|
502
|
+
const { importLines, importAliasBySource } = generateImports(params);
|
|
503
|
+
const { queriesByFile, mutationsByFile } = generateGroupedHandlers(
|
|
504
|
+
params.handlers
|
|
505
|
+
);
|
|
506
|
+
const { queriesTypeDef, mutationsTypeDef } = generateTypeDefs(
|
|
507
|
+
queriesByFile,
|
|
508
|
+
mutationsByFile
|
|
509
|
+
);
|
|
510
|
+
const { queriesInit, mutationsInit } = generateClientInits(
|
|
511
|
+
queriesByFile,
|
|
512
|
+
mutationsByFile,
|
|
513
|
+
importAliasBySource
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const typeBlocks = generateTypeBlocks(params.handlers, importAliasBySource);
|
|
517
|
+
|
|
518
|
+
return (
|
|
519
|
+
HEADER_TEMPLATE +
|
|
520
|
+
importLines.join("\n") +
|
|
521
|
+
TYPE_DEFINITIONS_TEMPLATE +
|
|
522
|
+
typeBlocks.join("\n\n") +
|
|
523
|
+
CLIENT_TYPES_TEMPLATE.replace("{{queriesTypeDef}}", queriesTypeDef)
|
|
524
|
+
.replace("{{mutationsTypeDef}}", mutationsTypeDef)
|
|
525
|
+
.replace("{{queriesInit}}", queriesInit)
|
|
526
|
+
.replace("{{mutationsInit}}", mutationsInit) +
|
|
527
|
+
UTILITY_FUNCTIONS_TEMPLATE
|
|
528
|
+
);
|
|
529
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DiscoveredHandler } from "../../utils/utils";
|
|
2
|
+
import { handlerTypePrefix, renderObjectKey, sortedEntries } from "./utils";
|
|
3
|
+
|
|
4
|
+
export function generateTypeBlocks(
|
|
5
|
+
handlers: DiscoveredHandler[],
|
|
6
|
+
importAliasBySource: Map<string, string>
|
|
7
|
+
): string[] {
|
|
8
|
+
const typeBlocks: string[] = [];
|
|
9
|
+
for (const h of handlers) {
|
|
10
|
+
const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
|
|
11
|
+
const handlerAccessor = `${importAlias}[${JSON.stringify(h.name)}]`;
|
|
12
|
+
const pascal = handlerTypePrefix(h);
|
|
13
|
+
typeBlocks.push(
|
|
14
|
+
`type ${pascal}Definition = typeof ${handlerAccessor};\n` +
|
|
15
|
+
`type ${pascal}Args = HandlerArgs<${pascal}Definition>;\n` +
|
|
16
|
+
`type ${pascal}Result = HandlerResult<${pascal}Definition>;\n` +
|
|
17
|
+
`type ${pascal}Client = AppflareHandler<${pascal}Definition>;`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return typeBlocks;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function generateQueriesTypeLines(
|
|
24
|
+
queriesByFile: Map<string, DiscoveredHandler[]>
|
|
25
|
+
): string {
|
|
26
|
+
return sortedEntries(queriesByFile)
|
|
27
|
+
.map(([fileName, list]) => {
|
|
28
|
+
const fileKey = renderObjectKey(fileName);
|
|
29
|
+
const inner = list
|
|
30
|
+
.slice()
|
|
31
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
32
|
+
.map((h) => {
|
|
33
|
+
const pascal = handlerTypePrefix(h);
|
|
34
|
+
return `\t\t${h.name}: ${pascal}Client;`;
|
|
35
|
+
})
|
|
36
|
+
.join("\n");
|
|
37
|
+
return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t};`;
|
|
38
|
+
})
|
|
39
|
+
.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function generateMutationsTypeLines(
|
|
43
|
+
mutationsByFile: Map<string, DiscoveredHandler[]>
|
|
44
|
+
): string {
|
|
45
|
+
return sortedEntries(mutationsByFile)
|
|
46
|
+
.map(([fileName, list]) => {
|
|
47
|
+
const fileKey = renderObjectKey(fileName);
|
|
48
|
+
const inner = list
|
|
49
|
+
.slice()
|
|
50
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
51
|
+
.map((h) => {
|
|
52
|
+
const pascal = handlerTypePrefix(h);
|
|
53
|
+
return `\t\t${h.name}: ${pascal}Client;`;
|
|
54
|
+
})
|
|
55
|
+
.join("\n");
|
|
56
|
+
return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t};`;
|
|
57
|
+
})
|
|
58
|
+
.join("\n");
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isValidIdentifier } from "../../utils/utils";
|
|
2
|
+
|
|
3
|
+
export const sortedEntries = <T>(map: Map<string, T[]>): Array<[string, T[]]> =>
|
|
4
|
+
Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
5
|
+
|
|
6
|
+
export const renderObjectKey = (key: string): string =>
|
|
7
|
+
isValidIdentifier(key) ? key : JSON.stringify(key);
|
|
8
|
+
|
|
9
|
+
export const handlerTypePrefix = (h: any): string =>
|
|
10
|
+
h.fileName
|
|
11
|
+
.replace(/[^a-zA-Z0-9]/g, "")
|
|
12
|
+
.replace(/^./, (c: string) => c.toUpperCase()) +
|
|
13
|
+
h.name
|
|
14
|
+
.replace(/[^a-zA-Z0-9]/g, "")
|
|
15
|
+
.replace(/^./, (c: string) => c.toUpperCase());
|
|
16
|
+
|
|
17
|
+
export const normalizeTableName = (fileName: string): string =>
|
|
18
|
+
fileName.endsWith("s") ? fileName : `${fileName}s`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { generateApiClient } from "./generate-api-client/index";
|