1- import type { Adapter , AdapterAccount , AdapterSession , AdapterUser , AdapterVerificationToken } from 'better-auth' ;
2- import type { ObjectQLClient } from '@objectstack/ql' ;
1+ import type { BetterAuthOptions } from 'better-auth' ;
32
43/**
54 * ObjectQL Adapter for Better-Auth
@@ -9,222 +8,135 @@ import type { ObjectQLClient } from '@objectstack/ql';
98 *
109 * Pattern: All database operations use ql.entity('EntityName').operation()
1110 * NO direct SQL, Prisma, or Drizzle calls.
11+ *
12+ * Note: The peer dependency @objectstack/ql provides the ObjectQLClient type.
13+ * This file uses 'any' type to avoid type errors when the peer dependency is not installed.
1214 */
1315
1416export interface ObjectQLAdapterConfig {
15- ql : ObjectQLClient ;
17+ ql : any ; // ObjectQLClient from @objectstack /ql peer dependency
18+ debugLogs ?: boolean ;
1619}
1720
1821/**
19- * Generate a unique ID for entities
20- * Prefers crypto.randomUUID, falls back to timestamp-based ID
22+ * Creates a Better-Auth DBAdapter for ObjectQL
23+ *
24+ * This follows the better-auth 1.4+ adapter pattern where adapters
25+ * are factory functions that return a function that takes BetterAuthOptions
26+ * and returns the actual adapter implementation.
2127 */
22- function generateId ( ) : string {
23- // Try crypto.randomUUID first (available in modern runtimes)
24- if ( typeof crypto !== 'undefined' && crypto . randomUUID ) {
25- return crypto . randomUUID ( ) ;
26- }
27-
28- // Fallback: timestamp + random number + counter for better uniqueness
29- const timestamp = Date . now ( ) . toString ( 36 ) ;
30- const random = Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ;
31- const counter = ( Math . random ( ) * 1000000 ) . toString ( 36 ) ;
32- return `${ timestamp } -${ random } -${ counter } ` ;
33- }
34-
35- export function createObjectQLAdapter ( config : ObjectQLAdapterConfig ) : Adapter {
36- const { ql } = config ;
37-
38- return {
39- // User operations
40- async createUser ( data : AdapterUser ) : Promise < AdapterUser > {
41- const user = await ql . entity ( 'User' ) . create ( {
42- data : {
43- id : data . id ,
44- email : data . email ,
45- emailVerified : data . emailVerified ?? false ,
46- name : data . name ?? null ,
47- image : data . image ?? null ,
48- createdAt : new Date ( ) ,
49- updatedAt : new Date ( ) ,
50- } ,
51- } ) ;
52- return user as AdapterUser ;
53- } ,
54-
55- async getUser ( id : string ) : Promise < AdapterUser | null > {
56- const user = await ql . entity ( 'User' ) . findUnique ( {
57- where : { id } ,
58- } ) ;
59- return user as AdapterUser | null ;
60- } ,
61-
62- async getUserByEmail ( email : string ) : Promise < AdapterUser | null > {
63- const user = await ql . entity ( 'User' ) . findUnique ( {
64- where : { email } ,
65- } ) ;
66- return user as AdapterUser | null ;
67- } ,
68-
69- async updateUser ( id : string , data : Partial < AdapterUser > ) : Promise < AdapterUser > {
70- const user = await ql . entity ( 'User' ) . update ( {
71- where : { id } ,
72- data : {
73- ...data ,
74- updatedAt : new Date ( ) ,
75- } ,
76- } ) ;
77- return user as AdapterUser ;
78- } ,
79-
80- async deleteUser ( id : string ) : Promise < void > {
81- await ql . entity ( 'User' ) . delete ( {
82- where : { id } ,
83- } ) ;
84- } ,
85-
86- // Session operations
87- async createSession ( data : AdapterSession ) : Promise < AdapterSession > {
88- const session = await ql . entity ( 'Session' ) . create ( {
89- data : {
90- id : data . id ,
91- userId : data . userId ,
92- expiresAt : data . expiresAt ,
93- token : data . token ,
94- ipAddress : data . ipAddress ?? null ,
95- userAgent : data . userAgent ?? null ,
96- createdAt : new Date ( ) ,
97- updatedAt : new Date ( ) ,
98- } ,
99- } ) ;
100- return session as AdapterSession ;
101- } ,
102-
103- async getSession ( token : string ) : Promise < AdapterSession | null > {
104- const session = await ql . entity ( 'Session' ) . findUnique ( {
105- where : { token } ,
106- } ) ;
107- return session as AdapterSession | null ;
108- } ,
109-
110- async updateSession ( token : string , data : Partial < AdapterSession > ) : Promise < AdapterSession > {
111- const session = await ql . entity ( 'Session' ) . update ( {
112- where : { token } ,
113- data : {
114- ...data ,
115- updatedAt : new Date ( ) ,
116- } ,
117- } ) ;
118- return session as AdapterSession ;
119- } ,
120-
121- async deleteSession ( token : string ) : Promise < void > {
122- await ql . entity ( 'Session' ) . delete ( {
123- where : { token } ,
124- } ) ;
125- } ,
126-
127- // Account operations
128- async createAccount ( data : AdapterAccount ) : Promise < AdapterAccount > {
129- const account = await ql . entity ( 'Account' ) . create ( {
130- data : {
131- id : data . id ,
132- userId : data . userId ,
133- accountId : data . accountId ,
134- providerId : data . providerId ,
135- accessToken : data . accessToken ?? null ,
136- refreshToken : data . refreshToken ?? null ,
137- idToken : data . idToken ?? null ,
138- expiresAt : data . expiresAt ?? null ,
139- scope : data . scope ?? null ,
140- password : data . password ?? null ,
141- createdAt : new Date ( ) ,
142- updatedAt : new Date ( ) ,
143- } ,
144- } ) ;
145- return account as AdapterAccount ;
146- } ,
147-
148- async getAccount ( providerId : string , accountId : string ) : Promise < AdapterAccount | null > {
149- const account = await ql . entity ( 'Account' ) . findFirst ( {
150- where : {
151- providerId,
152- accountId,
153- } ,
154- } ) ;
155- return account as AdapterAccount | null ;
156- } ,
157-
158- async updateAccount (
159- providerId : string ,
160- accountId : string ,
161- data : Partial < AdapterAccount >
162- ) : Promise < AdapterAccount > {
163- // Storage-agnostic approach: Find by composite fields, then update by ID
164- // This avoids Prisma-specific compound key syntax that may not work with all ObjectQL drivers
165- const existingAccount = await ql . entity ( 'Account' ) . findFirst ( {
166- where : {
167- providerId,
168- accountId,
169- } ,
170- } ) ;
28+ export function createObjectQLAdapter ( config : ObjectQLAdapterConfig ) {
29+ const { ql, debugLogs = false } = config ;
30+
31+ return ( _options : BetterAuthOptions ) => {
32+ // Model name mapping (better-auth uses lowercase model names)
33+ const modelMap : Record < string , string > = {
34+ user : 'User' ,
35+ session : 'Session' ,
36+ account : 'Account' ,
37+ verification : 'VerificationToken' ,
38+ } ;
39+
40+ const getModelName = ( model : string ) => modelMap [ model ] || model ;
41+
42+ return {
43+ id : 'objectql-adapter' ,
17144
172- if ( ! existingAccount ) {
173- throw new Error ( `Account not found: ${ providerId } /${ accountId } ` ) ;
174- }
175-
176- const account = await ql . entity ( 'Account' ) . update ( {
177- where : { id : existingAccount . id } ,
178- data : {
179- ...data ,
180- updatedAt : new Date ( ) ,
181- } ,
182- } ) ;
183- return account as AdapterAccount ;
184- } ,
185-
186- async deleteAccount ( providerId : string , accountId : string ) : Promise < void > {
187- // Use deleteMany with where clause for storage-agnostic deletion
188- await ql . entity ( 'Account' ) . deleteMany ( {
189- where : {
190- providerId,
191- accountId,
192- } ,
193- } ) ;
194- } ,
195-
196- // Verification token operations
197- async createVerificationToken ( data : AdapterVerificationToken ) : Promise < AdapterVerificationToken > {
198- const token = await ql . entity ( 'VerificationToken' ) . create ( {
199- data : {
200- id : data . id || generateId ( ) ,
201- identifier : data . identifier ,
202- token : data . token ,
203- expiresAt : data . expiresAt ,
204- createdAt : new Date ( ) ,
205- updatedAt : new Date ( ) ,
206- } ,
207- } ) ;
208- return token as AdapterVerificationToken ;
209- } ,
210-
211- async getVerificationToken ( identifier : string , token : string ) : Promise < AdapterVerificationToken | null > {
212- const verificationToken = await ql . entity ( 'VerificationToken' ) . findFirst ( {
213- where : {
214- identifier,
215- token,
216- } ,
217- } ) ;
218- return verificationToken as AdapterVerificationToken | null ;
219- } ,
220-
221- async deleteVerificationToken ( identifier : string , token : string ) : Promise < void > {
222- await ql . entity ( 'VerificationToken' ) . deleteMany ( {
223- where : {
224- identifier,
225- token,
226- } ,
227- } ) ;
228- } ,
45+ // Create a record in the specified model
46+ async create ( { model, data } : { model : string ; data : any } ) {
47+ const entityName = getModelName ( model ) ;
48+ if ( debugLogs ) console . log ( `[ObjectQL Adapter] create ${ entityName } :` , data ) ;
49+
50+ const result = await ql . entity ( entityName ) . create ( { data } ) ;
51+ return result ;
52+ } ,
53+
54+ // Find a single record matching the where clause
55+ async findOne ( { model, where } : { model : string ; where : any } ) {
56+ const entityName = getModelName ( model ) ;
57+ if ( debugLogs ) console . log ( `[ObjectQL Adapter] findOne ${ entityName } :` , where ) ;
58+
59+ // Try to use findUnique if there's a single unique field
60+ const whereKeys = Object . keys ( where ) ;
61+ if ( whereKeys . length === 1 ) {
62+ const result = await ql . entity ( entityName ) . findUnique ( { where } ) ;
63+ return result || null ;
64+ }
65+
66+ // Otherwise use findFirst for composite where clauses
67+ const result = await ql . entity ( entityName ) . findFirst ( { where } ) ;
68+ return result || null ;
69+ } ,
70+
71+ // Find multiple records matching the where clause
72+ async findMany ( { model, where, limit, offset, sortBy } : {
73+ model : string ;
74+ where ?: any ;
75+ limit ?: number ;
76+ offset ?: number ;
77+ sortBy ?: any ;
78+ } ) {
79+ const entityName = getModelName ( model ) ;
80+ if ( debugLogs ) console . log ( `[ObjectQL Adapter] findMany ${ entityName } :` , { where, limit, offset, sortBy } ) ;
81+
82+ const query : any = { } ;
83+ if ( where ) query . where = where ;
84+ if ( limit ) query . take = limit ;
85+ if ( offset ) query . skip = offset ;
86+ if ( sortBy ) query . orderBy = sortBy ;
87+
88+ const results = await ql . entity ( entityName ) . findMany ( query ) ;
89+ return results || [ ] ;
90+ } ,
91+
92+ // Update a record matching the where clause
93+ async update ( { model, where, update } : { model : string ; where : any ; update : any } ) {
94+ const entityName = getModelName ( model ) ;
95+ if ( debugLogs ) console . log ( `[ObjectQL Adapter] update ${ entityName } :` , { where, update } ) ;
96+
97+ const result = await ql . entity ( entityName ) . update ( {
98+ where,
99+ data : update ,
100+ } ) ;
101+ return result ;
102+ } ,
103+
104+ // Update multiple records matching the where clause
105+ async updateMany ( { model, where, update } : { model : string ; where : any ; update : any } ) {
106+ const entityName = getModelName ( model ) ;
107+ if ( debugLogs ) console . log ( `[ObjectQL Adapter] updateMany ${ entityName } :` , { where, update } ) ;
108+
109+ const result = await ql . entity ( entityName ) . updateMany ( {
110+ where,
111+ data : update ,
112+ } ) ;
113+ return result ;
114+ } ,
115+
116+ // Delete a record matching the where clause
117+ async delete ( { model, where } : { model : string ; where : any } ) {
118+ const entityName = getModelName ( model ) ;
119+ if ( debugLogs ) console . log ( `[ObjectQL Adapter] delete ${ entityName } :` , where ) ;
120+
121+ await ql . entity ( entityName ) . delete ( { where } ) ;
122+ } ,
123+
124+ // Delete multiple records matching the where clause
125+ async deleteMany ( { model, where } : { model : string ; where : any } ) {
126+ const entityName = getModelName ( model ) ;
127+ if ( debugLogs ) console . log ( `[ObjectQL Adapter] deleteMany ${ entityName } :` , where ) ;
128+
129+ await ql . entity ( entityName ) . deleteMany ( { where } ) ;
130+ } ,
131+
132+ // Count records matching the where clause
133+ async count ( { model, where } : { model : string ; where ?: any } ) {
134+ const entityName = getModelName ( model ) ;
135+ if ( debugLogs ) console . log ( `[ObjectQL Adapter] count ${ entityName } :` , where ) ;
136+
137+ const count = await ql . entity ( entityName ) . count ( { where } ) ;
138+ return count ;
139+ } ,
140+ } ;
229141 } ;
230142}
0 commit comments