Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit 8fdd349

Browse files
authored
fix(better-auth): support custom table enum field types and defaults (#621)
* fix(better-auth): support custom table enum field types and defaults fixes #592 * stricter enum type check * update better-auth packages
1 parent c7ad7d7 commit 8fdd349

6 files changed

Lines changed: 893 additions & 280 deletions

File tree

packages/auth-adapters/better-auth/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"build": "tsc --noEmit && tsup-node",
88
"watch": "tsup-node --watch",
99
"lint": "eslint src --ext ts",
10+
"test": "vitest run",
1011
"pack": "pnpm pack"
1112
},
1213
"keywords": [
@@ -45,10 +46,14 @@
4546
"better-auth": "^1.3.0"
4647
},
4748
"devDependencies": {
48-
"@better-auth/core": "^1.3.0",
49-
"better-auth": "^1.3.0",
49+
"@better-auth/core": "1.4.17",
50+
"better-auth": "1.4.17",
51+
"@better-auth/cli": "1.4.17",
52+
"@types/tmp": "catalog:",
53+
"@zenstackhq/cli": "workspace:*",
5054
"@zenstackhq/eslint-config": "workspace:*",
5155
"@zenstackhq/typescript-config": "workspace:*",
52-
"@zenstackhq/vitest-config": "workspace:*"
56+
"@zenstackhq/vitest-config": "workspace:*",
57+
"tmp": "catalog:"
5358
}
5459
}

packages/auth-adapters/better-auth/src/schema-generator.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
InvocationExpr,
1717
isDataModel,
1818
Model,
19+
NumberLiteral,
1920
ReferenceExpr,
2021
StringLiteral,
2122
} from '@zenstackhq/language/ast';
@@ -259,6 +260,13 @@ function getMappedFieldType({ bigint, type }: DBFieldAttribute) {
259260
.with('json', () => ({ type: 'Json' }))
260261
.with('string[]', () => ({ type: 'String', array: true }))
261262
.with('number[]', () => ({ type: 'Int', array: true }))
263+
.when(
264+
(v) => Array.isArray(v) && v.every((e) => typeof e === 'string'),
265+
() => {
266+
// Handle enum types (e.g., ['user', 'admin']), map them to String type for now
267+
return { type: 'String' };
268+
},
269+
)
262270
.otherwise(() => {
263271
throw new Error(`Unsupported field type: ${type}`);
264272
});
@@ -332,6 +340,10 @@ function addOrUpdateModel(
332340
addDefaultNow(df);
333341
} else if (typeof field.defaultValue === 'boolean') {
334342
addFieldAttribute(df, '@default', [createBooleanAttributeArg(field.defaultValue)]);
343+
} else if (typeof field.defaultValue === 'string') {
344+
addFieldAttribute(df, '@default', [createStringAttributeArg(field.defaultValue)]);
345+
} else if (typeof field.defaultValue === 'number') {
346+
addFieldAttribute(df, '@default', [createNumberAttributeArg(field.defaultValue)]);
335347
} else if (typeof field.defaultValue === 'function') {
336348
// For other function-based defaults, we'll need to check what they return
337349
const defaultVal = field.defaultValue();
@@ -537,6 +549,19 @@ function createBooleanAttributeArg(value: boolean) {
537549
return arg;
538550
}
539551

552+
function createNumberAttributeArg(value: number) {
553+
const arg: AttributeArg = {
554+
$type: 'AttributeArg',
555+
} as any;
556+
const expr: NumberLiteral = {
557+
$type: 'NumberLiteral',
558+
value: value.toString(),
559+
$container: arg,
560+
};
561+
arg.value = expr;
562+
return arg;
563+
}
564+
540565
function createStringAttributeArg(value: string) {
541566
const arg: AttributeArg = {
542567
$type: 'AttributeArg',
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { zenstackAdapter } from '../src/adapter';
2+
import { betterAuth } from 'better-auth';
3+
4+
export const auth = betterAuth({
5+
database: zenstackAdapter({} as any, {
6+
provider: 'postgresql',
7+
}),
8+
user: {
9+
additionalFields: {
10+
role: {
11+
type: ['user', 'admin'],
12+
required: false,
13+
defaultValue: 'user',
14+
input: false, // don't allow user to set role
15+
},
16+
lang: {
17+
type: 'string',
18+
required: false,
19+
defaultValue: 'en',
20+
},
21+
age: {
22+
type: 'number',
23+
required: true,
24+
defaultValue: 18,
25+
},
26+
admin: {
27+
type: 'boolean',
28+
required: false,
29+
defaultValue: false,
30+
},
31+
},
32+
},
33+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { zenstackAdapter } from '../src/adapter';
2+
import { betterAuth } from 'better-auth';
3+
4+
export const auth = betterAuth({
5+
database: zenstackAdapter({} as any, {
6+
provider: 'postgresql',
7+
}),
8+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { execSync } from 'node:child_process';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { describe, expect, it } from 'vitest';
5+
import { fileURLToPath } from 'node:url';
6+
import tmp from 'tmp';
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const __dirname = path.dirname(__filename);
10+
11+
/**
12+
* Helper function to generate schema using better-auth CLI
13+
*/
14+
function generateSchema(configFile: string): string {
15+
const { name: workDir } = tmp.dirSync({ unsafeCleanup: true });
16+
const schemaPath = path.join(workDir, 'schema.zmodel');
17+
const configPath = path.join(__dirname, configFile);
18+
19+
execSync(`pnpm better-auth generate --config ${configPath} --output ${schemaPath} --yes`, {
20+
cwd: __dirname,
21+
stdio: 'pipe',
22+
});
23+
24+
return schemaPath;
25+
}
26+
27+
/**
28+
* Helper function to verify schema with zenstack check
29+
*/
30+
function verifySchema(schemaPath: string) {
31+
const cliPath = path.join(__dirname, '../../../cli/dist/index.js');
32+
const workDir = path.dirname(schemaPath);
33+
34+
expect(fs.existsSync(schemaPath)).toBe(true);
35+
36+
expect(() => {
37+
execSync(`node ${cliPath} check --schema ${schemaPath}`, {
38+
cwd: workDir,
39+
stdio: 'pipe',
40+
});
41+
}).not.toThrow();
42+
}
43+
44+
describe('Cli schema generation tests', () => {
45+
it('works with simple config', async () => {
46+
const schemaPath = generateSchema('auth.ts');
47+
verifySchema(schemaPath);
48+
});
49+
50+
it('works with custom config', async () => {
51+
const schemaPath = generateSchema('auth-custom.ts');
52+
verifySchema(schemaPath);
53+
54+
// Verify that the generated schema contains the expected default values
55+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
56+
expect(schemaContent).toMatch(/role\s+String/);
57+
expect(schemaContent).toContain("@default('user')");
58+
expect(schemaContent).toMatch(/lang\s+String/);
59+
expect(schemaContent).toContain("@default('en')");
60+
expect(schemaContent).toMatch(/age\s+Int/);
61+
expect(schemaContent).toContain('@default(18)');
62+
expect(schemaContent).toMatch(/admin\s+Boolean/);
63+
expect(schemaContent).toContain('@default(false)');
64+
});
65+
});

0 commit comments

Comments
 (0)