appflare 0.2.37 → 0.2.39
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/generate.ts +47 -0
- package/cli/templates/auth/config.ts +2 -2
- package/cli/templates/handlers/generators/handlers.ts +3 -2
- package/cli/templates/handlers/generators/types/context.ts +30 -7
- package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +82 -2
- package/cli/templates/handlers/index.ts +4 -1
- package/cli/templates/handlers/types.ts +4 -2
- package/dist/cli/index.js +182 -93
- package/dist/cli/index.mjs +182 -93
- package/package.json +1 -1
package/cli/generate.ts
CHANGED
|
@@ -36,6 +36,46 @@ function extractRolesFromConfig(config: LoadedAppflareConfig["config"]): string[
|
|
|
36
36
|
return [];
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
type AdditionalField = { name: string; tsType: string };
|
|
40
|
+
|
|
41
|
+
function extractAdditionalFields(config: LoadedAppflareConfig["config"]): AdditionalField[] {
|
|
42
|
+
const options = config.auth.options as Record<string, unknown> | undefined;
|
|
43
|
+
const userSection = options?.user as Record<string, unknown> | undefined;
|
|
44
|
+
const raw = userSection?.additionalFields as Record<string, { type: string }> | undefined;
|
|
45
|
+
if (!raw || typeof raw !== "object") return [];
|
|
46
|
+
|
|
47
|
+
const typeMap: Record<string, string> = {
|
|
48
|
+
string: "string",
|
|
49
|
+
number: "number",
|
|
50
|
+
boolean: "boolean",
|
|
51
|
+
date: "Date",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return Object.entries(raw)
|
|
55
|
+
.filter(([, v]) => v && typeof v === "object" && "type" in v)
|
|
56
|
+
.map(([name, v]) => ({
|
|
57
|
+
name,
|
|
58
|
+
tsType: typeMap[(v as { type: string }).type] || "unknown",
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function patchAuthSchemaRole(schemaPath: string, roles: string[]): Promise<void> {
|
|
63
|
+
const content = await Bun.file(schemaPath).text();
|
|
64
|
+
const roleType = roles.map((r) => `"${r}"`).join(" | ");
|
|
65
|
+
|
|
66
|
+
const updated = content
|
|
67
|
+
.replace(
|
|
68
|
+
/(import.*?from\s+["']drizzle-orm\/sqlite-core["'])/,
|
|
69
|
+
"$1\nimport { customType } from \"drizzle-orm/sqlite-core\"",
|
|
70
|
+
)
|
|
71
|
+
.replace(
|
|
72
|
+
/role:\s*text\(["']role["']\)/,
|
|
73
|
+
`role: customType<{ data: ${roleType}; dataNotNull: ${roleType} }>({ dataType: () => "text" })("role")`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await Bun.write(schemaPath, updated);
|
|
77
|
+
}
|
|
78
|
+
|
|
39
79
|
function toConfigRelativePath(configDir: string, absolutePath: string): string {
|
|
40
80
|
const relativePath = relative(configDir, absolutePath).replace(/\\/g, "/");
|
|
41
81
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
@@ -96,6 +136,7 @@ export async function generateArtifacts(
|
|
|
96
136
|
);
|
|
97
137
|
|
|
98
138
|
const roles = extractRolesFromConfig(config);
|
|
139
|
+
const additionalFields = extractAdditionalFields(config);
|
|
99
140
|
|
|
100
141
|
const serverSource = generateServerSource(
|
|
101
142
|
config.auth.basePath,
|
|
@@ -118,6 +159,7 @@ export async function generateArtifacts(
|
|
|
118
159
|
discoveredHandlers,
|
|
119
160
|
config.r2[0]?.binding,
|
|
120
161
|
roles,
|
|
162
|
+
additionalFields,
|
|
121
163
|
);
|
|
122
164
|
const authConfigSource = generateAuthConfigSource(configImport);
|
|
123
165
|
const drizzleSchemaPaths = compiledSchema
|
|
@@ -201,6 +243,11 @@ export async function generateArtifacts(
|
|
|
201
243
|
}
|
|
202
244
|
process.stdout.write(`🔐 Auth schema (${(performance.now() - t0).toFixed(0)}ms)\n`);
|
|
203
245
|
|
|
246
|
+
if (roles.length > 0) {
|
|
247
|
+
await patchAuthSchemaRole(authSchemaPath, roles);
|
|
248
|
+
process.stdout.write(`🔧 Patched role type (${(performance.now() - t0).toFixed(0)}ms)\n`);
|
|
249
|
+
}
|
|
250
|
+
|
|
204
251
|
function generatedTsconfig(overrides: Record<string, unknown> = {}) {
|
|
205
252
|
return {
|
|
206
253
|
compilerOptions: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { generateTypes } from "../types";
|
|
2
|
+
import type { AdditionalField } from "../types";
|
|
2
3
|
|
|
3
|
-
export function generateHandlersSource(schemaImportPath: string, roles: string[] = []): string {
|
|
4
|
+
export function generateHandlersSource(schemaImportPath: string, roles: string[] = [], additionalFields: AdditionalField[] = []): string {
|
|
4
5
|
return `import type { Context } from "hono";
|
|
5
6
|
import type { D1Database } from "@cloudflare/workers-types";
|
|
6
7
|
import { drizzle } from "drizzle-orm/d1";
|
|
@@ -8,6 +9,6 @@ import { z, type ZodRawShape } from "zod";
|
|
|
8
9
|
import * as authSchema from "./auth.schema";
|
|
9
10
|
import * as schema from "${schemaImportPath}";
|
|
10
11
|
|
|
11
|
-
${generateTypes(roles)}
|
|
12
|
+
${generateTypes(roles, additionalFields)}
|
|
12
13
|
`;
|
|
13
14
|
}
|
|
@@ -1,13 +1,36 @@
|
|
|
1
|
-
export function generateTypesContextSection(
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
export function generateTypesContextSection(
|
|
2
|
+
roles: string[] = [],
|
|
3
|
+
additionalFields: Array<{ name: string; tsType: string }> = [],
|
|
4
|
+
): string {
|
|
5
|
+
const additionalFieldLines = additionalFields.map(
|
|
6
|
+
(f) => `\t${f.name}?: ${f.tsType} | null;`,
|
|
7
|
+
).join("\n");
|
|
8
|
+
|
|
9
|
+
const adminFieldLines = roles.length > 0
|
|
10
|
+
? [
|
|
11
|
+
"\trole: UserRole;",
|
|
12
|
+
"\tbanned: boolean | null;",
|
|
13
|
+
"\tbanReason?: string | null | undefined;",
|
|
14
|
+
"\tbanExpires?: Date | null | undefined;",
|
|
15
|
+
].join("\n")
|
|
16
|
+
: "";
|
|
17
|
+
|
|
18
|
+
const userFields = [adminFieldLines, additionalFieldLines].filter(Boolean).join("\n");
|
|
19
|
+
|
|
20
|
+
const userType = roles.length > 0
|
|
21
|
+
? `type UserRole = ${roles.map((r) => `"${r}"`).join(" | ")};
|
|
22
|
+
type User = import("better-auth").User & {
|
|
23
|
+
${userFields}
|
|
24
|
+
};`
|
|
25
|
+
: additionalFields.length > 0
|
|
26
|
+
? `type User = import("better-auth").User & {
|
|
27
|
+
${userFields}
|
|
28
|
+
};`
|
|
29
|
+
: "type User = import(\"better-auth\").User;";
|
|
7
30
|
|
|
8
31
|
return `type AuthSession = typeof auth.$Infer.Session;
|
|
9
32
|
type AuthAdapter = Awaited<typeof auth.$context>["internalAdapter"];
|
|
10
|
-
${
|
|
33
|
+
${userType}
|
|
11
34
|
type Session = AuthSession['session']
|
|
12
35
|
|
|
13
36
|
export type StoragePutArgs = {
|
|
@@ -55,6 +55,86 @@ function buildJsonFieldFilter(
|
|
|
55
55
|
return undefined;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function buildJsonArrayElementMatchCondition(
|
|
59
|
+
field: unknown,
|
|
60
|
+
matchValue: unknown,
|
|
61
|
+
): SQL {
|
|
62
|
+
if (!isRecord(matchValue) || matchValue instanceof Date || Array.isArray(matchValue)) {
|
|
63
|
+
return sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE j.value = \${matchValue})\`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const propConditions: SQL[] = [];
|
|
67
|
+
for (const [key, propValue] of Object.entries(matchValue)) {
|
|
68
|
+
const jsonPath = '$.' + key;
|
|
69
|
+
if (!isRecord(propValue) || propValue instanceof Date || Array.isArray(propValue)) {
|
|
70
|
+
propConditions.push(
|
|
71
|
+
sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE json_extract(j.value, \${jsonPath}) = \${propValue})\`,
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
const nestedConditions: SQL[] = [];
|
|
75
|
+
const eqVal = readOperatorValue(propValue, "eq");
|
|
76
|
+
if (eqVal !== undefined) {
|
|
77
|
+
nestedConditions.push(
|
|
78
|
+
sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE json_extract(j.value, \${jsonPath}) = \${eqVal})\`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const neVal = readOperatorValue(propValue, "ne");
|
|
82
|
+
if (neVal !== undefined) {
|
|
83
|
+
nestedConditions.push(
|
|
84
|
+
sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE json_extract(j.value, \${jsonPath}) != \${neVal})\`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const gtVal = readOperatorValue(propValue, "gt");
|
|
88
|
+
if (gtVal !== undefined) {
|
|
89
|
+
nestedConditions.push(
|
|
90
|
+
sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE json_extract(j.value, \${jsonPath}) > \${gtVal})\`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const gteVal = readOperatorValue(propValue, "gte");
|
|
94
|
+
if (gteVal !== undefined) {
|
|
95
|
+
nestedConditions.push(
|
|
96
|
+
sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE json_extract(j.value, \${jsonPath}) >= \${gteVal})\`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const ltVal = readOperatorValue(propValue, "lt");
|
|
100
|
+
if (ltVal !== undefined) {
|
|
101
|
+
nestedConditions.push(
|
|
102
|
+
sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE json_extract(j.value, \${jsonPath}) < \${ltVal})\`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const lteVal = readOperatorValue(propValue, "lte");
|
|
106
|
+
if (lteVal !== undefined) {
|
|
107
|
+
nestedConditions.push(
|
|
108
|
+
sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE json_extract(j.value, \${jsonPath}) <= \${lteVal})\`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const inVal = readOperatorValue(propValue, "in");
|
|
112
|
+
if (Array.isArray(inVal) && inVal.length > 0) {
|
|
113
|
+
nestedConditions.push(
|
|
114
|
+
sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE json_extract(j.value, \${jsonPath}) IN (\${sql.join(inVal.map((v) => sql\`\${v}\`), sql\`, \`)})\`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (nestedConditions.length > 0) {
|
|
118
|
+
propConditions.push(
|
|
119
|
+
nestedConditions.length === 1
|
|
120
|
+
? nestedConditions[0]
|
|
121
|
+
: and(...nestedConditions),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (propConditions.length === 0) {
|
|
128
|
+
return sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE json_type(j.value) = 'object')\`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (propConditions.length === 1) {
|
|
132
|
+
return propConditions[0];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return and(...propConditions);
|
|
136
|
+
}
|
|
137
|
+
|
|
58
138
|
function buildJsonArrayFilter(
|
|
59
139
|
fieldName: string,
|
|
60
140
|
field: unknown,
|
|
@@ -69,7 +149,7 @@ function buildJsonArrayFilter(
|
|
|
69
149
|
const includesValue = readOperatorValue(value, "includes");
|
|
70
150
|
if (Array.isArray(includesValue) && includesValue.length > 0) {
|
|
71
151
|
const conditions = includesValue.map((v) =>
|
|
72
|
-
|
|
152
|
+
buildJsonArrayElementMatchCondition(field, v),
|
|
73
153
|
);
|
|
74
154
|
filters.push(and(...conditions));
|
|
75
155
|
}
|
|
@@ -77,7 +157,7 @@ function buildJsonArrayFilter(
|
|
|
77
157
|
const includesAnyValue = readOperatorValue(value, "includesAny");
|
|
78
158
|
if (Array.isArray(includesAnyValue) && includesAnyValue.length > 0) {
|
|
79
159
|
const conditions = includesAnyValue.map((v) =>
|
|
80
|
-
|
|
160
|
+
buildJsonArrayElementMatchCondition(field, v),
|
|
81
161
|
);
|
|
82
162
|
filters.push(sql\`(\${sql.join(conditions, sql\` OR \`)})\`);
|
|
83
163
|
}
|
|
@@ -4,6 +4,8 @@ import { generateExecutionSource } from "./generators/execution";
|
|
|
4
4
|
import { generateHandlersSource } from "./generators/handlers";
|
|
5
5
|
import { generateRegistration } from "./registration";
|
|
6
6
|
|
|
7
|
+
export type AdditionalField = { name: string; tsType: string };
|
|
8
|
+
|
|
7
9
|
export type GeneratedHandlerArtifact = {
|
|
8
10
|
relativePath: string;
|
|
9
11
|
source: string;
|
|
@@ -14,8 +16,9 @@ export function generateHandlersArtifacts(
|
|
|
14
16
|
operations: DiscoveredHandlerOperation[],
|
|
15
17
|
defaultR2Binding?: string,
|
|
16
18
|
roles: string[] = [],
|
|
19
|
+
additionalFields: AdditionalField[] = [],
|
|
17
20
|
): GeneratedHandlerArtifact[] {
|
|
18
|
-
const handlersSource = generateHandlersSource(schemaImportPath, roles);
|
|
21
|
+
const handlersSource = generateHandlersSource(schemaImportPath, roles, additionalFields);
|
|
19
22
|
|
|
20
23
|
const contextSource = generateContextSource(defaultR2Binding);
|
|
21
24
|
|
|
@@ -4,12 +4,14 @@ import { generateTypesQueryRuntimeSection } from "./generators/types/query-runti
|
|
|
4
4
|
import { generateTypesContextSection } from "./generators/types/context";
|
|
5
5
|
import { generateTypesOperationsSection } from "./generators/types/operations";
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export type AdditionalField = { name: string; tsType: string };
|
|
8
|
+
|
|
9
|
+
export function generateTypes(roles: string[] = [], additionalFields: AdditionalField[] = []): string {
|
|
8
10
|
return [
|
|
9
11
|
generateTypesCoreSection(),
|
|
10
12
|
generateTypesQueryDefinitionsSection(),
|
|
11
13
|
generateTypesQueryRuntimeSection(),
|
|
12
|
-
generateTypesContextSection(roles),
|
|
14
|
+
generateTypesContextSection(roles, additionalFields),
|
|
13
15
|
generateTypesOperationsSection(),
|
|
14
16
|
].join("\n\n");
|
|
15
17
|
}
|