appflare 0.0.8 → 0.0.10
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/core/build.ts +9 -0
- package/cli/core/discover-handlers.ts +74 -2
- package/cli/core/handlers.ts +1 -0
- package/cli/generators/generate-cloudflare-worker/worker.ts +12 -4
- package/cli/generators/generate-cloudflare-worker/wrangler.ts +7 -0
- package/cli/generators/generate-cron-handlers/cron-handlers-block.ts +2 -0
- package/cli/generators/generate-cron-handlers/handler-entries.ts +29 -0
- package/cli/generators/generate-cron-handlers/index.ts +61 -0
- package/cli/generators/generate-cron-handlers/runtime-block.ts +49 -0
- package/cli/generators/generate-cron-handlers/type-helpers-block.ts +60 -0
- package/cli/generators/generate-db-handlers/templates.ts +51 -1
- package/cli/index.ts +22 -17
- package/cli/schema/schema-static-types.ts +259 -6
- package/cli/utils/utils.ts +3 -1
- package/cli/utils/zod-utils.ts +6 -0
- package/index.ts +1 -0
- package/lib/README.md +7 -0
- package/lib/location.ts +110 -0
- package/lib/values.ts +3 -0
- package/package.json +2 -1
- package/server/database/context.ts +89 -3
- package/server/database/populate.ts +104 -30
- package/server/database/query-builder.ts +58 -4
- package/server/types/types.ts +166 -2
package/cli/core/build.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
generateHonoServer,
|
|
15
15
|
generateWebsocketDurableObject,
|
|
16
16
|
generateSchedulerHandlers,
|
|
17
|
+
generateCronHandlers,
|
|
17
18
|
} from "./handlers";
|
|
18
19
|
|
|
19
20
|
export async function buildFromConfig(params: {
|
|
@@ -100,6 +101,14 @@ export async function buildFromConfig(params: {
|
|
|
100
101
|
schedulerTs
|
|
101
102
|
);
|
|
102
103
|
|
|
104
|
+
const { code: cronTs } = generateCronHandlers({
|
|
105
|
+
handlers,
|
|
106
|
+
outDirAbs,
|
|
107
|
+
schemaPathAbs,
|
|
108
|
+
configPathAbs,
|
|
109
|
+
});
|
|
110
|
+
await fs.writeFile(path.join(outDirAbs, "server", "cron.ts"), cronTs);
|
|
111
|
+
|
|
103
112
|
if (emit) {
|
|
104
113
|
// Remove previous emit output to avoid stale files lingering.
|
|
105
114
|
await fs.rm(path.join(outDirAbs, "dist"), { recursive: true, force: true });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import * as ts from "typescript";
|
|
3
4
|
import {
|
|
4
5
|
DiscoveredHandler,
|
|
5
6
|
HandlerKind,
|
|
@@ -29,15 +30,19 @@ export async function discoverHandlers(params: {
|
|
|
29
30
|
if (path.resolve(fileAbs) === path.resolve(params.configPathAbs)) continue;
|
|
30
31
|
|
|
31
32
|
const content = await fs.readFile(fileAbs, "utf8");
|
|
33
|
+
const cronTriggersByHandler = extractCronTriggers(content);
|
|
32
34
|
const regex =
|
|
33
|
-
/export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(query|mutation|internalQuery|internalMutation|scheduler)\s*\(/g;
|
|
35
|
+
/export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(query|mutation|internalQuery|internalMutation|scheduler|cron)\s*\(/g;
|
|
34
36
|
let match: RegExpExecArray | null;
|
|
35
37
|
while ((match = regex.exec(content)) !== null) {
|
|
38
|
+
const kind = match[2] as HandlerKind;
|
|
36
39
|
handlers.push({
|
|
37
40
|
fileName: path.basename(fileAbs, ".ts"),
|
|
38
41
|
name: match[1],
|
|
39
|
-
kind
|
|
42
|
+
kind,
|
|
40
43
|
sourceFileAbs: fileAbs,
|
|
44
|
+
cronTriggers:
|
|
45
|
+
kind === "cron" ? cronTriggersByHandler.get(match[1]) : undefined,
|
|
41
46
|
});
|
|
42
47
|
}
|
|
43
48
|
}
|
|
@@ -59,3 +64,70 @@ export async function discoverHandlers(params: {
|
|
|
59
64
|
|
|
60
65
|
return unique;
|
|
61
66
|
}
|
|
67
|
+
|
|
68
|
+
const cronPropertyNames = new Set(["cronTrigger", "cronTriggers"]);
|
|
69
|
+
|
|
70
|
+
const isExported = (node: ts.Node): boolean => {
|
|
71
|
+
return Boolean(
|
|
72
|
+
ts.canHaveModifiers(node) &&
|
|
73
|
+
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const extractCronTriggers = (sourceText: string): Map<string, string[]> => {
|
|
78
|
+
const sourceFile = ts.createSourceFile(
|
|
79
|
+
"cron-handlers.ts",
|
|
80
|
+
sourceText,
|
|
81
|
+
ts.ScriptTarget.Latest,
|
|
82
|
+
true
|
|
83
|
+
);
|
|
84
|
+
const triggersByHandler = new Map<string, string[]>();
|
|
85
|
+
|
|
86
|
+
const visit = (node: ts.Node): void => {
|
|
87
|
+
if (ts.isVariableStatement(node) && isExported(node)) {
|
|
88
|
+
for (const decl of node.declarationList.declarations) {
|
|
89
|
+
if (!ts.isIdentifier(decl.name)) continue;
|
|
90
|
+
const initializer = decl.initializer;
|
|
91
|
+
if (!initializer || !ts.isCallExpression(initializer)) continue;
|
|
92
|
+
const callee = initializer.expression;
|
|
93
|
+
if (!ts.isIdentifier(callee) || callee.text !== "cron") continue;
|
|
94
|
+
const cronTriggers = parseCronTriggerArg(initializer.arguments[0]);
|
|
95
|
+
if (cronTriggers && cronTriggers.length > 0) {
|
|
96
|
+
triggersByHandler.set(decl.name.text, cronTriggers);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
ts.forEachChild(node, visit);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
visit(sourceFile);
|
|
104
|
+
return triggersByHandler;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const parseCronTriggerArg = (arg?: ts.Expression): string[] | undefined => {
|
|
108
|
+
if (!arg || !ts.isObjectLiteralExpression(arg)) return undefined;
|
|
109
|
+
const cronProp = arg.properties.find((prop) => {
|
|
110
|
+
if (!ts.isPropertyAssignment(prop)) return false;
|
|
111
|
+
if (ts.isIdentifier(prop.name))
|
|
112
|
+
return cronPropertyNames.has(prop.name.text);
|
|
113
|
+
if (ts.isStringLiteralLike(prop.name))
|
|
114
|
+
return cronPropertyNames.has(prop.name.text);
|
|
115
|
+
return false;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!cronProp || !ts.isPropertyAssignment(cronProp)) return undefined;
|
|
119
|
+
const value = cronProp.initializer;
|
|
120
|
+
const triggers: string[] = [];
|
|
121
|
+
if (ts.isArrayLiteralExpression(value)) {
|
|
122
|
+
for (const element of value.elements) {
|
|
123
|
+
if (ts.isStringLiteralLike(element)) {
|
|
124
|
+
triggers.push(element.text.trim());
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} else if (ts.isStringLiteralLike(value)) {
|
|
128
|
+
triggers.push(value.text.trim());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const unique = Array.from(new Set(triggers.filter(Boolean)));
|
|
132
|
+
return unique.length > 0 ? unique : undefined;
|
|
133
|
+
};
|
package/cli/core/handlers.ts
CHANGED
|
@@ -4,3 +4,4 @@ export { generateApiClient } from "../generators/generate-api-client";
|
|
|
4
4
|
export { generateHonoServer } from "../generators/generate-hono-server";
|
|
5
5
|
export { generateWebsocketDurableObject } from "../generators/generate-websocket-durable-object";
|
|
6
6
|
export { generateSchedulerHandlers } from "../generators/generate-scheduler-handlers";
|
|
7
|
+
export { generateCronHandlers } from "../generators/generate-cron-handlers";
|
|
@@ -2,9 +2,16 @@ import { resolveAllowedOrigins } from "./helpers";
|
|
|
2
2
|
|
|
3
3
|
export function generateCloudflareWorkerIndex(params: {
|
|
4
4
|
allowedOrigins?: string[];
|
|
5
|
+
hasCronHandlers?: boolean;
|
|
5
6
|
}): string {
|
|
6
7
|
const allowedOrigins = resolveAllowedOrigins(params.allowedOrigins);
|
|
7
8
|
const allowedOriginsCsv = allowedOrigins.join(",");
|
|
9
|
+
const cronImports = params.hasCronHandlers
|
|
10
|
+
? 'import { handleCron } from "./cron";\n'
|
|
11
|
+
: "";
|
|
12
|
+
const scheduledBlock = params.hasCronHandlers
|
|
13
|
+
? `\n\tasync scheduled(controller, env, ctx): Promise<void> {\n\t\tawait handleCron({ controller, env, ctx });\n\t},\n`
|
|
14
|
+
: "";
|
|
8
15
|
|
|
9
16
|
return `/* eslint-disable */
|
|
10
17
|
/**
|
|
@@ -15,7 +22,7 @@ export function generateCloudflareWorkerIndex(params: {
|
|
|
15
22
|
import { createAppflareHonoServer } from "./server";
|
|
16
23
|
import { WebSocketHibernationServer } from "./websocket-hibernation-server";
|
|
17
24
|
import { createScheduler, handleSchedulerBatch } from "./scheduler";
|
|
18
|
-
import { getDatabase } from "cloudflare-do-mongo";
|
|
25
|
+
${cronImports}import { getDatabase } from "cloudflare-do-mongo";
|
|
19
26
|
import { MONGO_DURABLE_OBJECT } from "cloudflare-do-mongo/do";
|
|
20
27
|
import type { Hono } from "hono";
|
|
21
28
|
import { cors } from "hono/cors";
|
|
@@ -130,9 +137,10 @@ export default {
|
|
|
130
137
|
return response;
|
|
131
138
|
},
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
140
|
+
${scheduledBlock}
|
|
141
|
+
async queue(batch, env): Promise<void> {
|
|
142
|
+
await handleSchedulerBatch({ batch, env });
|
|
143
|
+
},
|
|
136
144
|
} satisfies ExportedHandler<Env>;
|
|
137
145
|
|
|
138
146
|
export { MONGO_DURABLE_OBJECT, WebSocketHibernationServer };
|
|
@@ -11,6 +11,7 @@ export function generateWranglerJson(params: {
|
|
|
11
11
|
config: AppflareConfig;
|
|
12
12
|
configDirAbs: string;
|
|
13
13
|
allowedOrigins?: string[];
|
|
14
|
+
cronTriggers?: string[];
|
|
14
15
|
}): string {
|
|
15
16
|
const allowedOrigins = resolveAllowedOrigins(params.allowedOrigins);
|
|
16
17
|
const bucketBinding =
|
|
@@ -82,5 +83,11 @@ export function generateWranglerJson(params: {
|
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
if (params.cronTriggers && params.cronTriggers.length > 0) {
|
|
87
|
+
wrangler.triggers = {
|
|
88
|
+
crons: Array.from(new Set(params.cronTriggers)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
return `${JSON.stringify(wrangler, null, 2)}\n`;
|
|
86
93
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DiscoveredHandler } from "../../utils/utils";
|
|
2
|
+
|
|
3
|
+
const stringifyTriggers = (triggers?: string[]): string => {
|
|
4
|
+
if (!triggers || triggers.length === 0) return "[]";
|
|
5
|
+
return `[${triggers.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const buildCronHandlerEntries = (params: {
|
|
9
|
+
handlers: DiscoveredHandler[];
|
|
10
|
+
localNameFor: (handler: DiscoveredHandler) => string;
|
|
11
|
+
}): string => {
|
|
12
|
+
if (params.handlers.length === 0) return "";
|
|
13
|
+
|
|
14
|
+
return params.handlers
|
|
15
|
+
.map((handler) => {
|
|
16
|
+
const local = params.localNameFor(handler);
|
|
17
|
+
const task = `${handler.fileName}/${handler.name}`;
|
|
18
|
+
const fallbackTriggers = stringifyTriggers(handler.cronTriggers);
|
|
19
|
+
return (
|
|
20
|
+
`\t${JSON.stringify(task)}: {\n` +
|
|
21
|
+
`\t\tfile: ${JSON.stringify(handler.fileName)},\n` +
|
|
22
|
+
`\t\tname: ${JSON.stringify(handler.name)},\n` +
|
|
23
|
+
`\t\tcronTrigger: ${local}.cronTrigger ?? ${fallbackTriggers},\n` +
|
|
24
|
+
`\t\trun: ${local}.handler,\n` +
|
|
25
|
+
`\t},`
|
|
26
|
+
);
|
|
27
|
+
})
|
|
28
|
+
.join("\n");
|
|
29
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { buildImportSection } from "../generate-hono-server/imports";
|
|
2
|
+
import type { DiscoveredHandler } from "../../utils/utils";
|
|
3
|
+
import {
|
|
4
|
+
schemaTypesImportPath,
|
|
5
|
+
serverImportPath,
|
|
6
|
+
filePreamble,
|
|
7
|
+
} from "../generate-scheduler-handlers/constants";
|
|
8
|
+
import { buildCronHandlerEntries } from "./handler-entries";
|
|
9
|
+
import { buildCronHandlersBlock } from "./cron-handlers-block";
|
|
10
|
+
import { buildTypeHelpersBlock } from "./type-helpers-block";
|
|
11
|
+
import { buildRuntimeBlock } from "./runtime-block";
|
|
12
|
+
|
|
13
|
+
export function generateCronHandlers(params: {
|
|
14
|
+
handlers: DiscoveredHandler[];
|
|
15
|
+
outDirAbs: string;
|
|
16
|
+
schemaPathAbs: string;
|
|
17
|
+
configPathAbs: string;
|
|
18
|
+
}): { code: string; cronTriggers: string[] } {
|
|
19
|
+
const cronHandlers = params.handlers.filter(
|
|
20
|
+
(handler) => handler.kind === "cron"
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const imports = buildImportSection({
|
|
24
|
+
handlers: cronHandlers,
|
|
25
|
+
outDirAbs: params.outDirAbs,
|
|
26
|
+
schemaPathAbs: params.schemaPathAbs,
|
|
27
|
+
configPathAbs: params.configPathAbs,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const handlerImportBlock =
|
|
31
|
+
imports.handlerImports.length > 0
|
|
32
|
+
? `${imports.handlerImports.join("\n")}\n\n`
|
|
33
|
+
: "";
|
|
34
|
+
|
|
35
|
+
const handlerEntries = buildCronHandlerEntries({
|
|
36
|
+
handlers: cronHandlers,
|
|
37
|
+
localNameFor: imports.localNameFor,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const importsBlock = `import { createAppflareDbContext, type AppflareDbContext } from ${JSON.stringify(serverImportPath)};\nimport type {\n\tScheduler,\n\tSchedulerEnqueueOptions,\n\tSchedulerPayload,\n\tSchedulerTaskName,\n\tAppflareAuthContext,\n\tAppflareAuthSession,\n\tAppflareAuthUser,\n} from ${JSON.stringify(schemaTypesImportPath)};\nimport { createScheduler } from "./scheduler";\nimport { getDatabase } from "cloudflare-do-mongo";\nimport { Db } from "mongodb";\n${handlerImportBlock}`;
|
|
41
|
+
|
|
42
|
+
const code = [
|
|
43
|
+
filePreamble,
|
|
44
|
+
"",
|
|
45
|
+
importsBlock,
|
|
46
|
+
buildCronHandlersBlock(handlerEntries),
|
|
47
|
+
buildTypeHelpersBlock(),
|
|
48
|
+
buildRuntimeBlock(),
|
|
49
|
+
"export type { CronTaskName };\n",
|
|
50
|
+
].join("\n");
|
|
51
|
+
|
|
52
|
+
const cronTriggers = Array.from(
|
|
53
|
+
new Set(
|
|
54
|
+
cronHandlers
|
|
55
|
+
.flatMap((handler) => handler.cronTriggers ?? [])
|
|
56
|
+
.filter((value): value is string => Boolean(value))
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return { code, cronTriggers };
|
|
61
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const buildRuntimeBlock = (): string => `
|
|
2
|
+
const normalizeCronTrigger = (value: CronTriggerValue): string[] => {
|
|
3
|
+
if (!value) return [];
|
|
4
|
+
if (typeof value === "string") return [value.trim()].filter(Boolean);
|
|
5
|
+
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export async function handleCron(params: {
|
|
9
|
+
controller: { cron: string };
|
|
10
|
+
env: Env;
|
|
11
|
+
ctx: { waitUntil(promise: Promise<unknown>): void };
|
|
12
|
+
}): Promise<void> {
|
|
13
|
+
const { controller, env, ctx } = params;
|
|
14
|
+
const db = createAppflareDbContext({
|
|
15
|
+
db: getDatabase(env.MONGO_DB) as unknown as Db,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const scheduler = env.APPFLARE_SCHEDULER_QUEUE
|
|
19
|
+
? createScheduler(env.APPFLARE_SCHEDULER_QUEUE)
|
|
20
|
+
: undefined;
|
|
21
|
+
|
|
22
|
+
const baseContext: CronHandlerContext = {
|
|
23
|
+
...emptyAuthContext,
|
|
24
|
+
db,
|
|
25
|
+
env,
|
|
26
|
+
scheduler,
|
|
27
|
+
controller: {
|
|
28
|
+
cron: controller.cron,
|
|
29
|
+
scheduledTime: (controller as { scheduledTime?: number }).scheduledTime,
|
|
30
|
+
nextScheduledTime: (controller as { nextScheduledTime?: number })
|
|
31
|
+
.nextScheduledTime,
|
|
32
|
+
},
|
|
33
|
+
ctx,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handlerLookup = cronHandlers as unknown as CronHandlerLookup;
|
|
37
|
+
const cronValue = controller.cron;
|
|
38
|
+
|
|
39
|
+
for (const [task, handler] of Object.entries(handlerLookup)) {
|
|
40
|
+
const triggers = normalizeCronTrigger(handler.cronTrigger);
|
|
41
|
+
if (!triggers.includes(cronValue)) continue;
|
|
42
|
+
try {
|
|
43
|
+
await handler.run(baseContext);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error("Cron task failed", task, err);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const buildTypeHelpersBlock = (): string => `
|
|
2
|
+
type CronHandlers = typeof cronHandlers;
|
|
3
|
+
|
|
4
|
+
type CronTaskName = keyof CronHandlers extends never
|
|
5
|
+
? string
|
|
6
|
+
: keyof CronHandlers;
|
|
7
|
+
|
|
8
|
+
type CronTriggerValue = string | readonly string[] | undefined;
|
|
9
|
+
|
|
10
|
+
type Env = {
|
|
11
|
+
MONGO_DB: unknown;
|
|
12
|
+
APPFLARE_SCHEDULER_QUEUE?: {
|
|
13
|
+
send: (body: unknown, options?: { delaySeconds?: number }) => Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
} & Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
const emptyAuthContext: AppflareAuthContext = {
|
|
18
|
+
session: null as AppflareAuthSession,
|
|
19
|
+
user: null as AppflareAuthUser,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type SchedulerEnqueue = {
|
|
23
|
+
<TTask extends SchedulerTaskName>(
|
|
24
|
+
task: TTask,
|
|
25
|
+
...args: SchedulerPayload<TTask> extends undefined
|
|
26
|
+
? [payload?: SchedulerPayload<TTask>, options?: SchedulerEnqueueOptions]
|
|
27
|
+
: [payload: SchedulerPayload<TTask>, options?: SchedulerEnqueueOptions]
|
|
28
|
+
): Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type TypedScheduler = Omit<Scheduler, "enqueue"> & {
|
|
32
|
+
enqueue: SchedulerEnqueue;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ScheduledController = {
|
|
36
|
+
cron: string;
|
|
37
|
+
scheduledTime?: number;
|
|
38
|
+
nextScheduledTime?: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type ExecutionContext = {
|
|
42
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type CronHandlerContext = AppflareAuthContext & {
|
|
46
|
+
db: AppflareDbContext;
|
|
47
|
+
env: Env;
|
|
48
|
+
scheduler?: TypedScheduler;
|
|
49
|
+
controller: ScheduledController;
|
|
50
|
+
ctx: ExecutionContext;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type CronHandlerLookup = Record<
|
|
54
|
+
string,
|
|
55
|
+
{
|
|
56
|
+
cronTrigger: CronTriggerValue;
|
|
57
|
+
run: (ctx: CronHandlerContext) => Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
>;
|
|
60
|
+
`;
|
|
@@ -13,6 +13,8 @@ export const buildHandlerFileContent = (
|
|
|
13
13
|
): string => {
|
|
14
14
|
const findFn = `find${params.pascalName}`;
|
|
15
15
|
const findOneFn = `findOne${params.pascalName}`;
|
|
16
|
+
const sumFn = `sum${params.pascalName}`;
|
|
17
|
+
const avgFn = `avg${params.pascalName}`;
|
|
16
18
|
const insertFn = `insert${params.pascalName}`;
|
|
17
19
|
const updateFn = `update${params.pascalName}`;
|
|
18
20
|
const deleteFn = `delete${params.pascalName}`;
|
|
@@ -26,7 +28,9 @@ import { z } from "zod";
|
|
|
26
28
|
import {
|
|
27
29
|
internalMutation,
|
|
28
30
|
internalQuery,
|
|
31
|
+
type AppflareInclude,
|
|
29
32
|
type EditableDoc,
|
|
33
|
+
type Doc,
|
|
30
34
|
type Id,
|
|
31
35
|
type QuerySort,
|
|
32
36
|
type QueryWhere,
|
|
@@ -40,6 +44,8 @@ export const ${findFn} = internalQuery({
|
|
|
40
44
|
.optional(),
|
|
41
45
|
limit: z.number().int().nonnegative().optional(),
|
|
42
46
|
offset: z.number().int().nonnegative().optional(),
|
|
47
|
+
include: z.custom<AppflareInclude<Doc<${JSON.stringify(params.tableName)}>>>()
|
|
48
|
+
.optional(),
|
|
43
49
|
},
|
|
44
50
|
handler: async (ctx, args) => {
|
|
45
51
|
return ctx.db[${JSON.stringify(params.tableName)} as any].findMany({
|
|
@@ -47,6 +53,7 @@ export const ${findFn} = internalQuery({
|
|
|
47
53
|
orderBy: args.sort as any,
|
|
48
54
|
skip: args.offset,
|
|
49
55
|
take: args.limit,
|
|
56
|
+
include: args.include as any,
|
|
50
57
|
});
|
|
51
58
|
},
|
|
52
59
|
});
|
|
@@ -58,6 +65,8 @@ export const ${findOneFn} = internalQuery({
|
|
|
58
65
|
sort: z.custom<QuerySort<${JSON.stringify(params.tableName)}>>()
|
|
59
66
|
.optional(),
|
|
60
67
|
offset: z.number().int().nonnegative().optional(),
|
|
68
|
+
include: z.custom<AppflareInclude<Doc<${JSON.stringify(params.tableName)}>>>()
|
|
69
|
+
.optional(),
|
|
61
70
|
},
|
|
62
71
|
handler: async (ctx, args) => {
|
|
63
72
|
return ctx.db[${JSON.stringify(params.tableName)} as any].findFirst({
|
|
@@ -65,6 +74,45 @@ export const ${findOneFn} = internalQuery({
|
|
|
65
74
|
orderBy: args.sort as any,
|
|
66
75
|
skip: args.offset,
|
|
67
76
|
take: 1,
|
|
77
|
+
include: args.include as any,
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const ${sumFn} = internalQuery({
|
|
83
|
+
args: {
|
|
84
|
+
fields: z.array(z.string()).min(1),
|
|
85
|
+
where: z.custom<QueryWhere<${JSON.stringify(params.tableName)}>>()
|
|
86
|
+
.optional(),
|
|
87
|
+
groupBy: z.union([z.string(), z.array(z.string())]).optional(),
|
|
88
|
+
populate: z.custom<AppflareInclude<Doc<${JSON.stringify(params.tableName)}>>>()
|
|
89
|
+
.optional(),
|
|
90
|
+
},
|
|
91
|
+
handler: async (ctx, args) => {
|
|
92
|
+
return ctx.db[${JSON.stringify(params.tableName)} as any].aggregate({
|
|
93
|
+
where: args.where as any,
|
|
94
|
+
groupBy: args.groupBy as any,
|
|
95
|
+
sum: args.fields as any,
|
|
96
|
+
populate: args.populate as any,
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export const ${avgFn} = internalQuery({
|
|
102
|
+
args: {
|
|
103
|
+
fields: z.array(z.string()).min(1),
|
|
104
|
+
where: z.custom<QueryWhere<${JSON.stringify(params.tableName)}>>()
|
|
105
|
+
.optional(),
|
|
106
|
+
groupBy: z.union([z.string(), z.array(z.string())]).optional(),
|
|
107
|
+
populate: z.custom<AppflareInclude<Doc<${JSON.stringify(params.tableName)}>>>()
|
|
108
|
+
.optional(),
|
|
109
|
+
},
|
|
110
|
+
handler: async (ctx, args) => {
|
|
111
|
+
return ctx.db[${JSON.stringify(params.tableName)} as any].aggregate({
|
|
112
|
+
where: args.where as any,
|
|
113
|
+
groupBy: args.groupBy as any,
|
|
114
|
+
avg: args.fields as any,
|
|
115
|
+
populate: args.populate as any,
|
|
68
116
|
});
|
|
69
117
|
},
|
|
70
118
|
});
|
|
@@ -121,11 +169,13 @@ export const ${deleteFn} = internalMutation({
|
|
|
121
169
|
export const buildExportLine = (params: ExportLineParams): string => {
|
|
122
170
|
const findFn = `find${params.pascalName}`;
|
|
123
171
|
const findOneFn = `findOne${params.pascalName}`;
|
|
172
|
+
const sumFn = `sum${params.pascalName}`;
|
|
173
|
+
const avgFn = `avg${params.pascalName}`;
|
|
124
174
|
const insertFn = `insert${params.pascalName}`;
|
|
125
175
|
const updateFn = `update${params.pascalName}`;
|
|
126
176
|
const deleteFn = `delete${params.pascalName}`;
|
|
127
177
|
|
|
128
|
-
return `export { ${findFn}, ${findOneFn}, ${insertFn}, ${updateFn}, ${deleteFn} } from "./${params.tableName}";`;
|
|
178
|
+
return `export { ${findFn}, ${findOneFn}, ${sumFn}, ${avgFn}, ${insertFn}, ${updateFn}, ${deleteFn} } from "./${params.tableName}";`;
|
|
129
179
|
};
|
|
130
180
|
|
|
131
181
|
export const buildIndexFileContent = (
|
package/cli/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
generateHonoServer,
|
|
13
13
|
generateWebsocketDurableObject,
|
|
14
14
|
generateSchedulerHandlers,
|
|
15
|
+
generateCronHandlers,
|
|
15
16
|
} from "./core/handlers";
|
|
16
17
|
import {
|
|
17
18
|
generateCloudflareWorkerIndex,
|
|
@@ -19,22 +20,11 @@ import {
|
|
|
19
20
|
} from "./generators/generate-cloudflare-worker";
|
|
20
21
|
import { generateSchemaTypes, getSchemaTableNames } from "./schema/schema";
|
|
21
22
|
import { runTscEmit, writeEmitTsconfig } from "./utils/tsc";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
outDir: string;
|
|
28
|
-
/** Optional override for where wrangler.json is written. Resolved relative to the config file. */
|
|
29
|
-
wranglerOutPath?: string;
|
|
30
|
-
/** Optional override for wrangler.json main entrypoint. Defaults to ./server/index.ts in the generated worker. */
|
|
31
|
-
wranglerMain?: string;
|
|
32
|
-
corsOrigin?: string | string[];
|
|
33
|
-
auth?: {
|
|
34
|
-
enabled?: boolean;
|
|
35
|
-
basePath?: string;
|
|
36
|
-
};
|
|
37
|
-
};
|
|
23
|
+
import {
|
|
24
|
+
AppflareConfig,
|
|
25
|
+
assertDirExists,
|
|
26
|
+
assertFileExists,
|
|
27
|
+
} from "./utils/utils";
|
|
38
28
|
|
|
39
29
|
type WatchConfig = {
|
|
40
30
|
targets: string[];
|
|
@@ -212,18 +202,33 @@ async function buildFromConfig(params: {
|
|
|
212
202
|
schedulerTs
|
|
213
203
|
);
|
|
214
204
|
|
|
205
|
+
const cronHandlersPresent = handlers.some(
|
|
206
|
+
(handler) => handler.kind === "cron"
|
|
207
|
+
);
|
|
208
|
+
const { code: cronTs, cronTriggers } = generateCronHandlers({
|
|
209
|
+
handlers,
|
|
210
|
+
outDirAbs,
|
|
211
|
+
schemaPathAbs,
|
|
212
|
+
configPathAbs,
|
|
213
|
+
});
|
|
214
|
+
await fs.writeFile(path.join(outDirAbs, "server", "cron.ts"), cronTs);
|
|
215
|
+
|
|
215
216
|
const allowedOrigins = normalizeAllowedOrigins(
|
|
216
217
|
process.env.APPFLARE_ALLOWED_ORIGINS ??
|
|
217
218
|
config.corsOrigin ??
|
|
218
219
|
"http://localhost:3000"
|
|
219
220
|
);
|
|
220
|
-
const workerIndexTs = generateCloudflareWorkerIndex({
|
|
221
|
+
const workerIndexTs = generateCloudflareWorkerIndex({
|
|
222
|
+
allowedOrigins,
|
|
223
|
+
hasCronHandlers: cronHandlersPresent,
|
|
224
|
+
});
|
|
221
225
|
await fs.writeFile(path.join(outDirAbs, "server", "index.ts"), workerIndexTs);
|
|
222
226
|
|
|
223
227
|
const wranglerJson = generateWranglerJson({
|
|
224
228
|
config,
|
|
225
229
|
configDirAbs,
|
|
226
230
|
allowedOrigins,
|
|
231
|
+
cronTriggers,
|
|
227
232
|
});
|
|
228
233
|
const wranglerOutPath =
|
|
229
234
|
config.wranglerOutPath ?? path.join(config.outDir, "wrangler.json");
|