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 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: {
@@ -41,11 +41,11 @@ export const createAuth = (
41
41
  : undefined,
42
42
  kv: env?.KV,
43
43
  },
44
- config.auth.options as any,
44
+ config.auth.options,
45
45
  );
46
46
 
47
47
  return betterAuth({
48
- ...(cloudflareOptions as any),
48
+ ...cloudflareOptions,
49
49
  ...(env
50
50
  ? {}
51
51
  : {
@@ -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(roles: string[] = []): string {
2
- const userTypeExtension =
3
- roles.length > 0
4
- ? `type UserRole = ${roles.map((r) => `"${r}"`).join(" | ")};
5
- type User = Omit<AuthSession['user'], 'role'> & { role: UserRole };`
6
- : `type User = AuthSession['user']`;
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
- ${userTypeExtension}
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
- sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE j.value = \${v})\`,
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
- sql\`EXISTS (SELECT 1 FROM json_each(\${field as never}) j WHERE j.value = \${v})\`,
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 function generateTypes(roles: string[] = []): string {
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
  }