Skip to content

Commit 4957aa1

Browse files
committed
fix better-auth array support
1 parent 8609d5b commit 4957aa1

5 files changed

Lines changed: 165 additions & 30 deletions

File tree

packages/auth-adapters/better-auth/src/adapter.ts

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { BetterAuthOptions } from '@better-auth/core';
2-
import type { DBAdapter, DBAdapterDebugLogOption, Where } from '@better-auth/core/db/adapter';
2+
import type { DBAdapter, Where } from '@better-auth/core/db/adapter';
33
import { BetterAuthError } from '@better-auth/core/error';
44
import type { ClientContract, ModelOperations, UpdateInput } from '@zenstackhq/orm';
55
import type { GetModels, SchemaDef } from '@zenstackhq/orm/schema';
@@ -8,30 +8,9 @@ import {
88
type AdapterFactoryCustomizeAdapterCreator,
99
type AdapterFactoryOptions,
1010
} from 'better-auth/adapters';
11+
import { getSupportsArrays, type AdapterConfig } from './config';
1112

12-
/**
13-
* Options for the ZenStack adapter factory.
14-
*/
15-
export interface AdapterConfig {
16-
/**
17-
* Database provider
18-
*/
19-
provider: 'sqlite' | 'postgresql';
20-
21-
/**
22-
* Enable debug logs for the adapter
23-
*
24-
* @default false
25-
*/
26-
debugLogs?: DBAdapterDebugLogOption | undefined;
27-
28-
/**
29-
* Use plural table names
30-
*
31-
* @default false
32-
*/
33-
usePlural?: boolean | undefined;
34-
}
13+
export type { AdapterConfig } from './config';
3514

3615
/**
3716
* Create a Better-Auth adapter for ZenStack ORM.
@@ -220,6 +199,7 @@ export const zenstackAdapter = <Schema extends SchemaDef>(db: ClientContract<Sch
220199
adapterName: 'ZenStack Adapter',
221200
usePlural: config.usePlural ?? false,
222201
debugLogs: config.debugLogs ?? false,
202+
supportsArrays: getSupportsArrays(config),
223203
transaction: (cb) =>
224204
db.$transaction((tx) => {
225205
const adapter = createAdapterFactory({
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { DBAdapterDebugLogOption } from '@better-auth/core/db/adapter';
2+
3+
/**
4+
* Options for the ZenStack adapter factory.
5+
*/
6+
export interface AdapterConfig {
7+
/**
8+
* Database provider
9+
*/
10+
provider: 'sqlite' | 'postgresql';
11+
12+
/**
13+
* Enable debug logs for the adapter
14+
*
15+
* @default false
16+
*/
17+
debugLogs?: DBAdapterDebugLogOption | undefined;
18+
19+
/**
20+
* Use plural table names
21+
*
22+
* @default false
23+
*/
24+
usePlural?: boolean | undefined;
25+
26+
/**
27+
* Preserve Better Auth array fields as native database arrays.
28+
*
29+
* Defaults to true for PostgreSQL and false for SQLite.
30+
*/
31+
supportsArrays?: boolean | undefined;
32+
}
33+
34+
export function getSupportsArrays(config: AdapterConfig) {
35+
return config.supportsArrays ?? config.provider === 'postgresql';
36+
}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import type { DBAdapterSchemaCreation } from 'better-auth/adapters';
2626
import type { BetterAuthDBSchema, DBFieldAttribute, DBFieldType } from 'better-auth/db';
2727
import fs from 'node:fs';
2828
import { match } from 'ts-pattern';
29-
import type { AdapterConfig } from './adapter';
29+
import { getSupportsArrays, type AdapterConfig } from './config';
3030

3131
export async function generateSchema(
3232
file: string | undefined,
@@ -95,13 +95,15 @@ async function updateSchema(
9595

9696
let changed = false;
9797

98+
const supportsArrays = getSupportsArrays(config);
9899
for (const [name, table] of Object.entries(tables)) {
99100
const c = addOrUpdateModel(
100101
name,
101102
table,
102103
zmodel,
103104
tables,
104105
toManyRelations,
106+
supportsArrays,
105107
!!options.advanced?.database?.useNumberId,
106108
);
107109
changed = changed || c;
@@ -251,15 +253,15 @@ function initializeZmodel(config: AdapterConfig) {
251253
return zmodel;
252254
}
253255

254-
function getMappedFieldType({ bigint, type }: DBFieldAttribute) {
256+
function getMappedFieldType({ bigint, type }: DBFieldAttribute, supportsArrays: boolean) {
255257
return match<DBFieldType, { type: string; array?: boolean }>(type)
256258
.with('string', () => ({ type: 'String' }))
257259
.with('number', () => (bigint ? { type: 'BigInt' } : { type: 'Int' }))
258260
.with('boolean', () => ({ type: 'Boolean' }))
259261
.with('date', () => ({ type: 'DateTime' }))
260262
.with('json', () => ({ type: 'Json' }))
261-
.with('string[]', () => ({ type: 'String', array: true }))
262-
.with('number[]', () => ({ type: 'Int', array: true }))
263+
.with('string[]', () => (supportsArrays ? { type: 'String', array: true } : { type: 'Json' }))
264+
.with('number[]', () => (supportsArrays ? { type: 'Int', array: true } : { type: 'Json' }))
263265
.when(
264266
(v) => Array.isArray(v) && v.every((e) => typeof e === 'string'),
265267
() => {
@@ -278,6 +280,7 @@ function addOrUpdateModel(
278280
zmodel: Model,
279281
tables: BetterAuthDBSchema,
280282
toManyRelations: Map<string, Set<string>>,
283+
supportsArrays: boolean,
281284
numericId: boolean,
282285
): boolean {
283286
let changed = false;
@@ -305,7 +308,7 @@ function addOrUpdateModel(
305308

306309
if (!field.references) {
307310
// scalar field
308-
const { array, type } = getMappedFieldType(field);
311+
const { array, type } = getMappedFieldType(field, supportsArrays);
309312

310313
const df: DataField = {
311314
$type: 'DataField',
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import type { BetterAuthOptions } from '@better-auth/core';
2+
import type { BetterAuthDBSchema } from 'better-auth/db';
3+
import fs from 'node:fs';
4+
import path from 'node:path';
5+
import tmp from 'tmp';
6+
import { describe, expect, it } from 'vitest';
7+
import { type AdapterConfig, zenstackAdapter } from '../src/adapter';
8+
import { generateSchema } from '../src/schema-generator';
9+
10+
const oauthClientSchema = {
11+
oauthClient: {
12+
modelName: 'oauthClient',
13+
fields: {
14+
name: {
15+
type: 'string',
16+
required: true,
17+
},
18+
scopes: {
19+
type: 'string[]',
20+
required: true,
21+
},
22+
retryDelays: {
23+
type: 'number[]',
24+
required: false,
25+
},
26+
},
27+
},
28+
} satisfies BetterAuthDBSchema;
29+
30+
function makeAuthOptions() {
31+
return {
32+
plugins: [
33+
{
34+
id: 'oauth-provider',
35+
schema: oauthClientSchema,
36+
},
37+
],
38+
} as unknown as BetterAuthOptions;
39+
}
40+
41+
function makeDb(captured: { createData?: Record<string, unknown> }) {
42+
return {
43+
oauthClient: {
44+
create: async ({ data }: { data: Record<string, unknown> }) => {
45+
captured.createData = data;
46+
return data;
47+
},
48+
},
49+
$transaction: async <T>(cb: (tx: unknown) => Promise<T>) => cb(makeDb(captured)),
50+
};
51+
}
52+
53+
async function createOauthClient(config: AdapterConfig) {
54+
const captured: { createData?: Record<string, unknown> } = {};
55+
const adapter = zenstackAdapter(makeDb(captured) as any, config)(makeAuthOptions());
56+
57+
await adapter.create({
58+
model: 'oauthClient',
59+
data: {
60+
id: 'client-1',
61+
name: 'client',
62+
scopes: ['openid', 'profile'],
63+
retryDelays: [1, 2],
64+
},
65+
forceAllowId: true,
66+
});
67+
68+
return captured.createData;
69+
}
70+
71+
async function generateOauthClientSchema(config: AdapterConfig) {
72+
const { name: workDir, removeCallback } = tmp.dirSync({ unsafeCleanup: true });
73+
const schemaPath = path.join(workDir, 'schema.zmodel');
74+
75+
try {
76+
const result = await generateSchema(schemaPath, oauthClientSchema, config, makeAuthOptions());
77+
return result.code;
78+
} finally {
79+
if (fs.existsSync(workDir)) {
80+
removeCallback();
81+
}
82+
}
83+
}
84+
85+
describe('ZenStack Better Auth adapter', () => {
86+
it('preserves native array inputs for PostgreSQL (#2615)', async () => {
87+
const data = await createOauthClient({ provider: 'postgresql' });
88+
89+
expect(data?.scopes).toEqual(['openid', 'profile']);
90+
expect(data?.retryDelays).toEqual([1, 2]);
91+
});
92+
93+
it('serializes array inputs when native arrays are disabled (#2615)', async () => {
94+
const data = await createOauthClient({ provider: 'postgresql', supportsArrays: false });
95+
96+
expect(data?.scopes).toBe(JSON.stringify(['openid', 'profile']));
97+
expect(data?.retryDelays).toBe(JSON.stringify([1, 2]));
98+
});
99+
100+
it('generates native array fields when the adapter supports arrays (#2615)', async () => {
101+
const schema = await generateOauthClientSchema({ provider: 'postgresql' });
102+
103+
expect(schema).toMatch(/scopes\s+String\[\]/);
104+
expect(schema).toMatch(/retryDelays\s+Int\[\]\?/);
105+
});
106+
107+
it('generates JSON fields when the adapter does not support arrays (#2615)', async () => {
108+
const schema = await generateOauthClientSchema({ provider: 'sqlite' });
109+
110+
expect(schema).toMatch(/scopes\s+Json/);
111+
expect(schema).toMatch(/retryDelays\s+Json\?/);
112+
expect(schema).not.toMatch(/scopes\s+String\[\]/);
113+
expect(schema).not.toMatch(/retryDelays\s+Int\[\]/);
114+
});
115+
});

packages/auth-adapters/better-auth/test/cli-generate.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import tmp from 'tmp';
77

88
const __filename = fileURLToPath(import.meta.url);
99
const __dirname = path.dirname(__filename);
10+
const packageRoot = path.join(__dirname, '..');
1011

1112
/**
1213
* Helper function to generate schema using better-auth CLI
@@ -17,7 +18,7 @@ function generateSchema(configFile: string): string {
1718
const configPath = path.join(__dirname, configFile);
1819

1920
execSync(`pnpm better-auth generate --config ${configPath} --output ${schemaPath} --yes`, {
20-
cwd: __dirname,
21+
cwd: packageRoot,
2122
stdio: 'pipe',
2223
});
2324

0 commit comments

Comments
 (0)