Skip to content

Commit 5a36a7d

Browse files
authored
Merge pull request #420 from zenstackhq/dev
merge dev to main (v3.0.0-beta.24)
2 parents 778526d + 710cb8b commit 5a36a7d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1562
-384
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-v3",
3-
"version": "3.0.0-beta.23",
3+
"version": "3.0.0-beta.24",
44
"description": "ZenStack",
55
"packageManager": "[email protected]",
66
"scripts": {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import config from '@zenstackhq/eslint-config/base.js';
2+
import tseslint from 'typescript-eslint';
3+
4+
/** @type {import("eslint").Linter.Config} */
5+
export default tseslint.config(config, {
6+
rules: {
7+
'@typescript-eslint/no-unused-expressions': 'off',
8+
},
9+
});

packages/dialects/sql.js/package.json renamed to packages/auth-adapters/better-auth/package.json

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
{
2-
"name": "@zenstackhq/kysely-sql-js",
3-
"version": "3.0.0-beta.23",
4-
"description": "Kysely dialect for sql.js",
2+
"name": "@zenstackhq/better-auth",
3+
"version": "3.0.0-beta.24",
4+
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
55
"type": "module",
66
"scripts": {
77
"build": "tsc --noEmit && tsup-node",
88
"watch": "tsup-node --watch",
99
"lint": "eslint src --ext ts",
1010
"pack": "pnpm pack"
1111
},
12-
"keywords": [],
12+
"keywords": [
13+
"better-auth",
14+
"auth"
15+
],
1316
"author": "ZenStack Team",
1417
"license": "MIT",
1518
"files": [
@@ -31,16 +34,21 @@
3134
"require": "./package.json"
3235
}
3336
},
37+
"dependencies": {
38+
"@zenstackhq/orm": "workspace:*",
39+
"@zenstackhq/language": "workspace:*",
40+
"@zenstackhq/common-helpers": "workspace:*",
41+
"ts-pattern": "catalog:"
42+
},
43+
"peerDependencies": {
44+
"@better-auth/core": "^1.3.0",
45+
"better-auth": "^1.3.0"
46+
},
3447
"devDependencies": {
35-
"@types/sql.js": "^1.4.9",
48+
"@better-auth/core": "^1.3.0",
49+
"better-auth": "^1.3.0",
3650
"@zenstackhq/eslint-config": "workspace:*",
3751
"@zenstackhq/typescript-config": "workspace:*",
38-
"@zenstackhq/vitest-config": "workspace:*",
39-
"sql.js": "^1.13.0",
40-
"kysely": "catalog:"
41-
},
42-
"peerDependencies": {
43-
"sql.js": "^1.13.0",
44-
"kysely": "catalog:"
52+
"@zenstackhq/vitest-config": "workspace:*"
4553
}
4654
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import type { BetterAuthOptions } from '@better-auth/core';
2+
import type { DBAdapter, DBAdapterDebugLogOption, Where } from '@better-auth/core/db/adapter';
3+
import { BetterAuthError } from '@better-auth/core/error';
4+
import type { ClientContract, ModelOperations, UpdateInput } from '@zenstackhq/orm';
5+
import type { GetModels, SchemaDef } from '@zenstackhq/orm/schema';
6+
import {
7+
createAdapterFactory,
8+
type AdapterFactoryCustomizeAdapterCreator,
9+
type AdapterFactoryOptions,
10+
} from 'better-auth/adapters';
11+
import { generateSchema } from './schema-generator';
12+
13+
/**
14+
* Options for the ZenStack adapter factory.
15+
*/
16+
export interface AdapterConfig {
17+
/**
18+
* Database provider
19+
*/
20+
provider: 'sqlite' | 'postgresql';
21+
22+
/**
23+
* Enable debug logs for the adapter
24+
*
25+
* @default false
26+
*/
27+
debugLogs?: DBAdapterDebugLogOption | undefined;
28+
29+
/**
30+
* Use plural table names
31+
*
32+
* @default false
33+
*/
34+
usePlural?: boolean | undefined;
35+
}
36+
37+
/**
38+
* Create a Better-Auth adapter for ZenStack ORM.
39+
* @param db ZenStack ORM client instance
40+
* @param config adapter configuration options
41+
*/
42+
export const zenstackAdapter = <Schema extends SchemaDef>(db: ClientContract<Schema>, config: AdapterConfig) => {
43+
let lazyOptions: BetterAuthOptions | null = null;
44+
const createCustomAdapter =
45+
(db: ClientContract<Schema>): AdapterFactoryCustomizeAdapterCreator =>
46+
({ getFieldName, options }) => {
47+
const convertSelect = (select?: string[], model?: string) => {
48+
if (!select || !model) return undefined;
49+
return select.reduce((prev, cur) => {
50+
return {
51+
...prev,
52+
[getFieldName({ model, field: cur })]: true,
53+
};
54+
}, {});
55+
};
56+
function operatorToORMOperator(operator: string) {
57+
switch (operator) {
58+
case 'starts_with':
59+
return 'startsWith';
60+
case 'ends_with':
61+
return 'endsWith';
62+
case 'ne':
63+
return 'not';
64+
case 'not_in':
65+
return 'notIn';
66+
default:
67+
return operator;
68+
}
69+
}
70+
const convertWhereClause = (model: string, where?: Where[]): any => {
71+
if (!where || !where.length) return {};
72+
if (where.length === 1) {
73+
const w = where[0]!;
74+
if (!w) {
75+
throw new BetterAuthError('Invalid where clause');
76+
}
77+
return {
78+
[getFieldName({ model, field: w.field })]:
79+
w.operator === 'eq' || !w.operator
80+
? w.value
81+
: {
82+
[operatorToORMOperator(w.operator)]: w.value,
83+
},
84+
};
85+
}
86+
const and = where.filter((w) => w.connector === 'AND' || !w.connector);
87+
const or = where.filter((w) => w.connector === 'OR');
88+
const andClause = and.map((w) => {
89+
return {
90+
[getFieldName({ model, field: w.field })]:
91+
w.operator === 'eq' || !w.operator
92+
? w.value
93+
: {
94+
[operatorToORMOperator(w.operator)]: w.value,
95+
},
96+
};
97+
});
98+
const orClause = or.map((w) => {
99+
return {
100+
[getFieldName({ model, field: w.field })]:
101+
w.operator === 'eq' || !w.operator
102+
? w.value
103+
: {
104+
[operatorToORMOperator(w.operator)]: w.value,
105+
},
106+
};
107+
});
108+
109+
return {
110+
...(andClause.length ? { AND: andClause } : {}),
111+
...(orClause.length ? { OR: orClause } : {}),
112+
};
113+
};
114+
115+
function requireModelDb(db: ClientContract<Schema>, model: string) {
116+
const modelDb = db[model as keyof typeof db];
117+
if (!modelDb) {
118+
throw new BetterAuthError(
119+
`Model ${model} does not exist in the database. If you haven't generated the ZenStack schema, you need to run 'npx zen generate'`,
120+
);
121+
}
122+
return modelDb as unknown as ModelOperations<SchemaDef, GetModels<SchemaDef>>;
123+
}
124+
125+
return {
126+
async create({ model, data: values, select }): Promise<any> {
127+
const modelDb = requireModelDb(db, model);
128+
return await modelDb.create({
129+
data: values,
130+
select: convertSelect(select, model),
131+
});
132+
},
133+
134+
async findOne({ model, where, select }): Promise<any> {
135+
const modelDb = requireModelDb(db, model);
136+
const whereClause = convertWhereClause(model, where);
137+
return await modelDb.findFirst({
138+
where: whereClause,
139+
select: convertSelect(select, model),
140+
});
141+
},
142+
143+
async findMany({ model, where, limit, offset, sortBy }): Promise<any[]> {
144+
const modelDb = requireModelDb(db, model);
145+
const whereClause = convertWhereClause(model, where);
146+
return await modelDb.findMany({
147+
where: whereClause,
148+
take: limit || 100,
149+
skip: offset || 0,
150+
...(sortBy?.field
151+
? {
152+
orderBy: {
153+
[getFieldName({ model, field: sortBy.field })]:
154+
sortBy.direction === 'desc' ? 'desc' : 'asc',
155+
} as any,
156+
}
157+
: {}),
158+
});
159+
},
160+
161+
async count({ model, where }) {
162+
const modelDb = requireModelDb(db, model);
163+
const whereClause = convertWhereClause(model, where);
164+
return await modelDb.count({
165+
where: whereClause,
166+
});
167+
},
168+
169+
async update({ model, where, update }): Promise<any> {
170+
const modelDb = requireModelDb(db, model);
171+
const whereClause = convertWhereClause(model, where);
172+
return await modelDb.update({
173+
where: whereClause,
174+
data: update as UpdateInput<SchemaDef, GetModels<SchemaDef>>,
175+
});
176+
},
177+
178+
async updateMany({ model, where, update }) {
179+
const modelDb = requireModelDb(db, model);
180+
const whereClause = convertWhereClause(model, where);
181+
const result = await modelDb.updateMany({
182+
where: whereClause,
183+
data: update,
184+
});
185+
return result ? (result.count as number) : 0;
186+
},
187+
188+
async delete({ model, where }): Promise<any> {
189+
const modelDb = requireModelDb(db, model);
190+
const whereClause = convertWhereClause(model, where);
191+
try {
192+
await modelDb.delete({
193+
where: whereClause,
194+
});
195+
} catch {
196+
// If the record doesn't exist, we don't want to throw an error
197+
}
198+
},
199+
200+
async deleteMany({ model, where }) {
201+
const modelDb = requireModelDb(db, model);
202+
const whereClause = convertWhereClause(model, where);
203+
const result = await modelDb.deleteMany({
204+
where: whereClause,
205+
});
206+
return result ? (result.count as number) : 0;
207+
},
208+
209+
options: config,
210+
211+
createSchema: async ({ file, tables }) => {
212+
return generateSchema(file, tables, config, options);
213+
},
214+
};
215+
};
216+
217+
const adapterOptions: AdapterFactoryOptions = {
218+
config: {
219+
adapterId: 'zenstack',
220+
adapterName: 'ZenStack Adapter',
221+
usePlural: config.usePlural ?? false,
222+
debugLogs: config.debugLogs ?? false,
223+
transaction: (cb) =>
224+
db.$transaction((tx) => {
225+
const adapter = createAdapterFactory({
226+
config: adapterOptions!.config,
227+
adapter: createCustomAdapter(tx as ClientContract<Schema>),
228+
})(lazyOptions!);
229+
return cb(adapter);
230+
}),
231+
},
232+
adapter: createCustomAdapter(db),
233+
};
234+
235+
const adapter = createAdapterFactory(adapterOptions);
236+
return (options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => {
237+
lazyOptions = options;
238+
return adapter(options);
239+
};
240+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { zenstackAdapter, type AdapterConfig } from './adapter';

0 commit comments

Comments
 (0)