appflare 0.2.35 → 0.2.37

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/Documentation.md CHANGED
@@ -398,14 +398,14 @@ bun run build
398
398
 
399
399
  What gets generated (core set):
400
400
 
401
- - `_generated/server.ts`
402
- - `_generated/client.ts`
403
- - `_generated/auth.config.ts`
404
- - `_generated/drizzle.config.ts`
405
- - `_generated/handlers.ts`
406
- - `_generated/handlers.context.ts`
407
- - `_generated/handlers.execution.ts`
408
- - `_generated/handlers.routes.ts`
401
+ - `_generated/server.js`
402
+ - `_generated/client.js`
403
+ - `_generated/auth.config.js`
404
+ - `_generated/drizzle.config.js`
405
+ - `_generated/handlers.js`
406
+ - `_generated/handlers.context.js`
407
+ - `_generated/handlers.execution.js`
408
+ - `_generated/handlers.routes.js`
409
409
  - `_generated/client/**`
410
410
 
411
411
  ### Watch mode
@@ -121,7 +121,7 @@ export async function runMigrate(
121
121
 
122
122
  const drizzleConfigPath = resolve(
123
123
  loadedConfig.outDirAbs,
124
- "drizzle.config.ts",
124
+ "drizzle.config.js",
125
125
  );
126
126
  const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx";
127
127
  const drizzleGenerate = Bun.spawn(
package/cli/generate.ts CHANGED
@@ -15,6 +15,27 @@ import { discoverHandlerOperations } from "./utils/handler-discovery";
15
15
  import { discoverSchema } from "./utils/schema-discovery";
16
16
  import { ensureRelativeImportPath } from "./utils/path-utils";
17
17
 
18
+ function extractRolesFromConfig(config: LoadedAppflareConfig["config"]): string[] {
19
+ const plugins = (config.auth.options as Record<string, unknown>).plugins;
20
+ if (!Array.isArray(plugins)) return [];
21
+
22
+ for (const plugin of plugins) {
23
+ if (plugin && typeof plugin === "object") {
24
+ const pluginObj = plugin as Record<string, unknown>;
25
+ if (pluginObj.id === "admin" && "options" in pluginObj) {
26
+ const options = (pluginObj as Record<string, unknown>).options as Record<string, unknown> | undefined;
27
+ if (options && typeof options === "object" && "roles" in options) {
28
+ const rolesObj = options.roles as Record<string, unknown>;
29
+ if (rolesObj && typeof rolesObj === "object") {
30
+ return Object.keys(rolesObj);
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ return [];
37
+ }
38
+
18
39
  function toConfigRelativePath(configDir: string, absolutePath: string): string {
19
40
  const relativePath = relative(configDir, absolutePath).replace(/\\/g, "/");
20
41
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
@@ -74,6 +95,8 @@ export async function generateArtifacts(
74
95
  `🔍 ${discoveredHandlers.length} handler(s) (${(performance.now() - t0).toFixed(0)}ms)\n`,
75
96
  );
76
97
 
98
+ const roles = extractRolesFromConfig(config);
99
+
77
100
  const serverSource = generateServerSource(
78
101
  config.auth.basePath,
79
102
  config.database[0].binding,
@@ -94,6 +117,7 @@ export async function generateArtifacts(
94
117
  schemaImport,
95
118
  discoveredHandlers,
96
119
  config.r2[0]?.binding,
120
+ roles,
97
121
  );
98
122
  const authConfigSource = generateAuthConfigSource(configImport);
99
123
  const drizzleSchemaPaths = compiledSchema
package/cli/index.ts CHANGED
@@ -13,7 +13,7 @@ program
13
13
  program
14
14
  .command("build")
15
15
  .description(
16
- "Generate server.ts, client.ts, auth.config.ts, drizzle.config.ts, and wrangler.json artifacts",
16
+ "Generate server.ts, client.ts, auth.config.js, drizzle.config.js, and wrangler.json artifacts",
17
17
  )
18
18
  .option(
19
19
  "-c, --config <path>",
@@ -4,6 +4,7 @@ import { pathToFileURL } from "node:url";
4
4
  import type {
5
5
  ColumnDefinition,
6
6
  ColumnType,
7
+ JsonShape,
7
8
  ManyRelationDefinition,
8
9
  ManyToManyRelationDefinition,
9
10
  OneRelationDefinition,
@@ -96,6 +97,11 @@ function cloneSchemaDefinition(definition: SchemaDefinition): SchemaDefinition {
96
97
  ];
97
98
  }),
98
99
  ),
100
+ enums: Object.fromEntries(
101
+ Object.entries(definition.enums ?? {}).map(([name, enumDef]) => {
102
+ return [name, { ...enumDef }];
103
+ }),
104
+ ),
99
105
  };
100
106
  }
101
107
 
@@ -658,6 +664,23 @@ function drizzleBaseColumn(
658
664
  return 't.int({ mode: "timestamp_ms" })';
659
665
  }
660
666
 
667
+ if (column.type === "enum" && column.enumValues && column.enumValues.length > 0) {
668
+ if (column.isArray) {
669
+ return needsExplicitName
670
+ ? `t.text(${quote(resolvedSqlName)}).array()`
671
+ : "t.text().array()";
672
+ }
673
+ return needsExplicitName ? `t.text(${quote(resolvedSqlName)})` : "t.text()";
674
+ }
675
+
676
+ if (column.type === "json" && column.jsonShape) {
677
+ const tsType = jsonShapeToTypeScript(column.jsonShape);
678
+ const base = needsExplicitName
679
+ ? `t.text(${quote(resolvedSqlName)}, { mode: "json" })`
680
+ : `t.text({ mode: "json" })`;
681
+ return `${base}.$type<${tsType}>()`;
682
+ }
683
+
661
684
  if (strategy === "camelToSnake") {
662
685
  return needsExplicitName ? `t.text(${quote(resolvedSqlName)})` : "t.text()";
663
686
  }
@@ -730,6 +753,53 @@ function resolveColumnReference(
730
753
  };
731
754
  }
732
755
 
756
+ function emitJsonColumnsMetadata(definition: SchemaDefinition): string {
757
+ const tableEntries: string[] = [];
758
+
759
+ for (const [tableName, table] of Object.entries(definition.tables)) {
760
+ const columnEntries: string[] = [];
761
+
762
+ for (const [fieldName, column] of Object.entries(table.columns)) {
763
+ if (column.type !== "json" || !column.jsonShape) continue;
764
+
765
+ const shapeStr = jsonShapeToRuntimeLiteral(column.jsonShape);
766
+ columnEntries.push(
767
+ `${quote(fieldName)}: { shape: ${shapeStr} },`,
768
+ );
769
+ }
770
+
771
+ if (columnEntries.length === 0) continue;
772
+
773
+ tableEntries.push(
774
+ `${quote(tableName)}: {
775
+ ${columnEntries.map((entry) => `\t\t${entry}`).join("\n")}
776
+ },`,
777
+ );
778
+ }
779
+
780
+ if (tableEntries.length === 0) {
781
+ return "export const __appflareJsonColumns = {} as const;\n";
782
+ }
783
+
784
+ return `export const __appflareJsonColumns = {
785
+ ${tableEntries.map((entry) => `\t${entry}`).join("\n")}
786
+ } as const;
787
+ `;
788
+ }
789
+
790
+ function jsonShapeToRuntimeLiteral(shape: JsonShape): string {
791
+ if (shape.kind === "array") {
792
+ return `{ kind: "array", element: ${jsonShapeToRuntimeLiteral(shape.element)} }`;
793
+ }
794
+ if (shape.kind === "object") {
795
+ const fields = Object.entries(shape.shape)
796
+ .map(([key, fieldShape]) => `${quote(key)}: ${jsonShapeToRuntimeLiteral(fieldShape)}`)
797
+ .join(", ");
798
+ return `{ kind: "object", shape: { ${fields} } }`;
799
+ }
800
+ return `{ kind: ${quote(shape.kind)} }`;
801
+ }
802
+
733
803
  function emitManyToManyRuntimeMetadata(definition: SchemaDefinition): string {
734
804
  const tableEntries: string[] = [];
735
805
 
@@ -861,6 +931,33 @@ function emitDrizzleSchema(
861
931
  }
862
932
  }
863
933
 
934
+ const enumColumns = new Map<string, ColumnDefinition>();
935
+ for (const table of Object.values(definition.tables)) {
936
+ for (const [fieldName, column] of Object.entries(table.columns)) {
937
+ if (
938
+ column.type === "enum" &&
939
+ column.enumValues &&
940
+ column.enumValues.length > 0
941
+ ) {
942
+ const key = column.enumRef ?? `${fieldName}`;
943
+ if (!enumColumns.has(key)) {
944
+ enumColumns.set(key, column);
945
+ }
946
+ }
947
+ }
948
+ }
949
+
950
+ const enumTypeLines: string[] = [];
951
+ const enumCustomTypeLines: string[] = [];
952
+ for (const [key, column] of enumColumns.entries()) {
953
+ const typeName = toPascalCase(key);
954
+ const valuesStr = column.enumValues!.map((v) => `"${v}"`).join(" | ");
955
+ enumTypeLines.push(`export type ${typeName} = ${valuesStr};`);
956
+ enumCustomTypeLines.push(
957
+ `export const ${typeName}Column = t.customType<{ data: ${typeName}; dataNotNull: ${typeName} }>({ dataType: () => "text" });`,
958
+ );
959
+ }
960
+
864
961
  const tableBlocks: string[] = [];
865
962
  const relationBlocks: string[] = [];
866
963
 
@@ -870,7 +967,25 @@ function emitDrizzleSchema(
870
967
  const indexes: string[] = [];
871
968
 
872
969
  for (const [fieldName, column] of Object.entries(table.columns)) {
873
- let expr = drizzleBaseColumn(fieldName, column, strategy);
970
+ let expr: string;
971
+ if (
972
+ column.type === "enum" &&
973
+ column.enumValues &&
974
+ column.enumValues.length > 0
975
+ ) {
976
+ const enumKey = column.enumRef ?? fieldName;
977
+ const typeName = toPascalCase(enumKey);
978
+ const resolvedSqlName = column.sqlName ?? toSnakeCase(fieldName);
979
+ const needsExplicitName = resolvedSqlName !== fieldName;
980
+ expr = needsExplicitName
981
+ ? `${typeName}Column(${quote(resolvedSqlName)})`
982
+ : `${typeName}Column()`;
983
+ if (column.isArray) {
984
+ expr += ".array()";
985
+ }
986
+ } else {
987
+ expr = drizzleBaseColumn(fieldName, column, strategy);
988
+ }
874
989
 
875
990
  if (column.uuidPrimaryKey) {
876
991
  expr += ".$defaultFn(() => crypto.randomUUID())";
@@ -987,16 +1102,38 @@ function emitDrizzleSchema(
987
1102
  import { sqliteTable as table } from "drizzle-orm/sqlite-core";
988
1103
  import { relations } from "drizzle-orm";
989
1104
  ${buildExternalTableImportLines(externalTables)}
1105
+ ${enumTypeLines.join("\n")}
1106
+ ${enumCustomTypeLines.join("\n")}
1107
+
990
1108
  ${tableBlocks.join("\n\n")}
991
1109
 
992
1110
  ${relationBlocks.join("\n\n")}
993
1111
 
1112
+ ${emitJsonColumnsMetadata(definition)}
1113
+
994
1114
  ${emitManyToManyRuntimeMetadata(definition)}
995
1115
 
996
1116
  ${emitRuntimeRelationMetadata(definition)}
997
1117
  `;
998
1118
  }
999
1119
 
1120
+ function jsonShapeToZod(shape: JsonShape): string {
1121
+ if (shape.kind === "array") {
1122
+ return `z.array(${jsonShapeToZod(shape.element)})`;
1123
+ }
1124
+ if (shape.kind === "object") {
1125
+ const fields = Object.entries(shape.shape)
1126
+ .map(([key, fieldShape]) => `${quote(key)}: ${jsonShapeToZod(fieldShape)}`)
1127
+ .join(", ");
1128
+ return `z.object({ ${fields} })`;
1129
+ }
1130
+ if (shape.kind === "string") return "z.string()";
1131
+ if (shape.kind === "number") return "z.number()";
1132
+ if (shape.kind === "boolean") return "z.boolean()";
1133
+ if (shape.kind === "date") return "z.date()";
1134
+ return "z.unknown()";
1135
+ }
1136
+
1000
1137
  function zodSchemaExpression(
1001
1138
  column: ColumnDefinition,
1002
1139
  optional: boolean,
@@ -1014,6 +1151,12 @@ function zodSchemaExpression(
1014
1151
  expr = "z.boolean()";
1015
1152
  } else if (column.type === "date") {
1016
1153
  expr = "z.date()";
1154
+ } else if (column.type === "enum" && column.enumValues && column.enumValues.length > 0) {
1155
+ const valuesStr = column.enumValues.map((v) => `"${v}"`).join(", ");
1156
+ const enumZod = `z.enum([${valuesStr}])`;
1157
+ expr = column.isArray ? `z.array(${enumZod})` : enumZod;
1158
+ } else if (column.type === "json" && column.jsonShape) {
1159
+ expr = jsonShapeToZod(column.jsonShape);
1017
1160
  }
1018
1161
 
1019
1162
  if (optional) {
@@ -1057,6 +1200,23 @@ export type ${pascal}Select = z.infer<typeof ${tableName}SelectSchema>;
1057
1200
  ${blocks.join("\n")}`;
1058
1201
  }
1059
1202
 
1203
+ function jsonShapeToTypeScript(shape: JsonShape): string {
1204
+ if (shape.kind === "array") {
1205
+ return `Array<${jsonShapeToTypeScript(shape.element)}>`;
1206
+ }
1207
+ if (shape.kind === "object") {
1208
+ const fields = Object.entries(shape.shape)
1209
+ .map(([key, fieldShape]) => `${key}: ${jsonShapeToTypeScript(fieldShape)}`)
1210
+ .join("; ");
1211
+ return `{ ${fields} }`;
1212
+ }
1213
+ if (shape.kind === "string") return "string";
1214
+ if (shape.kind === "number") return "number";
1215
+ if (shape.kind === "boolean") return "boolean";
1216
+ if (shape.kind === "date") return "Date";
1217
+ return "unknown";
1218
+ }
1219
+
1060
1220
  function toTypeScriptType(column: ColumnDefinition): string {
1061
1221
  if (column.type === "int") {
1062
1222
  return "number";
@@ -1070,10 +1230,27 @@ function toTypeScriptType(column: ColumnDefinition): string {
1070
1230
  if (column.type === "date") {
1071
1231
  return "Date";
1072
1232
  }
1233
+ if (column.type === "enum" && column.enumValues && column.enumValues.length > 0) {
1234
+ const union = column.enumValues.map((v) => `"${v}"`).join(" | ");
1235
+ if (column.isArray) {
1236
+ return `Array<${union}>`;
1237
+ }
1238
+ return union;
1239
+ }
1240
+ if (column.type === "json" && column.jsonShape) {
1241
+ return jsonShapeToTypeScript(column.jsonShape);
1242
+ }
1073
1243
  return "unknown";
1074
1244
  }
1075
1245
 
1076
1246
  function emitTypes(definition: SchemaDefinition): string {
1247
+ const enumTypeLines: string[] = [];
1248
+ for (const [name, enumDef] of Object.entries(definition.enums ?? {})) {
1249
+ const typeName = toPascalCase(name);
1250
+ const valuesStr = enumDef.values.map((v) => `"${v}"`).join(" | ");
1251
+ enumTypeLines.push(`export type ${typeName} = ${valuesStr};`);
1252
+ }
1253
+
1077
1254
  const lines: string[] = [];
1078
1255
 
1079
1256
  for (const [tableName, table] of Object.entries(definition.tables)) {
@@ -1097,7 +1274,8 @@ function emitTypes(definition: SchemaDefinition): string {
1097
1274
  export type New${pascal} = {\n${insertFields.join("\n")}\n};`);
1098
1275
  }
1099
1276
 
1100
- return `${lines.join("\n\n")}
1277
+ return `${enumTypeLines.join("\n")}
1278
+ ${lines.join("\n\n")}
1101
1279
  `;
1102
1280
  }
1103
1281
 
@@ -12,7 +12,7 @@ The core template modules are responsible for generating:
12
12
 
13
13
  1. **Worker server source** (`src/index.ts` style output)
14
14
  2. **Cloudflare Wrangler config** (`wrangler.json` shape)
15
- 3. **Drizzle config source** (`drizzle.config.ts`)
15
+ 3. **Drizzle config source** (`drizzle.config.js`)
16
16
  4. **Typed Appflare client source** (Better Auth client wrapper)
17
17
  5. **Generated handlers integration** (registering auto-generated route handlers)
18
18
 
@@ -111,6 +111,13 @@ function normalizeOperation(
111
111
  .filter(Boolean)
112
112
  .slice(1);
113
113
 
114
+ if (
115
+ segments.length >= 2 &&
116
+ segments[segments.length - 1] === segments[segments.length - 2]
117
+ ) {
118
+ segments.pop();
119
+ }
120
+
114
121
  if (segments.length === 0) {
115
122
  return null;
116
123
  }
@@ -1,6 +1,6 @@
1
1
  import { generateTypes } from "../types";
2
2
 
3
- export function generateHandlersSource(schemaImportPath: string): string {
3
+ export function generateHandlersSource(schemaImportPath: string, roles: string[] = []): string {
4
4
  return `import type { Context } from "hono";
5
5
  import type { D1Database } from "@cloudflare/workers-types";
6
6
  import { drizzle } from "drizzle-orm/d1";
@@ -8,6 +8,6 @@ import { z, type ZodRawShape } from "zod";
8
8
  import * as authSchema from "./auth.schema";
9
9
  import * as schema from "${schemaImportPath}";
10
10
 
11
- ${generateTypes()}
11
+ ${generateTypes(roles)}
12
12
  `;
13
13
  }
@@ -188,26 +188,20 @@ function compareOrderValues(
188
188
  function hasKnownOperator(condition: Record<string, unknown>): boolean {
189
189
  return [
190
190
  "eq",
191
- "$eq",
192
191
  "ne",
193
- "$ne",
194
192
  "in",
195
- "$in",
196
193
  "nin",
197
- "$nin",
198
194
  "gt",
199
- "$gt",
200
195
  "gte",
201
- "$gte",
202
196
  "lt",
203
- "$lt",
204
197
  "lte",
205
- "$lte",
206
198
  "exists",
207
199
  "regex",
208
- "$options",
200
+ "options",
209
201
  "geoWithin",
210
- "$geoWithin",
202
+ "includes",
203
+ "includesAny",
204
+ "length",
211
205
  ].some((key) => key in condition);
212
206
  }
213
207
 
@@ -1,7 +1,13 @@
1
- export function generateTypesContextSection(): string {
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']`;
7
+
2
8
  return `type AuthSession = typeof auth.$Infer.Session;
3
9
  type AuthAdapter = Awaited<typeof auth.$context>["internalAdapter"];
4
- type User = AuthSession['user']
10
+ ${userTypeExtension}
5
11
  type Session = AuthSession['session']
6
12
 
7
13
  export type StoragePutArgs = {
@@ -19,16 +19,12 @@ type GeoWithinOperand = {
19
19
  $geometry: GeoPoint | GeoCoordinates;
20
20
  latitudeField?: string;
21
21
  longitudeField?: string;
22
- $minDistance?: number;
23
- $maxDistance?: number;
22
+ minDistance?: number;
23
+ maxDistance?: number;
24
24
  gt?: number;
25
25
  gte?: number;
26
26
  lt?: number;
27
27
  lte?: number;
28
- $gt?: number;
29
- $gte?: number;
30
- $lt?: number;
31
- $lte?: number;
32
28
  };
33
29
 
34
30
  type GeoWithinOperandForField<TFieldKey extends string> = Omit<
@@ -41,25 +37,20 @@ type GeoWithinOperandForField<TFieldKey extends string> = Omit<
41
37
 
42
38
  type FieldOperators<T, TFieldKey extends string = string> = {
43
39
  eq?: Friendly<NonNil<T>>;
44
- $eq?: Friendly<NonNil<T>>;
45
40
  ne?: Friendly<NonNil<T>>;
46
- $ne?: Friendly<NonNil<T>>;
47
41
  in?: ReadonlyArray<Friendly<NonNil<T>>>;
48
- $in?: ReadonlyArray<Friendly<NonNil<T>>>;
49
42
  nin?: ReadonlyArray<Friendly<NonNil<T>>>;
50
- $nin?: ReadonlyArray<Friendly<NonNil<T>>>;
51
43
  gt?: Comparable<T>;
52
- $gt?: Comparable<T>;
53
44
  gte?: Comparable<T>;
54
- $gte?: Comparable<T>;
55
45
  lt?: Comparable<T>;
56
- $lt?: Comparable<T>;
57
46
  lte?: Comparable<T>;
58
- $lte?: Comparable<T>;
59
47
  exists?: boolean;
60
48
  regex?: RegexOperand<T>;
61
- $options?: string;
49
+ options?: string;
62
50
  geoWithin?: GeoWithinOperandForField<TFieldKey>;
51
+ includes?: T extends ReadonlyArray<infer E> ? ReadonlyArray<E> : never;
52
+ includesAny?: T extends ReadonlyArray<infer E> ? ReadonlyArray<E> : never;
53
+ length?: T extends ReadonlyArray<unknown> ? number : never;
63
54
  };
64
55
 
65
56
  type WhereFieldValue<T, TFieldKey extends string> =
@@ -72,7 +63,6 @@ type RelationWhereInput<TModel extends Record<string, unknown>, TName extends Ta
72
63
  | (TModel[K] extends Record<string, unknown> ? RelationWhereInput<TModel[K]> : never);
73
64
  } & {
74
65
  geoWithin?: GeoWithinOperandForField<Extract<keyof TModel, string>>;
75
- $geoWithin?: GeoWithinOperandForField<Extract<keyof TModel, string>>;
76
66
  } & (TName extends TableName ? {
77
67
  [TRelation in keyof NativeFindManyWith<TName>]?: NativeFindManyWith<TName>[TRelation] extends infer TRelationConfig
78
68
  ? TRelationConfig extends Record<string, unknown>