appflare 0.0.28 → 0.1.0
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/commands/index.ts +140 -0
- package/cli/generate.ts +149 -0
- package/cli/index.ts +56 -447
- package/cli/load-config.ts +182 -0
- package/cli/schema-compiler.ts +657 -0
- package/cli/templates/auth/README.md +156 -0
- package/cli/templates/auth/config.ts +61 -0
- package/cli/templates/auth/route-config.ts +18 -0
- package/cli/templates/auth/route-handler.ts +18 -0
- package/cli/templates/auth/route-request-utils.ts +55 -0
- package/cli/templates/auth/route.ts +14 -0
- package/cli/templates/core/README.md +266 -0
- package/cli/templates/core/app-creation.ts +19 -0
- package/cli/templates/core/client/appflare.ts +37 -0
- package/cli/templates/core/client/index.ts +6 -0
- package/cli/templates/core/client/storage.ts +100 -0
- package/cli/templates/core/client/types.ts +54 -0
- package/cli/templates/core/client-modules/appflare.ts +112 -0
- package/cli/templates/core/client-modules/handlers/index.ts +740 -0
- package/cli/templates/core/client-modules/handlers.ts +1 -0
- package/cli/templates/core/client-modules/index.ts +7 -0
- package/cli/templates/core/client-modules/storage.ts +180 -0
- package/cli/templates/core/client-modules/types.ts +145 -0
- package/cli/templates/core/client.ts +39 -0
- package/cli/templates/core/drizzle.ts +15 -0
- package/cli/templates/core/export.ts +14 -0
- package/cli/templates/core/handlers-route.ts +23 -0
- package/cli/templates/core/handlers.ts +1 -0
- package/cli/templates/core/imports.ts +8 -0
- package/cli/templates/core/server.ts +38 -0
- package/cli/templates/core/types.ts +6 -0
- package/cli/templates/core/wrangler.ts +109 -0
- package/cli/templates/handlers/README.md +265 -0
- package/cli/templates/handlers/auth.ts +36 -0
- package/cli/templates/handlers/execution.ts +39 -0
- package/cli/templates/handlers/generators/context/context-creation.ts +80 -0
- package/cli/templates/handlers/generators/context/error-helpers.ts +11 -0
- package/cli/templates/handlers/generators/context/scheduler.ts +24 -0
- package/cli/templates/handlers/generators/context/storage-api.ts +112 -0
- package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -0
- package/cli/templates/handlers/generators/context/types.ts +18 -0
- package/cli/templates/handlers/generators/context.ts +43 -0
- package/cli/templates/handlers/generators/execution.ts +15 -0
- package/cli/templates/handlers/generators/handlers.ts +13 -0
- package/cli/templates/handlers/index.ts +43 -0
- package/cli/templates/handlers/operations.ts +116 -0
- package/cli/templates/handlers/registration.ts +1114 -0
- package/cli/templates/handlers/types.ts +960 -0
- package/cli/templates/handlers/utils.ts +48 -0
- package/cli/types.ts +108 -0
- package/cli/utils/handler-discovery.ts +366 -0
- package/cli/utils/json-utils.ts +24 -0
- package/cli/utils/path-utils.ts +19 -0
- package/cli/utils/schema-discovery.ts +390 -0
- package/index.ts +27 -4
- package/package.json +23 -20
- package/react/index.ts +5 -3
- package/react/use-infinite-query.ts +190 -0
- package/react/use-mutation.ts +54 -0
- package/react/use-query.ts +158 -0
- package/schema.ts +262 -0
- package/tsconfig.json +2 -4
- package/cli/README.md +0 -108
- package/cli/core/build.ts +0 -187
- package/cli/core/config.ts +0 -92
- package/cli/core/discover-handlers.ts +0 -143
- package/cli/core/handlers.ts +0 -7
- package/cli/core/index.ts +0 -205
- package/cli/generators/generate-api-client/client.ts +0 -163
- package/cli/generators/generate-api-client/extract-configuration.ts +0 -121
- package/cli/generators/generate-api-client/index.ts +0 -973
- package/cli/generators/generate-api-client/types.ts +0 -164
- package/cli/generators/generate-api-client/utils.ts +0 -22
- package/cli/generators/generate-api-client.ts +0 -1
- package/cli/generators/generate-cloudflare-worker/helpers.ts +0 -24
- package/cli/generators/generate-cloudflare-worker/index.ts +0 -2
- package/cli/generators/generate-cloudflare-worker/worker.ts +0 -148
- package/cli/generators/generate-cloudflare-worker/wrangler.ts +0 -108
- package/cli/generators/generate-cloudflare-worker.ts +0 -4
- package/cli/generators/generate-cron-handlers/cron-handlers-block.ts +0 -2
- package/cli/generators/generate-cron-handlers/handler-entries.ts +0 -29
- package/cli/generators/generate-cron-handlers/index.ts +0 -61
- package/cli/generators/generate-cron-handlers/runtime-block.ts +0 -49
- package/cli/generators/generate-cron-handlers/type-helpers-block.ts +0 -60
- package/cli/generators/generate-db-handlers/index.ts +0 -33
- package/cli/generators/generate-db-handlers/prepare.ts +0 -24
- package/cli/generators/generate-db-handlers/templates.ts +0 -189
- package/cli/generators/generate-db-handlers.ts +0 -1
- package/cli/generators/generate-hono-server/auth.ts +0 -97
- package/cli/generators/generate-hono-server/imports.ts +0 -55
- package/cli/generators/generate-hono-server/index.ts +0 -52
- package/cli/generators/generate-hono-server/routes.ts +0 -115
- package/cli/generators/generate-hono-server/template.ts +0 -371
- package/cli/generators/generate-hono-server.ts +0 -1
- package/cli/generators/generate-scheduler-handlers/constants.ts +0 -8
- package/cli/generators/generate-scheduler-handlers/handler-entries.ts +0 -22
- package/cli/generators/generate-scheduler-handlers/index.ts +0 -51
- package/cli/generators/generate-scheduler-handlers/runtime-block.ts +0 -68
- package/cli/generators/generate-scheduler-handlers/scheduler-handlers-block.ts +0 -2
- package/cli/generators/generate-scheduler-handlers/type-helpers-block.ts +0 -68
- package/cli/generators/generate-scheduler-handlers.ts +0 -1
- package/cli/generators/generate-websocket-durable-object/auth.ts +0 -30
- package/cli/generators/generate-websocket-durable-object/imports.ts +0 -55
- package/cli/generators/generate-websocket-durable-object/index.ts +0 -41
- package/cli/generators/generate-websocket-durable-object/query-handlers.ts +0 -18
- package/cli/generators/generate-websocket-durable-object/template.ts +0 -714
- package/cli/generators/generate-websocket-durable-object.ts +0 -1
- package/cli/schema/schema-static-types.ts +0 -702
- package/cli/schema/schema.ts +0 -151
- package/cli/utils/tsc.ts +0 -54
- package/cli/utils/utils.ts +0 -190
- package/cli/utils/zod-utils.ts +0 -121
- package/lib/README.md +0 -50
- package/lib/db.ts +0 -19
- package/lib/location.ts +0 -110
- package/lib/values.ts +0 -27
- package/react/README.md +0 -67
- package/react/hooks/useMutation.ts +0 -89
- package/react/hooks/usePaginatedQuery.ts +0 -213
- package/react/hooks/useQuery.ts +0 -106
- package/react/shared/queryShared.ts +0 -174
- package/server/README.md +0 -218
- package/server/auth.ts +0 -107
- package/server/database/builders.ts +0 -83
- package/server/database/context.ts +0 -327
- package/server/database/populate.ts +0 -234
- package/server/database/query-builder.ts +0 -161
- package/server/database/query-utils.ts +0 -25
- package/server/db.ts +0 -2
- package/server/storage/auth.ts +0 -16
- package/server/storage/bucket.ts +0 -22
- package/server/storage/context.ts +0 -34
- package/server/storage/index.ts +0 -38
- package/server/storage/operations.ts +0 -149
- package/server/storage/route-handler.ts +0 -60
- package/server/storage/types.ts +0 -55
- package/server/storage/utils.ts +0 -47
- package/server/storage.ts +0 -6
- package/server/types/schema-refs.ts +0 -66
- package/server/types/types.ts +0 -633
- package/server/utils/id-utils.ts +0 -230
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type UseMutationOptions,
|
|
3
|
+
type UseMutationResult,
|
|
4
|
+
useMutation as useTanstackMutation,
|
|
5
|
+
} from "@tanstack/react-query";
|
|
6
|
+
|
|
7
|
+
type AppflareRequestErrorLike = {
|
|
8
|
+
message: string;
|
|
9
|
+
status?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type AppflareRequestResultLike<TData> = {
|
|
13
|
+
data: TData | null;
|
|
14
|
+
error: AppflareRequestErrorLike | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type AppflareMutationLike<TArgs, TData> = {
|
|
18
|
+
run: (
|
|
19
|
+
args: TArgs,
|
|
20
|
+
options?: any,
|
|
21
|
+
) => Promise<AppflareRequestResultLike<TData>>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function toError(error: AppflareRequestErrorLike): Error {
|
|
25
|
+
const next = new Error(error.message);
|
|
26
|
+
if (error.status !== undefined) {
|
|
27
|
+
(next as Error & { status?: number }).status = error.status;
|
|
28
|
+
}
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useMutation<TArgs, TData, TContext = unknown>(
|
|
33
|
+
mutation: AppflareMutationLike<TArgs, TData>,
|
|
34
|
+
args: TArgs,
|
|
35
|
+
mutationOptions?: Omit<
|
|
36
|
+
UseMutationOptions<TData, Error, void, TContext>,
|
|
37
|
+
"mutationFn"
|
|
38
|
+
>,
|
|
39
|
+
): UseMutationResult<TData, Error, void, TContext> {
|
|
40
|
+
return useTanstackMutation<TData, Error, void, TContext>({
|
|
41
|
+
...(mutationOptions as Omit<
|
|
42
|
+
UseMutationOptions<TData, Error, void, TContext>,
|
|
43
|
+
"mutationFn"
|
|
44
|
+
>),
|
|
45
|
+
mutationFn: async () => {
|
|
46
|
+
const response = await mutation.run(args);
|
|
47
|
+
if (response.error) {
|
|
48
|
+
throw toError(response.error);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return response.data as TData;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type QueryKey,
|
|
3
|
+
type UseQueryOptions,
|
|
4
|
+
type UseQueryResult,
|
|
5
|
+
useQuery as useTanstackQuery,
|
|
6
|
+
useQueryClient,
|
|
7
|
+
} from "@tanstack/react-query";
|
|
8
|
+
import { useEffect, useMemo } from "react";
|
|
9
|
+
|
|
10
|
+
type AppflareRequestErrorLike = {
|
|
11
|
+
message: string;
|
|
12
|
+
status?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type AppflareRequestResultLike<TData> = {
|
|
16
|
+
data: TData | null;
|
|
17
|
+
error: AppflareRequestErrorLike | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type AppflareRealtimeSubscriptionLike = {
|
|
21
|
+
remove: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type AppflareRealtimeQueryUpdateLike<TData> = {
|
|
25
|
+
event: "query:update";
|
|
26
|
+
payload: {
|
|
27
|
+
queryName: string;
|
|
28
|
+
signature: string;
|
|
29
|
+
data: TData;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type AppflareQueryLike<TArgs, TData> = {
|
|
34
|
+
run: (
|
|
35
|
+
args: TArgs,
|
|
36
|
+
options?: any,
|
|
37
|
+
) => Promise<AppflareRequestResultLike<TData>>;
|
|
38
|
+
subscribe?: (options: {
|
|
39
|
+
args?: TArgs;
|
|
40
|
+
authToken?: string;
|
|
41
|
+
requestOptions?: any;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
onChange: (
|
|
44
|
+
data: TData,
|
|
45
|
+
update: AppflareRealtimeQueryUpdateLike<TData>,
|
|
46
|
+
) => void;
|
|
47
|
+
onError?: (error: unknown) => void;
|
|
48
|
+
}) => AppflareRealtimeSubscriptionLike;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type UseAppflareQueryOptions<
|
|
52
|
+
TArgs,
|
|
53
|
+
TData,
|
|
54
|
+
TSelected = TData,
|
|
55
|
+
TKey extends QueryKey = QueryKey,
|
|
56
|
+
> = {
|
|
57
|
+
realtime?: {
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
authToken?: string;
|
|
60
|
+
requestOptions?: any;
|
|
61
|
+
onChange?: (
|
|
62
|
+
data: TData,
|
|
63
|
+
update: AppflareRealtimeQueryUpdateLike<TData>,
|
|
64
|
+
) => void;
|
|
65
|
+
onError?: (error: unknown) => void;
|
|
66
|
+
};
|
|
67
|
+
requestOptions?: any;
|
|
68
|
+
queryOptions?: Omit<
|
|
69
|
+
UseQueryOptions<TData, Error, TSelected, TKey>,
|
|
70
|
+
"queryFn" | "queryKey"
|
|
71
|
+
> & {
|
|
72
|
+
queryKey?: TKey;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function toError(error: AppflareRequestErrorLike): Error {
|
|
77
|
+
const next = new Error(error.message);
|
|
78
|
+
if (error.status !== undefined) {
|
|
79
|
+
(next as Error & { status?: number }).status = error.status;
|
|
80
|
+
}
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function useQuery<
|
|
85
|
+
TArgs,
|
|
86
|
+
TData,
|
|
87
|
+
TSelected = TData,
|
|
88
|
+
TKey extends QueryKey = QueryKey,
|
|
89
|
+
>(
|
|
90
|
+
query: AppflareQueryLike<TArgs, TData>,
|
|
91
|
+
args: TArgs,
|
|
92
|
+
options?: UseAppflareQueryOptions<TArgs, TData, TSelected, TKey>,
|
|
93
|
+
): UseQueryResult<TSelected, Error> {
|
|
94
|
+
const queryClient = useQueryClient();
|
|
95
|
+
const resolvedQueryKey = useMemo(() => {
|
|
96
|
+
return (
|
|
97
|
+
options?.queryOptions?.queryKey ??
|
|
98
|
+
(["appflare", "query", query, args] as unknown as TKey)
|
|
99
|
+
);
|
|
100
|
+
}, [args, options?.queryOptions?.queryKey, query]);
|
|
101
|
+
|
|
102
|
+
const result = useTanstackQuery<TData, Error, TSelected, TKey>({
|
|
103
|
+
...(options?.queryOptions as Omit<
|
|
104
|
+
UseQueryOptions<TData, Error, TSelected, TKey>,
|
|
105
|
+
"queryFn" | "queryKey"
|
|
106
|
+
>),
|
|
107
|
+
queryKey: resolvedQueryKey,
|
|
108
|
+
queryFn: async () => {
|
|
109
|
+
const response = await query.run(args, options?.requestOptions);
|
|
110
|
+
if (response.error) {
|
|
111
|
+
throw toError(response.error);
|
|
112
|
+
}
|
|
113
|
+
return response.data as TData;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (options?.realtime?.enabled === false || !query.subscribe) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const subscription = query.subscribe({
|
|
124
|
+
args,
|
|
125
|
+
authToken: options.realtime?.authToken,
|
|
126
|
+
requestOptions: options.realtime?.requestOptions,
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
onChange: (data, update) => {
|
|
129
|
+
queryClient.setQueryData<TData>(resolvedQueryKey, data as TData);
|
|
130
|
+
options.realtime?.onChange?.(
|
|
131
|
+
data as TData,
|
|
132
|
+
update as AppflareRealtimeQueryUpdateLike<TData>,
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
onError: (error) => {
|
|
136
|
+
options.realtime?.onError?.(error);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return () => {
|
|
141
|
+
controller.abort();
|
|
142
|
+
subscription.remove();
|
|
143
|
+
};
|
|
144
|
+
}, [
|
|
145
|
+
args,
|
|
146
|
+
options?.realtime?.authToken,
|
|
147
|
+
options?.realtime?.enabled,
|
|
148
|
+
options?.realtime?.onChange,
|
|
149
|
+
options?.realtime?.onError,
|
|
150
|
+
options?.realtime?.requestOptions,
|
|
151
|
+
query,
|
|
152
|
+
query.subscribe,
|
|
153
|
+
queryClient,
|
|
154
|
+
resolvedQueryKey,
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
}
|
package/schema.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
export type ColumnType = "int" | "string" | "boolean" | "date";
|
|
2
|
+
|
|
3
|
+
export type ColumnBuilderOptions = {
|
|
4
|
+
sqlName?: string;
|
|
5
|
+
length?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type PrimaryKeyOptions = {
|
|
9
|
+
autoIncrement?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type RelationOneOptions = {
|
|
13
|
+
referenceField?: string;
|
|
14
|
+
field?: string;
|
|
15
|
+
fkType?: ColumnType;
|
|
16
|
+
sqlName?: string;
|
|
17
|
+
notNull?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type RelationManyOptions = {
|
|
21
|
+
referenceField?: string;
|
|
22
|
+
field?: string;
|
|
23
|
+
fkType?: ColumnType;
|
|
24
|
+
sqlName?: string;
|
|
25
|
+
notNull?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ColumnReference = {
|
|
29
|
+
table: string;
|
|
30
|
+
column: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type ColumnDefinition = {
|
|
34
|
+
kind: "column";
|
|
35
|
+
type: ColumnType;
|
|
36
|
+
sqlName?: string;
|
|
37
|
+
length?: number;
|
|
38
|
+
notNull?: boolean;
|
|
39
|
+
primaryKey?: boolean;
|
|
40
|
+
autoIncrement?: boolean;
|
|
41
|
+
unique?: true | { name?: string };
|
|
42
|
+
index?: true | { name?: string };
|
|
43
|
+
sqlDefault?: unknown;
|
|
44
|
+
runtimeDefaultFn?: () => unknown;
|
|
45
|
+
references?: ColumnReference;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type OneRelationDefinition = {
|
|
49
|
+
kind: "relation";
|
|
50
|
+
relation: "one";
|
|
51
|
+
targetTable: string;
|
|
52
|
+
field?: string;
|
|
53
|
+
referenceField?: string;
|
|
54
|
+
fkType?: ColumnType;
|
|
55
|
+
sqlName?: string;
|
|
56
|
+
notNull?: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type ManyRelationDefinition = {
|
|
60
|
+
kind: "relation";
|
|
61
|
+
relation: "many";
|
|
62
|
+
targetTable: string;
|
|
63
|
+
field?: string;
|
|
64
|
+
referenceField?: string;
|
|
65
|
+
fkType?: ColumnType;
|
|
66
|
+
sqlName?: string;
|
|
67
|
+
notNull?: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type RelationDefinition = OneRelationDefinition | ManyRelationDefinition;
|
|
71
|
+
|
|
72
|
+
export type TableOptions = {
|
|
73
|
+
sqlName?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type TableShape = Record<string, ColumnBuilder | RelationDefinition>;
|
|
77
|
+
|
|
78
|
+
export type TableDefinition = {
|
|
79
|
+
kind: "table";
|
|
80
|
+
sqlName?: string;
|
|
81
|
+
columns: Record<string, ColumnDefinition>;
|
|
82
|
+
relations: Record<string, RelationDefinition>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type SchemaDefinition = {
|
|
86
|
+
kind: "schema";
|
|
87
|
+
tables: Record<string, TableDefinition>;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export class ColumnBuilder {
|
|
91
|
+
private definition: ColumnDefinition;
|
|
92
|
+
|
|
93
|
+
public constructor(
|
|
94
|
+
typeOrDefinition: ColumnType | ColumnDefinition,
|
|
95
|
+
options: ColumnBuilderOptions = {},
|
|
96
|
+
) {
|
|
97
|
+
if (typeof typeOrDefinition === "string") {
|
|
98
|
+
this.definition = {
|
|
99
|
+
kind: "column",
|
|
100
|
+
type: typeOrDefinition,
|
|
101
|
+
sqlName: options.sqlName,
|
|
102
|
+
length: options.length,
|
|
103
|
+
};
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.definition = {
|
|
108
|
+
...typeOrDefinition,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private with(patch: Partial<ColumnDefinition>): ColumnBuilder {
|
|
113
|
+
return new ColumnBuilder({
|
|
114
|
+
...this.definition,
|
|
115
|
+
...patch,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public sql(name: string): ColumnBuilder {
|
|
120
|
+
return this.with({ sqlName: name });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public notNull(): ColumnBuilder {
|
|
124
|
+
return this.with({ notNull: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public primaryKey(options: PrimaryKeyOptions = {}): ColumnBuilder {
|
|
128
|
+
return this.with({
|
|
129
|
+
primaryKey: true,
|
|
130
|
+
autoIncrement: options.autoIncrement ?? this.definition.autoIncrement,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public unique(name?: string): ColumnBuilder {
|
|
135
|
+
return this.with({ unique: name ? { name } : true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public index(name?: string): ColumnBuilder {
|
|
139
|
+
return this.with({ index: name ? { name } : true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public default(value: unknown): ColumnBuilder {
|
|
143
|
+
return this.with({ sqlDefault: value });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public defaultFn(fn: () => unknown): ColumnBuilder {
|
|
147
|
+
return this.with({ runtimeDefaultFn: fn });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public references(table: string, column = "id"): ColumnBuilder {
|
|
151
|
+
return this.with({ references: { table, column } });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public toDefinition(): ColumnDefinition {
|
|
155
|
+
return { ...this.definition };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function table(
|
|
160
|
+
shape: TableShape,
|
|
161
|
+
options: TableOptions = {},
|
|
162
|
+
): TableDefinition {
|
|
163
|
+
const columns: Record<string, ColumnDefinition> = {};
|
|
164
|
+
const relations: Record<string, RelationDefinition> = {};
|
|
165
|
+
|
|
166
|
+
for (const [fieldName, value] of Object.entries(shape)) {
|
|
167
|
+
if (value instanceof ColumnBuilder) {
|
|
168
|
+
columns[fieldName] = value.toDefinition();
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (value.kind === "relation") {
|
|
173
|
+
relations[fieldName] = value;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Invalid table field '${fieldName}'. Use column builders or relation helpers.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
kind: "table",
|
|
184
|
+
sqlName: options.sqlName,
|
|
185
|
+
columns,
|
|
186
|
+
relations,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function schema(
|
|
191
|
+
tables: Record<string, TableDefinition>,
|
|
192
|
+
): SchemaDefinition {
|
|
193
|
+
return {
|
|
194
|
+
kind: "schema",
|
|
195
|
+
tables,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function isSchemaDefinition(value: unknown): value is SchemaDefinition {
|
|
200
|
+
if (typeof value !== "object" || value === null) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const candidate = value as { kind?: string; tables?: unknown };
|
|
204
|
+
return candidate.kind === "schema" && typeof candidate.tables === "object";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const v = {
|
|
208
|
+
int: (options: ColumnBuilderOptions = {}) =>
|
|
209
|
+
new ColumnBuilder("int", options),
|
|
210
|
+
string: (options: ColumnBuilderOptions = {}) =>
|
|
211
|
+
new ColumnBuilder("string", options),
|
|
212
|
+
boolean: (options: ColumnBuilderOptions = {}) =>
|
|
213
|
+
new ColumnBuilder("boolean", options),
|
|
214
|
+
date: (options: ColumnBuilderOptions = {}) =>
|
|
215
|
+
new ColumnBuilder("date", options),
|
|
216
|
+
one: (
|
|
217
|
+
targetTable: string,
|
|
218
|
+
fieldOrOptions?: string | RelationOneOptions,
|
|
219
|
+
options: RelationOneOptions = {},
|
|
220
|
+
): OneRelationDefinition => {
|
|
221
|
+
const field =
|
|
222
|
+
typeof fieldOrOptions === "string" ? fieldOrOptions : undefined;
|
|
223
|
+
const mergedOptions =
|
|
224
|
+
typeof fieldOrOptions === "string"
|
|
225
|
+
? options
|
|
226
|
+
: (fieldOrOptions ?? options);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
kind: "relation",
|
|
230
|
+
relation: "one",
|
|
231
|
+
targetTable,
|
|
232
|
+
field: mergedOptions.field ?? field,
|
|
233
|
+
referenceField: mergedOptions.referenceField,
|
|
234
|
+
fkType: mergedOptions.fkType,
|
|
235
|
+
sqlName: mergedOptions.sqlName,
|
|
236
|
+
notNull: mergedOptions.notNull,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
many: (
|
|
240
|
+
targetTable: string,
|
|
241
|
+
fieldOrOptions?: string | RelationManyOptions,
|
|
242
|
+
options: RelationManyOptions = {},
|
|
243
|
+
): ManyRelationDefinition => {
|
|
244
|
+
const field =
|
|
245
|
+
typeof fieldOrOptions === "string" ? fieldOrOptions : undefined;
|
|
246
|
+
const mergedOptions =
|
|
247
|
+
typeof fieldOrOptions === "string"
|
|
248
|
+
? options
|
|
249
|
+
: (fieldOrOptions ?? options);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
kind: "relation",
|
|
253
|
+
relation: "many",
|
|
254
|
+
targetTable,
|
|
255
|
+
field: mergedOptions.field ?? field,
|
|
256
|
+
referenceField: mergedOptions.referenceField,
|
|
257
|
+
fkType: mergedOptions.fkType,
|
|
258
|
+
sqlName: mergedOptions.sqlName,
|
|
259
|
+
notNull: mergedOptions.notNull,
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
};
|
package/tsconfig.json
CHANGED
package/cli/README.md
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
# Appflare CLI
|
|
2
|
-
|
|
3
|
-
This folder contains the build toolchain that turns an Appflare project (schema + query/mutation handlers) into a fully generated API surface (typed client, Hono server, websocket durable object, and optional JS/.d.ts emit). The CLI is Bun-based and is intended to run inside a project that exports `appflare.config.ts`.
|
|
4
|
-
|
|
5
|
-
## Command surface
|
|
6
|
-
|
|
7
|
-
- **build**: entrypoint defined in [packages/appflare/cli/index.ts](packages/appflare/cli/index.ts). Generates all artifacts into the configured `outDir` and optionally emits compiled output.
|
|
8
|
-
- `-c, --config <path>`: path to the config file (defaults to `appflare.config.ts`).
|
|
9
|
-
- `--emit`: after generation, run `bunx tsc` with a temporary tsconfig to emit JS and .d.ts into `outDir/dist`.
|
|
10
|
-
- `-w, --watch`: keep the process alive, watch for file changes (excluding `outDir`/`node_modules`/build artifacts), and rerun the build. Recomputes watched paths when the config changes.
|
|
11
|
-
|
|
12
|
-
### Config shape
|
|
13
|
-
|
|
14
|
-
`appflare.config.ts` must default-export an object with:
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
export default {
|
|
18
|
-
dir: "./app", // Root folder containing your handlers
|
|
19
|
-
schema: "./schema.ts", // Path to the Zod schema file
|
|
20
|
-
outDir: "./_generated", // Where generated files are written
|
|
21
|
-
auth: {
|
|
22
|
-
// Optional: Better Auth config forwarded to the generated server
|
|
23
|
-
enabled: false,
|
|
24
|
-
basePath: "/auth",
|
|
25
|
-
options: {},
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
The loader in [packages/appflare/cli/core/config.ts](packages/appflare/cli/core/config.ts) validates presence and types of `dir`, `schema`, and `outDir`, lightly checks optional `auth` (base path, enabled flag, options object), and resolves paths relative to the config file location.
|
|
31
|
-
|
|
32
|
-
## Build pipeline
|
|
33
|
-
|
|
34
|
-
The build orchestrator in [packages/appflare/cli/core/build.ts](packages/appflare/cli/core/build.ts) performs the following steps when `build` runs:
|
|
35
|
-
|
|
36
|
-
1. Resolve absolute paths for `dir`, `schema`, and `outDir`; ensure `dir` and `schema` exist.
|
|
37
|
-
2. Create `outDir/src` and `outDir/server` if missing.
|
|
38
|
-
3. Generate typed schema helpers into `outDir/src/schema-types.ts` using [packages/appflare/cli/schema/schema.ts](packages/appflare/cli/schema/schema.ts). This produces table doc interfaces, `TableNames`, `Id`, query helpers, and convenience types from [packages/appflare/cli/schema/schema-static-types.ts](packages/appflare/cli/schema/schema-static-types.ts).
|
|
39
|
-
4. Generate built-in CRUD handlers for every table into `outDir/src/handlers/<table>.ts` plus an index via [packages/appflare/cli/generators/generate-db-handlers.ts](packages/appflare/cli/generators/generate-db-handlers.ts). These include `find*`, `findOne*`, `insert*`, `update*`, and `delete*` operations backed by the generated schema types.
|
|
40
|
-
5. Discover user-defined handlers under `dir` with [packages/appflare/cli/core/discover-handlers.ts](packages/appflare/cli/core/discover-handlers.ts):
|
|
41
|
-
- Recurses through `.ts` files (excluding `node_modules`, `.git`, `dist`, `build`, and the configured `outDir`).
|
|
42
|
-
- Recognizes handlers declared as `export const <name> = query(` or `export const <name> = mutation(`.
|
|
43
|
-
- Skips the schema file and the config file; deduplicates by kind/file/name.
|
|
44
|
-
6. Generate a typed client at `outDir/src/api.ts` via [packages/appflare/cli/generators/generate-api-client.ts](packages/appflare/cli/generators/generate-api-client.ts):
|
|
45
|
-
- Produces `createAppflareApi()` with `queries` and `mutations` collections keyed by `<file>/<handler>`.
|
|
46
|
-
- Each handler function wraps `fetch` (default `better-fetch`) and carries metadata: Zod schema, websocket helper, and route path.
|
|
47
|
-
- Realtime helpers build websocket URLs for subscriptions, normalizing `ws`/`wss` bases and providing hooks (`onOpen`, `onMessage`, `onData`, etc.).
|
|
48
|
-
7. Generate a Hono server at `outDir/server/server.ts` with [packages/appflare/cli/generators/generate-hono-server.ts](packages/appflare/cli/generators/generate-hono-server.ts):
|
|
49
|
-
- Routes: `GET /queries/<file>/<name>` and `POST /mutations/<file>/<name>`.
|
|
50
|
-
- Uses `@hono/standard-validator` + Zod arg schemas and wraps Mongo via `createMongoDbContext` from `appflare/server/db`.
|
|
51
|
-
- Supports optional mutation notifications for realtime (custom notifier or Durable Object hook).
|
|
52
|
-
8. Generate a websocket Durable Object shim at `outDir/server/websocket-hibernation-server.ts` via [packages/appflare/cli/generators/generate-websocket-durable-object.ts](packages/appflare/cli/generators/generate-websocket-durable-object.ts):
|
|
53
|
-
- Implements `WebSocketHibernationServer` to handle subscriptions at `/ws` and mutation notifications at `/notify`.
|
|
54
|
-
- Selects a default query handler per table (or a specific handler) and re-fetches data on mutation notifications, emitting `data` messages to subscribers.
|
|
55
|
-
9. If `--emit` is set, remove any previous `outDir/dist`, write a temporary tsconfig (includes generated schema types and handlers only), and run `bunx tsc` to emit JS + .d.ts into `outDir/dist` (logic in [packages/appflare/cli/utils/tsc.ts](packages/appflare/cli/utils/tsc.ts)).
|
|
56
|
-
|
|
57
|
-
## Handler authoring guidelines
|
|
58
|
-
|
|
59
|
-
- Handlers must be exported as `query({ args, handler })` or `mutation({ args, handler })` objects.
|
|
60
|
-
- Filenames become the first route/path segment and grouping key in the client (`<file>/<handler>`).
|
|
61
|
-
- Arguments are validated with Zod; the client infers optional vs required keys and provides typed `args` for both client and server.
|
|
62
|
-
- The discovery step ignores `.d.ts` files and anything outside the configured `dir`.
|
|
63
|
-
|
|
64
|
-
## Generated layout (relative to `outDir`)
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
src/
|
|
68
|
-
schema-types.ts # Typed schema exports, helpers, and Zod-powered validator types
|
|
69
|
-
handlers/ # Auto CRUD handlers per table + index
|
|
70
|
-
<table>.ts
|
|
71
|
-
index.ts
|
|
72
|
-
api.ts # Typed queries/mutations client with realtime helpers
|
|
73
|
-
server/
|
|
74
|
-
server.ts # Hono server that wires handlers + Mongo context
|
|
75
|
-
websocket-hibernation-server.ts # Durable Object websocket bridge
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Realtime and Durable Object flow
|
|
79
|
-
|
|
80
|
-
- Client websockets are created via `handler.websocket(args?, options?)`, building URLs against `realtime.baseUrl` (or handler override) and defaulting the `table` and `handler` params based on the handler’s file/name.
|
|
81
|
-
- The Durable Object handles `/ws` upgrades, parses subscription params (`table`, `handler`, `where`, `orderBy`, `take`, `skip`, `select`, `include`, `args`), and caches subscriptions. On `/notify` payloads, it re-runs the relevant query or table fetch and pushes a `data` message to connected sockets.
|
|
82
|
-
- Mutation notifications can be sent by the generated Hono server (if `realtime.notify` or `realtime.durableObject` is provided) so subscriptions stay in sync.
|
|
83
|
-
|
|
84
|
-
## Error handling and safeguards
|
|
85
|
-
|
|
86
|
-
- Config, schema, and project directories are validated before generation; missing paths throw with readable errors.
|
|
87
|
-
- Build de-duplicates discovered handlers to avoid duplicate route generation.
|
|
88
|
-
- `--emit` uses a scoped tsconfig that only references generated files to prevent user code outside `rootDir` from breaking emit.
|
|
89
|
-
- Generated files include `/* eslint-disable */` headers to avoid lint noise.
|
|
90
|
-
|
|
91
|
-
## Typical usage
|
|
92
|
-
|
|
93
|
-
```sh
|
|
94
|
-
# From the repo root (config defaults to ./appflare.config.ts)
|
|
95
|
-
bunx appflare build
|
|
96
|
-
|
|
97
|
-
# Custom config location and emit compiled JS
|
|
98
|
-
bunx appflare build --config ./config/appflare.config.ts --emit
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
After running, import the generated client/server:
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
import { createAppflareApi } from "./_generated/src/api";
|
|
105
|
-
import server from "./_generated/server/server";
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Use the client in web/React or server contexts, and deploy the generated Hono server + Durable Object to your runtime of choice (e.g., Cloudflare Workers with Mongo).
|