Skip to content

Commit 1936d2c

Browse files
authored
Merge pull request #13 from objectstack-ai/copilot/check-authentication-logs
2 parents 4b5daf8 + 4a87ebf commit 1936d2c

File tree

5 files changed

+186
-230
lines changed

5 files changed

+186
-230
lines changed

src/adapter/index.ts

Lines changed: 123 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
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

1416
export 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
}

src/client/hooks.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,39 @@ export const {
5959

6060
/**
6161
* Type-safe session from client
62+
* Extended to include permissions from ObjectOS RBAC
6263
*/
63-
export type ObjectStackClientSession = InferSessionFromClient<typeof authClient>;
64+
export type ObjectStackClientSession = {
65+
user?: {
66+
id: string;
67+
email: string;
68+
name?: string;
69+
image?: string;
70+
emailVerified: boolean;
71+
permissions?: string[] | Record<string, boolean> | null;
72+
};
73+
session?: {
74+
id: string;
75+
expiresAt: Date;
76+
token: string;
77+
ipAddress?: string;
78+
userAgent?: string;
79+
};
80+
} | null;
6481

6582
/**
6683
* Convenience hook to access user permissions
6784
*/
6885
export function usePermissions() {
6986
const { data: session, isPending } = useSession();
7087

88+
// Safely extract permissions, defaulting to undefined if not present
89+
const permissions = session?.user && 'permissions' in session.user
90+
? (session.user as any).permissions
91+
: undefined;
92+
7193
return {
72-
permissions: session?.user?.permissions,
94+
permissions,
7395
isPending,
7496
isAuthenticated: !!session,
7597
};

0 commit comments

Comments
 (0)