appflare 0.0.24 → 0.0.26
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 +8 -6
- package/cli/core/config.ts +19 -4
- package/cli/core/index.ts +12 -9
- package/cli/generators/generate-api-client/index.ts +71 -36
- package/cli/generators/generate-cloudflare-worker/wrangler.ts +9 -0
- package/cli/generators/generate-hono-server/auth.ts +3 -1
- package/cli/index.ts +17 -15
- package/cli/utils/utils.ts +14 -4
- package/package.json +4 -4
- package/server/auth.ts +23 -2
package/cli/core/build.ts
CHANGED
|
@@ -32,7 +32,7 @@ export async function buildFromConfig(params: {
|
|
|
32
32
|
|
|
33
33
|
await assertDirExists(
|
|
34
34
|
projectDirAbs,
|
|
35
|
-
`Project dir not found: ${projectDirAbs}
|
|
35
|
+
`Project dir not found: ${projectDirAbs}`,
|
|
36
36
|
);
|
|
37
37
|
await assertFileExists(schemaPathAbs, `Schema not found: ${schemaPathAbs}`);
|
|
38
38
|
|
|
@@ -42,7 +42,7 @@ export async function buildFromConfig(params: {
|
|
|
42
42
|
// Re-export the user schema inside the generated output so downstream code can import it from the build directory.
|
|
43
43
|
const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
|
|
44
44
|
outDirAbs,
|
|
45
|
-
schemaPathAbs
|
|
45
|
+
schemaPathAbs,
|
|
46
46
|
);
|
|
47
47
|
const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
|
|
48
48
|
export type AppflareGeneratedSchema = typeof schema;
|
|
@@ -57,7 +57,7 @@ export default schema;
|
|
|
57
57
|
});
|
|
58
58
|
await fs.writeFile(
|
|
59
59
|
path.join(outDirAbs, "src", "schema-types.ts"),
|
|
60
|
-
schemaTypesTs
|
|
60
|
+
schemaTypesTs,
|
|
61
61
|
);
|
|
62
62
|
|
|
63
63
|
// (Re)generate built-in DB handlers based on the schema tables.
|
|
@@ -78,6 +78,8 @@ export default schema;
|
|
|
78
78
|
config.auth && config.auth.enabled === false
|
|
79
79
|
? undefined
|
|
80
80
|
: (config.auth?.basePath ?? "/auth"),
|
|
81
|
+
authEnabled: config.auth?.enabled !== false,
|
|
82
|
+
configPathAbs,
|
|
81
83
|
});
|
|
82
84
|
await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
|
|
83
85
|
|
|
@@ -99,7 +101,7 @@ export default schema;
|
|
|
99
101
|
});
|
|
100
102
|
await fs.writeFile(
|
|
101
103
|
path.join(outDirAbs, "server", "websocket-hibernation-server.ts"),
|
|
102
|
-
websocketDoTs
|
|
104
|
+
websocketDoTs,
|
|
103
105
|
);
|
|
104
106
|
|
|
105
107
|
const schedulerTs = generateSchedulerHandlers({
|
|
@@ -110,7 +112,7 @@ export default schema;
|
|
|
110
112
|
});
|
|
111
113
|
await fs.writeFile(
|
|
112
114
|
path.join(outDirAbs, "server", "scheduler.ts"),
|
|
113
|
-
schedulerTs
|
|
115
|
+
schedulerTs,
|
|
114
116
|
);
|
|
115
117
|
|
|
116
118
|
const { code: cronTs } = generateCronHandlers({
|
|
@@ -144,7 +146,7 @@ async function writeEmitTsconfig(params: {
|
|
|
144
146
|
"./_generated";
|
|
145
147
|
const tsconfigPathAbs = path.join(
|
|
146
148
|
params.configDirAbs,
|
|
147
|
-
".appflare.tsconfig.emit.json"
|
|
149
|
+
".appflare.tsconfig.emit.json",
|
|
148
150
|
);
|
|
149
151
|
const content = {
|
|
150
152
|
compilerOptions: {
|
package/cli/core/config.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import { AppflareConfig, assertFileExists } from "../utils/utils";
|
|
5
5
|
|
|
6
6
|
export async function loadConfig(
|
|
7
|
-
configPathAbs: string
|
|
7
|
+
configPathAbs: string,
|
|
8
8
|
): Promise<{ config: AppflareConfig; configDirAbs: string }> {
|
|
9
9
|
await assertFileExists(configPathAbs, `Config not found: ${configPathAbs}`);
|
|
10
10
|
const configDirAbs = path.dirname(configPathAbs);
|
|
@@ -13,7 +13,7 @@ export async function loadConfig(
|
|
|
13
13
|
const config = (mod?.default ?? mod) as Partial<AppflareConfig>;
|
|
14
14
|
if (!config || typeof config !== "object") {
|
|
15
15
|
throw new Error(
|
|
16
|
-
`Invalid config export in ${configPathAbs} (expected default export object)
|
|
16
|
+
`Invalid config export in ${configPathAbs} (expected default export object)`,
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
if (typeof config.dir !== "string" || !config.dir) {
|
|
@@ -40,6 +40,12 @@ export async function loadConfig(
|
|
|
40
40
|
if (auth.options !== undefined && typeof auth.options !== "object") {
|
|
41
41
|
throw new Error(`Invalid config.auth.options in ${configPathAbs}`);
|
|
42
42
|
}
|
|
43
|
+
if (
|
|
44
|
+
auth.clientOptions !== undefined &&
|
|
45
|
+
typeof auth.clientOptions !== "object"
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(`Invalid config.auth.clientOptions in ${configPathAbs}`);
|
|
48
|
+
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
const storage = (config as AppflareConfig).storage;
|
|
@@ -61,7 +67,7 @@ export async function loadConfig(
|
|
|
61
67
|
typeof storage.bucketBinding !== "string"
|
|
62
68
|
) {
|
|
63
69
|
throw new Error(
|
|
64
|
-
`Invalid config.storage.bucketBinding in ${configPathAbs}
|
|
70
|
+
`Invalid config.storage.bucketBinding in ${configPathAbs}`,
|
|
65
71
|
);
|
|
66
72
|
}
|
|
67
73
|
if (
|
|
@@ -69,9 +75,18 @@ export async function loadConfig(
|
|
|
69
75
|
typeof storage.defaultCacheControl !== "string"
|
|
70
76
|
) {
|
|
71
77
|
throw new Error(
|
|
72
|
-
`Invalid config.storage.defaultCacheControl in ${configPathAbs}
|
|
78
|
+
`Invalid config.storage.defaultCacheControl in ${configPathAbs}`,
|
|
73
79
|
);
|
|
74
80
|
}
|
|
81
|
+
if (
|
|
82
|
+
storage.kvBinding !== undefined &&
|
|
83
|
+
typeof storage.kvBinding !== "string"
|
|
84
|
+
) {
|
|
85
|
+
throw new Error(`Invalid config.storage.kvBinding in ${configPathAbs}`);
|
|
86
|
+
}
|
|
87
|
+
if (storage.kvId !== undefined && typeof storage.kvId !== "string") {
|
|
88
|
+
throw new Error(`Invalid config.storage.kvId in ${configPathAbs}`);
|
|
89
|
+
}
|
|
75
90
|
}
|
|
76
91
|
return { config: config as AppflareConfig, configDirAbs };
|
|
77
92
|
}
|
package/cli/core/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ type AppflareConfig = {
|
|
|
27
27
|
auth?: {
|
|
28
28
|
enabled?: boolean;
|
|
29
29
|
basePath?: string;
|
|
30
|
+
clientOptions?: Record<string, unknown>;
|
|
30
31
|
};
|
|
31
32
|
};
|
|
32
33
|
|
|
@@ -37,12 +38,12 @@ program.name("appflare").description("Appflare CLI").version("0.0.0");
|
|
|
37
38
|
program
|
|
38
39
|
.command("build")
|
|
39
40
|
.description(
|
|
40
|
-
"Generate typed schema + query/mutation client/server into outDir"
|
|
41
|
+
"Generate typed schema + query/mutation client/server into outDir",
|
|
41
42
|
)
|
|
42
43
|
.option(
|
|
43
44
|
"-c, --config <path>",
|
|
44
45
|
"Path to appflare.config.ts",
|
|
45
|
-
"appflare.config.ts"
|
|
46
|
+
"appflare.config.ts",
|
|
46
47
|
)
|
|
47
48
|
.option("--emit", "Also run tsc to emit JS + .d.ts into outDir/dist")
|
|
48
49
|
.action(async (options: { config: string; emit?: boolean }) => {
|
|
@@ -69,7 +70,7 @@ async function main(): Promise<void> {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
async function loadConfig(
|
|
72
|
-
configPathAbs: string
|
|
73
|
+
configPathAbs: string,
|
|
73
74
|
): Promise<{ config: AppflareConfig; configDirAbs: string }> {
|
|
74
75
|
await assertFileExists(configPathAbs, `Config not found: ${configPathAbs}`);
|
|
75
76
|
const configDirAbs = path.dirname(configPathAbs);
|
|
@@ -78,7 +79,7 @@ async function loadConfig(
|
|
|
78
79
|
const config = (mod?.default ?? mod) as Partial<AppflareConfig>;
|
|
79
80
|
if (!config || typeof config !== "object") {
|
|
80
81
|
throw new Error(
|
|
81
|
-
`Invalid config export in ${configPathAbs} (expected default export object)
|
|
82
|
+
`Invalid config export in ${configPathAbs} (expected default export object)`,
|
|
82
83
|
);
|
|
83
84
|
}
|
|
84
85
|
if (typeof config.dir !== "string" || !config.dir) {
|
|
@@ -107,7 +108,7 @@ async function buildFromConfig(params: {
|
|
|
107
108
|
|
|
108
109
|
await assertDirExists(
|
|
109
110
|
projectDirAbs,
|
|
110
|
-
`Project dir not found: ${projectDirAbs}
|
|
111
|
+
`Project dir not found: ${projectDirAbs}`,
|
|
111
112
|
);
|
|
112
113
|
await assertFileExists(schemaPathAbs, `Schema not found: ${schemaPathAbs}`);
|
|
113
114
|
|
|
@@ -116,7 +117,7 @@ async function buildFromConfig(params: {
|
|
|
116
117
|
|
|
117
118
|
const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
|
|
118
119
|
outDirAbs,
|
|
119
|
-
schemaPathAbs
|
|
120
|
+
schemaPathAbs,
|
|
120
121
|
);
|
|
121
122
|
const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
|
|
122
123
|
export type AppflareGeneratedSchema = typeof schema;
|
|
@@ -131,7 +132,7 @@ export default schema;
|
|
|
131
132
|
});
|
|
132
133
|
await fs.writeFile(
|
|
133
134
|
path.join(outDirAbs, "src", "schema-types.ts"),
|
|
134
|
-
schemaTypesTs
|
|
135
|
+
schemaTypesTs,
|
|
135
136
|
);
|
|
136
137
|
|
|
137
138
|
// (Re)generate built-in DB handlers based on the schema tables.
|
|
@@ -152,6 +153,8 @@ export default schema;
|
|
|
152
153
|
config.auth && config.auth.enabled === false
|
|
153
154
|
? undefined
|
|
154
155
|
: (config.auth?.basePath ?? "/auth"),
|
|
156
|
+
authEnabled: config.auth?.enabled !== false,
|
|
157
|
+
configPathAbs,
|
|
155
158
|
});
|
|
156
159
|
await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
|
|
157
160
|
|
|
@@ -173,7 +176,7 @@ export default schema;
|
|
|
173
176
|
});
|
|
174
177
|
await fs.writeFile(
|
|
175
178
|
path.join(outDirAbs, "server", "websocket-hibernation-server.ts"),
|
|
176
|
-
websocketDoTs
|
|
179
|
+
websocketDoTs,
|
|
177
180
|
);
|
|
178
181
|
|
|
179
182
|
const schedulerTs = generateSchedulerHandlers({
|
|
@@ -184,7 +187,7 @@ export default schema;
|
|
|
184
187
|
});
|
|
185
188
|
await fs.writeFile(
|
|
186
189
|
path.join(outDirAbs, "server", "scheduler.ts"),
|
|
187
|
-
schedulerTs
|
|
190
|
+
schedulerTs,
|
|
188
191
|
);
|
|
189
192
|
|
|
190
193
|
if (emit) {
|
|
@@ -38,7 +38,7 @@ import type {
|
|
|
38
38
|
InternalQueryContext,
|
|
39
39
|
InternalQueryDefinition,
|
|
40
40
|
} from "./schema-types";
|
|
41
|
-
|
|
41
|
+
{{configImport}}
|
|
42
42
|
`;
|
|
43
43
|
|
|
44
44
|
const TYPE_DEFINITIONS_TEMPLATE = `
|
|
@@ -286,11 +286,13 @@ export type QueriesClient = {{queriesTypeDef}};
|
|
|
286
286
|
|
|
287
287
|
export type MutationsClient = {{mutationsTypeDef}};
|
|
288
288
|
|
|
289
|
+
{{authClientTypeDefinitions}}
|
|
290
|
+
|
|
289
291
|
export type AppflareApiClient = {
|
|
290
292
|
queries: QueriesClient;
|
|
291
293
|
mutations: MutationsClient;
|
|
292
294
|
storage: StorageManagerClient;
|
|
293
|
-
auth?:
|
|
295
|
+
auth?: AppflareAuthClient;
|
|
294
296
|
};
|
|
295
297
|
|
|
296
298
|
export type AppflareApiOptions = {
|
|
@@ -298,10 +300,10 @@ export type AppflareApiOptions = {
|
|
|
298
300
|
fetcher?: RequestExecutor;
|
|
299
301
|
realtime?: RealtimeConfig;
|
|
300
302
|
storage?: StorageManagerOptions;
|
|
301
|
-
auth?:
|
|
303
|
+
auth?: (BetterAuthClientOptions & { baseURL?: string });
|
|
302
304
|
};
|
|
303
305
|
|
|
304
|
-
export function createAppflareApi(options: AppflareApiOptions = {})
|
|
306
|
+
export function createAppflareApi(options: AppflareApiOptions = {}) {
|
|
305
307
|
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
306
308
|
const request = options.fetcher ?? defaultFetcher;
|
|
307
309
|
const realtime = resolveRealtimeConfig(baseUrl, options.realtime);
|
|
@@ -309,14 +311,7 @@ export function createAppflareApi(options: AppflareApiOptions = {}): AppflareApi
|
|
|
309
311
|
const mutations: MutationsClient = {{mutationsInit}};
|
|
310
312
|
const storage = createStorageManagerClient(baseUrl, request, options.storage);
|
|
311
313
|
const authBasePath = normalizeAuthBasePath({{authBasePath}}) ?? "/auth";
|
|
312
|
-
|
|
313
|
-
? undefined
|
|
314
|
-
: createAuthClient({
|
|
315
|
-
...(options.auth ?? {}),
|
|
316
|
-
baseURL:
|
|
317
|
-
(options.auth as any)?.baseURL ??
|
|
318
|
-
buildUrl(baseUrl, authBasePath),
|
|
319
|
-
});
|
|
314
|
+
{{authClientInit}}
|
|
320
315
|
return { queries, mutations, storage, auth };
|
|
321
316
|
}
|
|
322
317
|
|
|
@@ -675,7 +670,7 @@ function generateImports(params: {
|
|
|
675
670
|
}): { importLines: string[]; importAliasBySource: Map<string, string> } {
|
|
676
671
|
const handlerImportsGrouped = groupBy(
|
|
677
672
|
params.handlers,
|
|
678
|
-
(h) => h.sourceFileAbs
|
|
673
|
+
(h) => h.sourceFileAbs,
|
|
679
674
|
);
|
|
680
675
|
|
|
681
676
|
const importLines: string[] = [];
|
|
@@ -685,7 +680,7 @@ function generateImports(params: {
|
|
|
685
680
|
importAliasBySource.set(fileAbs, alias);
|
|
686
681
|
const importPath = toImportPathFromGeneratedSrc(params.outDirAbs, fileAbs);
|
|
687
682
|
importLines.push(
|
|
688
|
-
`import * as ${alias} from ${JSON.stringify(importPath)}
|
|
683
|
+
`import * as ${alias} from ${JSON.stringify(importPath)};`,
|
|
689
684
|
);
|
|
690
685
|
}
|
|
691
686
|
return { importLines, importAliasBySource };
|
|
@@ -701,7 +696,7 @@ function generateGroupedHandlers(handlers: DiscoveredHandler[]): {
|
|
|
701
696
|
const mutations = handlers.filter((h) => h.kind === "mutation");
|
|
702
697
|
const internalQueries = handlers.filter((h) => h.kind === "internalQuery");
|
|
703
698
|
const internalMutations = handlers.filter(
|
|
704
|
-
(h) => h.kind === "internalMutation"
|
|
699
|
+
(h) => h.kind === "internalMutation",
|
|
705
700
|
);
|
|
706
701
|
|
|
707
702
|
const queriesByFile = groupBy(queries, (h) => h.routePath);
|
|
@@ -709,7 +704,7 @@ function generateGroupedHandlers(handlers: DiscoveredHandler[]): {
|
|
|
709
704
|
const internalQueriesByFile = groupBy(internalQueries, (h) => h.routePath);
|
|
710
705
|
const internalMutationsByFile = groupBy(
|
|
711
706
|
internalMutations,
|
|
712
|
-
(h) => h.routePath
|
|
707
|
+
(h) => h.routePath,
|
|
713
708
|
);
|
|
714
709
|
|
|
715
710
|
return {
|
|
@@ -725,7 +720,7 @@ function generateTypeDefs(
|
|
|
725
720
|
mutationsByFile: Map<string, DiscoveredHandler[]>,
|
|
726
721
|
internalQueriesByFile: Map<string, DiscoveredHandler[]>,
|
|
727
722
|
internalMutationsByFile: Map<string, DiscoveredHandler[]>,
|
|
728
|
-
importAliasBySource: Map<string, string
|
|
723
|
+
importAliasBySource: Map<string, string>,
|
|
729
724
|
): {
|
|
730
725
|
queriesTypeDef: string;
|
|
731
726
|
mutationsTypeDef: string;
|
|
@@ -736,11 +731,11 @@ function generateTypeDefs(
|
|
|
736
731
|
const mutationsTypeLines = generateMutationsTypeLines(mutationsByFile);
|
|
737
732
|
const internalQueriesTypeLines = generateInternalTypeLines(
|
|
738
733
|
internalQueriesByFile,
|
|
739
|
-
importAliasBySource
|
|
734
|
+
importAliasBySource,
|
|
740
735
|
);
|
|
741
736
|
const internalMutationsTypeLines = generateInternalTypeLines(
|
|
742
737
|
internalMutationsByFile,
|
|
743
|
-
importAliasBySource
|
|
738
|
+
importAliasBySource,
|
|
744
739
|
);
|
|
745
740
|
|
|
746
741
|
const queriesTypeDef =
|
|
@@ -767,15 +762,15 @@ function generateTypeDefs(
|
|
|
767
762
|
function generateClientInits(
|
|
768
763
|
queriesByFile: Map<string, DiscoveredHandler[]>,
|
|
769
764
|
mutationsByFile: Map<string, DiscoveredHandler[]>,
|
|
770
|
-
importAliasBySource: Map<string, string
|
|
765
|
+
importAliasBySource: Map<string, string>,
|
|
771
766
|
): { queriesInit: string; mutationsInit: string } {
|
|
772
767
|
const queriesClientLines = generateQueriesClientLines(
|
|
773
768
|
queriesByFile,
|
|
774
|
-
importAliasBySource
|
|
769
|
+
importAliasBySource,
|
|
775
770
|
);
|
|
776
771
|
const mutationsClientLines = generateMutationsClientLines(
|
|
777
772
|
mutationsByFile,
|
|
778
|
-
importAliasBySource
|
|
773
|
+
importAliasBySource,
|
|
779
774
|
);
|
|
780
775
|
|
|
781
776
|
const queriesInit =
|
|
@@ -788,12 +783,12 @@ function generateClientInits(
|
|
|
788
783
|
|
|
789
784
|
function generateInternalInit(
|
|
790
785
|
internalByFile: Map<string, DiscoveredHandler[]>,
|
|
791
|
-
importAliasBySource: Map<string, string
|
|
786
|
+
importAliasBySource: Map<string, string>,
|
|
792
787
|
): string {
|
|
793
788
|
if (internalByFile.size === 0) return "{}";
|
|
794
789
|
const lines: string[] = [];
|
|
795
790
|
for (const [fileName, list] of Array.from(internalByFile.entries()).sort(
|
|
796
|
-
(a, b) => a[0].localeCompare(b[0])
|
|
791
|
+
(a, b) => a[0].localeCompare(b[0]),
|
|
797
792
|
)) {
|
|
798
793
|
const fileKey = renderObjectKey(fileName);
|
|
799
794
|
const inner = list
|
|
@@ -813,19 +808,19 @@ ${lines.join("\n")}
|
|
|
813
808
|
|
|
814
809
|
function generateInternalMeta(
|
|
815
810
|
internalByFile: Map<string, DiscoveredHandler[]>,
|
|
816
|
-
importAliasBySource: Map<string, string
|
|
811
|
+
importAliasBySource: Map<string, string>,
|
|
817
812
|
): string {
|
|
818
813
|
if (internalByFile.size === 0) return "";
|
|
819
814
|
const lines: string[] = [];
|
|
820
815
|
for (const [fileName, list] of Array.from(internalByFile.entries()).sort(
|
|
821
|
-
(a, b) => a[0].localeCompare(b[0])
|
|
816
|
+
(a, b) => a[0].localeCompare(b[0]),
|
|
822
817
|
)) {
|
|
823
818
|
for (const h of list.slice().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
824
819
|
const alias = importAliasBySource.get(h.sourceFileAbs)!;
|
|
825
820
|
lines.push(
|
|
826
821
|
`{ file: ${JSON.stringify(fileName)}, name: ${JSON.stringify(
|
|
827
|
-
h.name
|
|
828
|
-
)}, handler: ${alias}.${h.name} }
|
|
822
|
+
h.name,
|
|
823
|
+
)}, handler: ${alias}.${h.name} },`,
|
|
829
824
|
);
|
|
830
825
|
}
|
|
831
826
|
}
|
|
@@ -836,6 +831,8 @@ export function generateApiClient(params: {
|
|
|
836
831
|
handlers: DiscoveredHandler[];
|
|
837
832
|
outDirAbs: string;
|
|
838
833
|
authBasePath?: string;
|
|
834
|
+
authEnabled?: boolean;
|
|
835
|
+
configPathAbs?: string;
|
|
839
836
|
}): string {
|
|
840
837
|
const { importLines, importAliasBySource } = generateImports(params);
|
|
841
838
|
const {
|
|
@@ -854,12 +851,12 @@ export function generateApiClient(params: {
|
|
|
854
851
|
mutationsByFile,
|
|
855
852
|
internalQueriesByFile,
|
|
856
853
|
internalMutationsByFile,
|
|
857
|
-
importAliasBySource
|
|
854
|
+
importAliasBySource,
|
|
858
855
|
);
|
|
859
856
|
const { queriesInit, mutationsInit } = generateClientInits(
|
|
860
857
|
queriesByFile,
|
|
861
858
|
mutationsByFile,
|
|
862
|
-
importAliasBySource
|
|
859
|
+
importAliasBySource,
|
|
863
860
|
);
|
|
864
861
|
const internalHandlersCombined = new Map<string, DiscoveredHandler[]>();
|
|
865
862
|
for (const [file, list] of Array.from(internalQueriesByFile.entries())) {
|
|
@@ -871,29 +868,65 @@ export function generateApiClient(params: {
|
|
|
871
868
|
}
|
|
872
869
|
const internalInit = generateInternalInit(
|
|
873
870
|
internalHandlersCombined,
|
|
874
|
-
importAliasBySource
|
|
871
|
+
importAliasBySource,
|
|
875
872
|
);
|
|
876
873
|
const internalQueriesMeta = generateInternalMeta(
|
|
877
874
|
internalQueriesByFile,
|
|
878
|
-
importAliasBySource
|
|
875
|
+
importAliasBySource,
|
|
879
876
|
);
|
|
880
877
|
const internalMutationsMeta = generateInternalMeta(
|
|
881
878
|
internalMutationsByFile,
|
|
882
|
-
importAliasBySource
|
|
879
|
+
importAliasBySource,
|
|
883
880
|
);
|
|
884
881
|
|
|
885
882
|
const authBasePathLiteral = JSON.stringify(params.authBasePath ?? "/auth");
|
|
886
883
|
|
|
884
|
+
// Generate config import and auth client options handling
|
|
885
|
+
let configImport = "";
|
|
886
|
+
let authClientTypeDefinitions =
|
|
887
|
+
"type AppflareAuthClient = ReturnType<typeof createAuthClient>;";
|
|
888
|
+
let authClientInit = ` const auth = createAuthClient({
|
|
889
|
+
...(options.auth ?? {}),
|
|
890
|
+
baseURL:
|
|
891
|
+
(options.auth as any)?.baseURL ??
|
|
892
|
+
buildUrl(baseUrl, authBasePath),
|
|
893
|
+
});`;
|
|
894
|
+
|
|
895
|
+
if (params.authEnabled && params.configPathAbs) {
|
|
896
|
+
const configImportPath = toImportPathFromGeneratedSrc(
|
|
897
|
+
params.outDirAbs,
|
|
898
|
+
params.configPathAbs,
|
|
899
|
+
);
|
|
900
|
+
configImport = `\nimport __appflareConfig from ${JSON.stringify(configImportPath)};`;
|
|
901
|
+
|
|
902
|
+
// Use a factory function pattern to properly infer the client type from clientOptions
|
|
903
|
+
authClientTypeDefinitions = `const __getAppflareAuthClientOptions = () => (__appflareConfig.auth?.clientOptions ?? {}) as const;
|
|
904
|
+
type AppflareAuthClientOptions = ReturnType<typeof __getAppflareAuthClientOptions>;
|
|
905
|
+
const __createTypedAuthClient = (baseURL: string) => createAuthClient({
|
|
906
|
+
...__getAppflareAuthClientOptions(),
|
|
907
|
+
baseURL,
|
|
908
|
+
});
|
|
909
|
+
type AppflareAuthClient = ReturnType<typeof __createTypedAuthClient>;`;
|
|
910
|
+
|
|
911
|
+
authClientInit = ` const auth = createAuthClient({
|
|
912
|
+
...__getAppflareAuthClientOptions(),
|
|
913
|
+
...(options.auth ?? {}),
|
|
914
|
+
baseURL:
|
|
915
|
+
(options.auth as any)?.baseURL ??
|
|
916
|
+
buildUrl(baseUrl, authBasePath),
|
|
917
|
+
});`;
|
|
918
|
+
}
|
|
919
|
+
|
|
887
920
|
const typeBlocks = generateTypeBlocks(params.handlers, importAliasBySource);
|
|
888
921
|
|
|
889
922
|
return (
|
|
890
|
-
HEADER_TEMPLATE +
|
|
923
|
+
HEADER_TEMPLATE.replace("{{configImport}}", configImport) +
|
|
891
924
|
importLines.join("\n") +
|
|
892
925
|
TYPE_DEFINITIONS_TEMPLATE +
|
|
893
926
|
typeBlocks.join("\n\n") +
|
|
894
927
|
INTERNAL_TEMPLATE.replace(
|
|
895
928
|
"{{internalQueriesTypeDef}}",
|
|
896
|
-
internalQueriesTypeDef
|
|
929
|
+
internalQueriesTypeDef,
|
|
897
930
|
)
|
|
898
931
|
.replace("{{internalMutationsTypeDef}}", internalMutationsTypeDef)
|
|
899
932
|
.replace("{{internalInit}}", internalInit)
|
|
@@ -903,7 +936,9 @@ export function generateApiClient(params: {
|
|
|
903
936
|
.replace("{{mutationsTypeDef}}", mutationsTypeDef)
|
|
904
937
|
.replace("{{queriesInit}}", queriesInit)
|
|
905
938
|
.replace("{{mutationsInit}}", mutationsInit)
|
|
906
|
-
.replace("{{authBasePath}}", authBasePathLiteral)
|
|
939
|
+
.replace("{{authBasePath}}", authBasePathLiteral)
|
|
940
|
+
.replace("{{authClientTypeDefinitions}}", authClientTypeDefinitions)
|
|
941
|
+
.replace("{{authClientInit}}", authClientInit) +
|
|
907
942
|
UTILITY_FUNCTIONS_TEMPLATE
|
|
908
943
|
);
|
|
909
944
|
}
|
|
@@ -89,6 +89,15 @@ export function generateWranglerJson(params: {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
if (params.config.storage?.kvBinding) {
|
|
93
|
+
wrangler.kv_namespaces = [
|
|
94
|
+
{
|
|
95
|
+
binding: params.config.storage.kvBinding,
|
|
96
|
+
id: params.config.storage.kvId ?? "",
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
if (params.cronTriggers && params.cronTriggers.length > 0) {
|
|
93
102
|
wrangler.triggers = {
|
|
94
103
|
crons: Array.from(new Set(params.cronTriggers)),
|
|
@@ -24,11 +24,13 @@ export function buildAuthSection(config: AppflareConfig): AuthSection {
|
|
|
24
24
|
? [
|
|
25
25
|
`const __appflareAuthConfig = (appflareConfig as any).auth;`,
|
|
26
26
|
`const __appflareAuthBasePath = __appflareAuthConfig?.basePath ?? "/auth";`,
|
|
27
|
+
`const __appflareStorageConfig = (appflareConfig as any).storage;`,
|
|
28
|
+
`const __appflareKvBinding = __appflareStorageConfig?.kvBinding;`,
|
|
27
29
|
`const __appflareAuth =`,
|
|
28
30
|
`\t__appflareAuthConfig &&`,
|
|
29
31
|
`\t__appflareAuthConfig.enabled !== false &&`,
|
|
30
32
|
`\t__appflareAuthConfig.options`,
|
|
31
|
-
`\t\t? initBetterAuth(__appflareAuthConfig.options as any)`,
|
|
33
|
+
`\t\t? initBetterAuth(__appflareAuthConfig.options as any, __appflareKvBinding)`,
|
|
32
34
|
`\t\t: undefined;`,
|
|
33
35
|
`const __appflareAuthRouter = __appflareAuth`,
|
|
34
36
|
`\t? createBetterAuthRouter({`,
|
package/cli/index.ts
CHANGED
|
@@ -39,12 +39,12 @@ program.name("appflare").description("Appflare CLI").version("0.0.0");
|
|
|
39
39
|
program
|
|
40
40
|
.command("build")
|
|
41
41
|
.description(
|
|
42
|
-
"Generate typed schema + query/mutation client/server into outDir"
|
|
42
|
+
"Generate typed schema + query/mutation client/server into outDir",
|
|
43
43
|
)
|
|
44
44
|
.option(
|
|
45
45
|
"-c, --config <path>",
|
|
46
46
|
"Path to appflare.config.ts",
|
|
47
|
-
"appflare.config.ts"
|
|
47
|
+
"appflare.config.ts",
|
|
48
48
|
)
|
|
49
49
|
.option("--emit", "Also run tsc to emit JS + .d.ts into outDir/dist")
|
|
50
50
|
.option("-w, --watch", "Watch for changes and rebuild")
|
|
@@ -73,7 +73,7 @@ program
|
|
|
73
73
|
console.error(message);
|
|
74
74
|
process.exitCode = 1;
|
|
75
75
|
}
|
|
76
|
-
}
|
|
76
|
+
},
|
|
77
77
|
);
|
|
78
78
|
|
|
79
79
|
void main();
|
|
@@ -83,7 +83,7 @@ async function main(): Promise<void> {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
async function loadConfig(
|
|
86
|
-
configPathAbs: string
|
|
86
|
+
configPathAbs: string,
|
|
87
87
|
): Promise<{ config: AppflareConfig; configDirAbs: string }> {
|
|
88
88
|
await assertFileExists(configPathAbs, `Config not found: ${configPathAbs}`);
|
|
89
89
|
const configDirAbs = path.dirname(configPathAbs);
|
|
@@ -92,7 +92,7 @@ async function loadConfig(
|
|
|
92
92
|
const config = (mod?.default ?? mod) as Partial<AppflareConfig>;
|
|
93
93
|
if (!config || typeof config !== "object") {
|
|
94
94
|
throw new Error(
|
|
95
|
-
`Invalid config export in ${configPathAbs} (expected default export object)
|
|
95
|
+
`Invalid config export in ${configPathAbs} (expected default export object)`,
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
if (typeof config.dir !== "string" || !config.dir) {
|
|
@@ -133,7 +133,7 @@ async function buildFromConfig(params: {
|
|
|
133
133
|
|
|
134
134
|
await assertDirExists(
|
|
135
135
|
projectDirAbs,
|
|
136
|
-
`Project dir not found: ${projectDirAbs}
|
|
136
|
+
`Project dir not found: ${projectDirAbs}`,
|
|
137
137
|
);
|
|
138
138
|
await assertFileExists(schemaPathAbs, `Schema not found: ${schemaPathAbs}`);
|
|
139
139
|
|
|
@@ -143,7 +143,7 @@ async function buildFromConfig(params: {
|
|
|
143
143
|
// Re-export the user schema inside the generated output so downstream code can import it from the build directory.
|
|
144
144
|
const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
|
|
145
145
|
outDirAbs,
|
|
146
|
-
schemaPathAbs
|
|
146
|
+
schemaPathAbs,
|
|
147
147
|
);
|
|
148
148
|
const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
|
|
149
149
|
export type AppflareGeneratedSchema = typeof schema;
|
|
@@ -158,7 +158,7 @@ export default schema;
|
|
|
158
158
|
});
|
|
159
159
|
await fs.writeFile(
|
|
160
160
|
path.join(outDirAbs, "src", "schema-types.ts"),
|
|
161
|
-
schemaTypesTs
|
|
161
|
+
schemaTypesTs,
|
|
162
162
|
);
|
|
163
163
|
|
|
164
164
|
// (Re)generate built-in DB handlers based on the schema tables.
|
|
@@ -179,6 +179,8 @@ export default schema;
|
|
|
179
179
|
config.auth && config.auth.enabled === false
|
|
180
180
|
? undefined
|
|
181
181
|
: (config.auth?.basePath ?? "/auth"),
|
|
182
|
+
authEnabled: config.auth?.enabled !== false,
|
|
183
|
+
configPathAbs,
|
|
182
184
|
});
|
|
183
185
|
await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
|
|
184
186
|
|
|
@@ -200,7 +202,7 @@ export default schema;
|
|
|
200
202
|
});
|
|
201
203
|
await fs.writeFile(
|
|
202
204
|
path.join(outDirAbs, "server", "websocket-hibernation-server.ts"),
|
|
203
|
-
websocketDoTs
|
|
205
|
+
websocketDoTs,
|
|
204
206
|
);
|
|
205
207
|
|
|
206
208
|
const schedulerTs = generateSchedulerHandlers({
|
|
@@ -211,11 +213,11 @@ export default schema;
|
|
|
211
213
|
});
|
|
212
214
|
await fs.writeFile(
|
|
213
215
|
path.join(outDirAbs, "server", "scheduler.ts"),
|
|
214
|
-
schedulerTs
|
|
216
|
+
schedulerTs,
|
|
215
217
|
);
|
|
216
218
|
|
|
217
219
|
const cronHandlersPresent = handlers.some(
|
|
218
|
-
(handler) => handler.kind === "cron"
|
|
220
|
+
(handler) => handler.kind === "cron",
|
|
219
221
|
);
|
|
220
222
|
const { code: cronTs, cronTriggers } = generateCronHandlers({
|
|
221
223
|
handlers,
|
|
@@ -228,7 +230,7 @@ export default schema;
|
|
|
228
230
|
const allowedOrigins = normalizeAllowedOrigins(
|
|
229
231
|
process.env.APPFLARE_ALLOWED_ORIGINS ??
|
|
230
232
|
config.corsOrigin ??
|
|
231
|
-
"http://localhost:3000"
|
|
233
|
+
"http://localhost:3000",
|
|
232
234
|
);
|
|
233
235
|
const workerIndexTs = generateCloudflareWorkerIndex({
|
|
234
236
|
allowedOrigins,
|
|
@@ -297,7 +299,7 @@ async function watchAndBuild(params: {
|
|
|
297
299
|
|
|
298
300
|
lastWatchConfig = normalized;
|
|
299
301
|
console.log(
|
|
300
|
-
`[appflare] watching ${normalized.targets.length} path(s) (ignoring ${normalized.ignored.length})
|
|
302
|
+
`[appflare] watching ${normalized.targets.length} path(s) (ignoring ${normalized.ignored.length})`,
|
|
301
303
|
);
|
|
302
304
|
};
|
|
303
305
|
|
|
@@ -319,7 +321,7 @@ async function watchAndBuild(params: {
|
|
|
319
321
|
config,
|
|
320
322
|
configDirAbs,
|
|
321
323
|
configPathAbs: params.configPathAbs,
|
|
322
|
-
})
|
|
324
|
+
}),
|
|
323
325
|
);
|
|
324
326
|
console.log("[appflare] build started");
|
|
325
327
|
await buildFromConfig({
|
|
@@ -384,7 +386,7 @@ function normalizeWatchConfig(config: WatchConfig): WatchConfig {
|
|
|
384
386
|
const normalizeList = (list: string[]): string[] =>
|
|
385
387
|
[
|
|
386
388
|
...new Set(
|
|
387
|
-
list.map((item) => (hasGlob(item) ? item : path.resolve(item)))
|
|
389
|
+
list.map((item) => (hasGlob(item) ? item : path.resolve(item))),
|
|
388
390
|
),
|
|
389
391
|
].sort();
|
|
390
392
|
|
package/cli/utils/utils.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { BetterAuthOptions } from "better-auth";
|
|
4
|
+
import type { BetterAuthClientOptions } from "better-auth/client";
|
|
4
5
|
import type { StorageManagerOptions } from "../../server/storage/types";
|
|
5
6
|
|
|
6
7
|
export type AppflareAuthConfig<
|
|
7
|
-
Options extends BetterAuthOptions
|
|
8
|
+
Options extends BetterAuthOptions,
|
|
9
|
+
ClientOptions extends BetterAuthClientOptions,
|
|
8
10
|
> = {
|
|
9
11
|
enabled?: boolean;
|
|
10
12
|
basePath?: string;
|
|
13
|
+
/** Server-side Better Auth options. */
|
|
11
14
|
options?: Options;
|
|
15
|
+
/** Client-side Better Auth options used in the generated API client. */
|
|
16
|
+
clientOptions?: ClientOptions;
|
|
12
17
|
};
|
|
13
|
-
|
|
14
18
|
export type AppflareStorageConfig<
|
|
15
19
|
Env = unknown,
|
|
16
20
|
Principal = unknown,
|
|
17
|
-
> = StorageManagerOptions<Env, Principal
|
|
21
|
+
> = StorageManagerOptions<Env, Principal> & {
|
|
22
|
+
/** Optional KV binding name created in the generated worker. */
|
|
23
|
+
kvBinding?: string;
|
|
24
|
+
/** Optional KV namespace ID used in wrangler.json. */
|
|
25
|
+
kvId?: string;
|
|
26
|
+
};
|
|
18
27
|
|
|
19
28
|
export type AppflareSchedulerConfig = {
|
|
20
29
|
/** Set to false to disable scheduler generation. */
|
|
@@ -27,6 +36,7 @@ export type AppflareSchedulerConfig = {
|
|
|
27
36
|
|
|
28
37
|
export type AppflareConfig<
|
|
29
38
|
Options extends BetterAuthOptions = BetterAuthOptions,
|
|
39
|
+
ClientOptions extends BetterAuthClientOptions = BetterAuthClientOptions,
|
|
30
40
|
> = {
|
|
31
41
|
dir: string;
|
|
32
42
|
schema: string;
|
|
@@ -46,7 +56,7 @@ export type AppflareConfig<
|
|
|
46
56
|
compatibilityDate?: string;
|
|
47
57
|
[key: string]: unknown;
|
|
48
58
|
};
|
|
49
|
-
auth?: AppflareAuthConfig<Options>;
|
|
59
|
+
auth?: AppflareAuthConfig<Options, ClientOptions>;
|
|
50
60
|
storage?: AppflareStorageConfig;
|
|
51
61
|
scheduler?: AppflareSchedulerConfig;
|
|
52
62
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appflare",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"bin": {
|
|
5
5
|
"appflare": "./cli/index.ts"
|
|
6
6
|
},
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"react": "^18.3.1"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"
|
|
28
|
+
"@hono/standard-validator": "^0.2.1",
|
|
29
|
+
"better-auth": "^1.4.18",
|
|
29
30
|
"better-fetch": "^1.1.2",
|
|
30
31
|
"chokidar": "^5.0.0",
|
|
31
|
-
"commander": "^14.0.1",
|
|
32
|
-
"@hono/standard-validator": "^0.2.1",
|
|
33
32
|
"cloudflare-do-mongo": "^0.1.2",
|
|
33
|
+
"commander": "^14.0.1",
|
|
34
34
|
"hono": "^4.6.8",
|
|
35
35
|
"mongodb": "^7.0.0",
|
|
36
36
|
"zod": "^4.1.13"
|
package/server/auth.ts
CHANGED
|
@@ -41,11 +41,32 @@ export function createBetterAuthRouter<
|
|
|
41
41
|
|
|
42
42
|
export function initBetterAuth<Options extends BetterAuthOptions>(
|
|
43
43
|
options: Options,
|
|
44
|
+
kvBinding?: string,
|
|
44
45
|
): Auth<Options> {
|
|
45
|
-
|
|
46
|
+
const authConfig: BetterAuthOptions = {
|
|
46
47
|
...options,
|
|
47
48
|
database: mongodbAdapter(getDatabase((env as any).MONGO_DB) as any),
|
|
48
|
-
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Add secondary storage if KV binding is provided
|
|
52
|
+
if (kvBinding && (env as any)[kvBinding]) {
|
|
53
|
+
(authConfig as any).secondaryStorage = {
|
|
54
|
+
get: async (key: string) => {
|
|
55
|
+
const kv = (env as any)[kvBinding];
|
|
56
|
+
return await kv.get(key);
|
|
57
|
+
},
|
|
58
|
+
set: async (key: string, value: string, ttl?: number) => {
|
|
59
|
+
const kv = (env as any)[kvBinding];
|
|
60
|
+
await kv.put(key, value, ttl ? { expirationTtl: ttl } : undefined);
|
|
61
|
+
},
|
|
62
|
+
delete: async (key: string) => {
|
|
63
|
+
const kv = (env as any)[kvBinding];
|
|
64
|
+
await kv.delete(key);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return betterAuth(authConfig as Options);
|
|
49
70
|
}
|
|
50
71
|
export const getHeaders = (headers: Headers) => {
|
|
51
72
|
const newHeaders = Object.fromEntries(headers as any);
|