Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/auth-adapters/better-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"build": "tsc --noEmit && tsup-node",
"watch": "tsup-node --watch",
"lint": "eslint src --ext ts",
"test": "vitest run",
"pack": "pnpm pack"
},
"keywords": [
Expand Down Expand Up @@ -45,10 +46,14 @@
"better-auth": "^1.3.0"
},
"devDependencies": {
"@better-auth/core": "^1.3.0",
"better-auth": "^1.3.0",
"@better-auth/core": "1.4.17",
"better-auth": "1.4.17",
"@better-auth/cli": "1.4.17",
"@types/tmp": "catalog:",
"@zenstackhq/cli": "workspace:*",
"@zenstackhq/eslint-config": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/vitest-config": "workspace:*"
"@zenstackhq/vitest-config": "workspace:*",
"tmp": "catalog:"
}
}
25 changes: 25 additions & 0 deletions packages/auth-adapters/better-auth/src/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
InvocationExpr,
isDataModel,
Model,
NumberLiteral,
ReferenceExpr,
StringLiteral,
} from '@zenstackhq/language/ast';
Expand Down Expand Up @@ -259,6 +260,13 @@ function getMappedFieldType({ bigint, type }: DBFieldAttribute) {
.with('json', () => ({ type: 'Json' }))
.with('string[]', () => ({ type: 'String', array: true }))
.with('number[]', () => ({ type: 'Int', array: true }))
.when(
(v) => Array.isArray(v) && v.every((e) => typeof e === 'string'),
() => {
// Handle enum types (e.g., ['user', 'admin']), map them to String type for now
return { type: 'String' };
},
)
.otherwise(() => {
throw new Error(`Unsupported field type: ${type}`);
});
Expand Down Expand Up @@ -332,6 +340,10 @@ function addOrUpdateModel(
addDefaultNow(df);
} else if (typeof field.defaultValue === 'boolean') {
addFieldAttribute(df, '@default', [createBooleanAttributeArg(field.defaultValue)]);
} else if (typeof field.defaultValue === 'string') {
addFieldAttribute(df, '@default', [createStringAttributeArg(field.defaultValue)]);
} else if (typeof field.defaultValue === 'number') {
addFieldAttribute(df, '@default', [createNumberAttributeArg(field.defaultValue)]);
} else if (typeof field.defaultValue === 'function') {
// For other function-based defaults, we'll need to check what they return
const defaultVal = field.defaultValue();
Expand Down Expand Up @@ -537,6 +549,19 @@ function createBooleanAttributeArg(value: boolean) {
return arg;
}

function createNumberAttributeArg(value: number) {
const arg: AttributeArg = {
$type: 'AttributeArg',
} as any;
const expr: NumberLiteral = {
$type: 'NumberLiteral',
value: value.toString(),
$container: arg,
};
arg.value = expr;
return arg;
}

function createStringAttributeArg(value: string) {
const arg: AttributeArg = {
$type: 'AttributeArg',
Expand Down
33 changes: 33 additions & 0 deletions packages/auth-adapters/better-auth/test/auth-custom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { zenstackAdapter } from '../src/adapter';
import { betterAuth } from 'better-auth';

export const auth = betterAuth({
database: zenstackAdapter({} as any, {
provider: 'postgresql',
}),
user: {
additionalFields: {
role: {
type: ['user', 'admin'],
required: false,
defaultValue: 'user',
input: false, // don't allow user to set role
},
lang: {
type: 'string',
required: false,
defaultValue: 'en',
},
age: {
type: 'number',
required: true,
defaultValue: 18,
},
admin: {
type: 'boolean',
required: false,
defaultValue: false,
},
},
},
});
8 changes: 8 additions & 0 deletions packages/auth-adapters/better-auth/test/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { zenstackAdapter } from '../src/adapter';
import { betterAuth } from 'better-auth';

export const auth = betterAuth({
Comment thread
ymc9 marked this conversation as resolved.
database: zenstackAdapter({} as any, {
provider: 'postgresql',
}),
});
65 changes: 65 additions & 0 deletions packages/auth-adapters/better-auth/test/cli-generate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { fileURLToPath } from 'node:url';
import tmp from 'tmp';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

/**
* Helper function to generate schema using better-auth CLI
*/
function generateSchema(configFile: string): string {
const { name: workDir } = tmp.dirSync({ unsafeCleanup: true });
const schemaPath = path.join(workDir, 'schema.zmodel');
const configPath = path.join(__dirname, configFile);

execSync(`pnpm better-auth generate --config ${configPath} --output ${schemaPath} --yes`, {
Comment thread Dismissed
cwd: __dirname,
stdio: 'pipe',
});
Comment thread
ymc9 marked this conversation as resolved.

return schemaPath;
Comment thread
ymc9 marked this conversation as resolved.
}

/**
* Helper function to verify schema with zenstack check
*/
function verifySchema(schemaPath: string) {
const cliPath = path.join(__dirname, '../../../cli/dist/index.js');
const workDir = path.dirname(schemaPath);

expect(fs.existsSync(schemaPath)).toBe(true);

expect(() => {
execSync(`node ${cliPath} check --schema ${schemaPath}`, {
Comment thread Dismissed
cwd: workDir,
stdio: 'pipe',
});
}).not.toThrow();
}

describe('Cli schema generation tests', () => {
it('works with simple config', async () => {
const schemaPath = generateSchema('auth.ts');
verifySchema(schemaPath);
});

it('works with custom config', async () => {
const schemaPath = generateSchema('auth-custom.ts');
verifySchema(schemaPath);

// Verify that the generated schema contains the expected default values
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
expect(schemaContent).toMatch(/role\s+String/);
expect(schemaContent).toContain("@default('user')");
expect(schemaContent).toMatch(/lang\s+String/);
expect(schemaContent).toContain("@default('en')");
expect(schemaContent).toMatch(/age\s+Int/);
expect(schemaContent).toContain('@default(18)');
expect(schemaContent).toMatch(/admin\s+Boolean/);
expect(schemaContent).toContain('@default(false)');
});
Comment thread
ymc9 marked this conversation as resolved.
});
Loading