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 1 commit
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
7 changes: 6 additions & 1 deletion 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 @@ -47,8 +48,12 @@
"devDependencies": {
"@better-auth/core": "^1.3.0",
"better-auth": "^1.3.0",
"@better-auth/cli": "^1.3.0",
"@types/tmp": "catalog:",
"@zenstackhq/cli": "workspace:*",
"@zenstackhq/eslint-config": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/vitest-config": "workspace:*"
"@zenstackhq/vitest-config": "workspace:*",
"tmp": "catalog:"
}
}
23 changes: 23 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 @@ -260,6 +261,11 @@ function getMappedFieldType({ bigint, type }: DBFieldAttribute) {
.with('string[]', () => ({ type: 'String', array: true }))
.with('number[]', () => ({ type: 'Int', array: true }))
.otherwise(() => {
// Handle enum types (e.g., ['user', 'admin'] or "user,admin")
// Map them to String type for now
Comment thread
ymc9 marked this conversation as resolved.
Outdated
if (Array.isArray(type) || (typeof type === 'string' && type.includes(','))) {
return { type: 'String' };
Comment thread
ymc9 marked this conversation as resolved.
Outdated
}
Comment thread
ymc9 marked this conversation as resolved.
Outdated
throw new Error(`Unsupported field type: ${type}`);
});
}
Expand Down Expand Up @@ -332,6 +338,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 +547,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`, {

Check warning on line 19 in packages/auth-adapters/better-auth/test/cli-generate.test.ts

View check run for this annotation

GitHub Advanced Security / CodeQL

Shell command built from environment values

This shell command depends on an uncontrolled [absolute path](1).
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}`, {

Check warning on line 37 in packages/auth-adapters/better-auth/test/cli-generate.test.ts

View check run for this annotation

GitHub Advanced Security / CodeQL

Shell command built from environment values

This shell command depends on an uncontrolled [absolute path](1).
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
Loading