Skip to content

Commit 2ea215b

Browse files
Copilothotlong
andcommitted
Implement ObjectQL-based database objects for auth plugin
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 76a7625 commit 2ea215b

10 files changed

Lines changed: 673 additions & 27 deletions

File tree

packages/plugins/plugin-auth/package.json

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,5 @@
1818
"@types/node": "^25.2.2",
1919
"typescript": "^5.0.0",
2020
"vitest": "^4.0.18"
21-
},
22-
"peerDependencies": {
23-
"drizzle-orm": "^0.41.0"
24-
},
25-
"peerDependenciesMeta": {
26-
"drizzle-orm": {
27-
"optional": true
28-
}
2921
}
3022
}

packages/plugins/plugin-auth/src/auth-manager.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { betterAuth } from 'better-auth';
44
import type { Auth, BetterAuthOptions } from 'better-auth';
55
import type { AuthConfig } from '@objectstack/spec/system';
6+
import type { IDataEngine } from '@objectstack/core';
7+
import { createObjectQLAdapter } from './objectql-adapter.js';
68

79
/**
810
* Extended options for AuthManager
@@ -13,6 +15,12 @@ export interface AuthManagerOptions extends Partial<AuthConfig> {
1315
* If not provided, one will be created from config
1416
*/
1517
authInstance?: Auth<any>;
18+
19+
/**
20+
* ObjectQL Data Engine instance
21+
* Required for database operations using ObjectQL instead of third-party ORMs
22+
*/
23+
dataEngine?: IDataEngine;
1624
}
1725

1826
/**
@@ -82,25 +90,24 @@ export class AuthManager {
8290
}
8391

8492
/**
85-
* Create database configuration
86-
* TODO: Implement proper database adapter when drizzle-orm is available
93+
* Create database configuration using ObjectQL adapter
8794
*/
8895
private createDatabaseConfig(): any {
89-
// If databaseUrl is provided, we would use drizzle adapter
90-
// For now, this is a placeholder configuration
91-
if (this.config.databaseUrl) {
92-
console.warn(
93-
'Database URL provided but adapter integration not yet complete. ' +
94-
'Install drizzle-orm and configure a proper adapter for production use.'
95-
);
96+
// Use ObjectQL adapter if dataEngine is provided
97+
if (this.config.dataEngine) {
98+
return createObjectQLAdapter(this.config.dataEngine);
9699
}
97100

98-
// Return a minimal configuration that better-auth can work with
99-
// This will need to be replaced with a proper adapter
100-
return {
101-
// Placeholder - will be replaced with actual adapter
102-
adapter: 'in-memory' as any,
103-
};
101+
// Fallback warning if no dataEngine is provided
102+
console.warn(
103+
'⚠️ WARNING: No dataEngine provided to AuthManager! ' +
104+
'Using in-memory storage. This is NOT suitable for production. ' +
105+
'Please provide a dataEngine instance (e.g., ObjectQL) in AuthManagerOptions.'
106+
);
107+
108+
// Return a minimal in-memory configuration as fallback
109+
// This allows the system to work in development/testing without a real database
110+
return undefined; // better-auth will use its default in-memory adapter
104111
}
105112

106113
/**

packages/plugins/plugin-auth/src/auth-plugin.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { Plugin, PluginContext, IHttpServer } from '@objectstack/core';
44
import { AuthConfig } from '@objectstack/spec/system';
55
import { AuthManager } from './auth-manager.js';
6+
import { AuthUser, AuthSession, AuthAccount, AuthVerification } from './objects/index.js';
67

78
/**
89
* Auth Plugin Options
@@ -67,8 +68,17 @@ export class AuthPlugin implements Plugin {
6768
throw new Error('AuthPlugin: secret is required');
6869
}
6970

70-
// Initialize auth manager
71-
this.authManager = new AuthManager(this.options);
71+
// Get data engine service for database operations
72+
const dataEngine = ctx.getService<any>('data');
73+
if (!dataEngine) {
74+
ctx.logger.warn('No data engine service found - auth will use in-memory storage');
75+
}
76+
77+
// Initialize auth manager with data engine
78+
this.authManager = new AuthManager({
79+
...this.options,
80+
dataEngine,
81+
});
7282

7383
// Register auth service
7484
ctx.registerService('auth', this.authManager);

packages/plugins/plugin-auth/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
*
66
* Authentication & Identity Plugin for ObjectStack
77
* Powered by better-auth for robust, secure authentication
8+
* Uses ObjectQL for data persistence (no third-party ORM required)
89
*/
910

10-
export * from './auth-plugin';
11-
export * from './auth-manager';
11+
export * from './auth-plugin.js';
12+
export * from './auth-manager.js';
13+
export * from './objectql-adapter.js';
14+
export * from './objects/index.js';
1215
export type { AuthConfig, AuthProviderConfig, AuthPluginConfig } from '@objectstack/spec/system';
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { IDataEngine } from '@objectstack/core';
4+
import { createAdapter, type CleanedWhere, type JoinConfig } from 'better-auth/adapters';
5+
6+
/**
7+
* ObjectQL Adapter for better-auth
8+
*
9+
* Bridges better-auth's database adapter interface with ObjectQL's IDataEngine.
10+
* This allows better-auth to use ObjectQL for data persistence instead of
11+
* third-party ORMs like drizzle-orm.
12+
*
13+
* @param dataEngine - ObjectQL data engine instance
14+
* @returns better-auth Adapter instance
15+
*/
16+
export function createObjectQLAdapter(dataEngine: IDataEngine) {
17+
/**
18+
* Convert better-auth table names to ObjectQL object names
19+
* better-auth uses camelCase, ObjectQL uses snake_case
20+
*/
21+
function toObjectName(tableName: string): string {
22+
// Map better-auth table names to our object names
23+
const tableMap: Record<string, string> = {
24+
'user': 'auth_user',
25+
'session': 'auth_session',
26+
'account': 'auth_account',
27+
'verification': 'auth_verification',
28+
};
29+
return tableMap[tableName] || `auth_${tableName}`;
30+
}
31+
32+
/**
33+
* Convert better-auth field names to ObjectQL field names
34+
* better-auth uses camelCase, ObjectQL uses snake_case
35+
*/
36+
function toFieldName(fieldName: string): string {
37+
// Convert camelCase to snake_case
38+
return fieldName.replace(/([A-Z])/g, '_$1').toLowerCase();
39+
}
40+
41+
/**
42+
* Convert ObjectQL field names back to better-auth field names
43+
* ObjectQL uses snake_case, better-auth uses camelCase
44+
*/
45+
function fromFieldName(fieldName: string): string {
46+
// Convert snake_case to camelCase
47+
return fieldName.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
48+
}
49+
50+
/**
51+
* Convert better-auth where clause to ObjectQL query format
52+
*/
53+
function convertWhere(where: CleanedWhere[]): Record<string, any> {
54+
const filter: Record<string, any> = {};
55+
56+
for (const condition of where) {
57+
const fieldName = toFieldName(condition.field);
58+
59+
if (condition.operator === 'eq') {
60+
filter[fieldName] = condition.value;
61+
} else if (condition.operator === 'ne') {
62+
filter[fieldName] = { $ne: condition.value };
63+
} else if (condition.operator === 'in') {
64+
filter[fieldName] = { $in: condition.value };
65+
} else if (condition.operator === 'gt') {
66+
filter[fieldName] = { $gt: condition.value };
67+
} else if (condition.operator === 'gte') {
68+
filter[fieldName] = { $gte: condition.value };
69+
} else if (condition.operator === 'lt') {
70+
filter[fieldName] = { $lt: condition.value };
71+
} else if (condition.operator === 'lte') {
72+
filter[fieldName] = { $lte: condition.value };
73+
} else if (condition.operator === 'contains') {
74+
filter[fieldName] = { $regex: condition.value };
75+
}
76+
}
77+
78+
return filter;
79+
}
80+
81+
/**
82+
* Convert data from better-auth format to ObjectQL format
83+
*/
84+
function convertDataToObjectQL(data: Record<string, any>): Record<string, any> {
85+
const converted: Record<string, any> = {};
86+
for (const [key, value] of Object.entries(data)) {
87+
converted[toFieldName(key)] = value;
88+
}
89+
return converted;
90+
}
91+
92+
/**
93+
* Convert data from ObjectQL format to better-auth format
94+
*/
95+
function convertDataFromObjectQL(data: Record<string, any>): Record<string, any> {
96+
const converted: Record<string, any> = {};
97+
for (const [key, value] of Object.entries(data)) {
98+
converted[fromFieldName(key)] = value;
99+
}
100+
return converted;
101+
}
102+
103+
return createAdapter({
104+
id: 'objectql',
105+
106+
async create({ model, data, select }) {
107+
const objectName = toObjectName(model);
108+
const objectData = convertDataToObjectQL(data);
109+
110+
const result = await dataEngine.insert(objectName, objectData);
111+
return convertDataFromObjectQL(result);
112+
},
113+
114+
async findOne({ model, where, select, join }) {
115+
const objectName = toObjectName(model);
116+
const filter = convertWhere(where);
117+
118+
const fields = select?.map(toFieldName);
119+
120+
const result = await dataEngine.findOne(objectName, {
121+
filter,
122+
fields,
123+
});
124+
125+
return result ? convertDataFromObjectQL(result) : null;
126+
},
127+
128+
async findMany({ model, where, limit, offset, sortBy, join }) {
129+
const objectName = toObjectName(model);
130+
const filter = where ? convertWhere(where) : {};
131+
132+
const sort = sortBy ? {
133+
field: toFieldName(sortBy.field),
134+
direction: sortBy.direction,
135+
} : undefined;
136+
137+
const results = await dataEngine.find(objectName, {
138+
filter,
139+
limit: limit || 100,
140+
offset,
141+
sort: sort ? [sort] : undefined,
142+
});
143+
144+
return results.map(convertDataFromObjectQL);
145+
},
146+
147+
async count({ model, where }) {
148+
const objectName = toObjectName(model);
149+
const filter = where ? convertWhere(where) : {};
150+
151+
return await dataEngine.count(objectName, { filter });
152+
},
153+
154+
async update({ model, where, update }) {
155+
const objectName = toObjectName(model);
156+
const filter = convertWhere(where);
157+
const updateData = convertDataToObjectQL(update);
158+
159+
// Find the record first to get its ID
160+
const record = await dataEngine.findOne(objectName, { filter });
161+
if (!record) {
162+
return null;
163+
}
164+
165+
const result = await dataEngine.update(objectName, {
166+
...updateData,
167+
id: record.id,
168+
});
169+
170+
return result ? convertDataFromObjectQL(result) : null;
171+
},
172+
173+
async updateMany({ model, where, update }) {
174+
const objectName = toObjectName(model);
175+
const filter = convertWhere(where);
176+
const updateData = convertDataToObjectQL(update);
177+
178+
// Find all matching records
179+
const records = await dataEngine.find(objectName, { filter });
180+
181+
// Update each record
182+
for (const record of records) {
183+
await dataEngine.update(objectName, {
184+
...updateData,
185+
id: record.id,
186+
});
187+
}
188+
189+
return records.length;
190+
},
191+
192+
async delete({ model, where }) {
193+
const objectName = toObjectName(model);
194+
const filter = convertWhere(where);
195+
196+
// Find the record first to get its ID
197+
const record = await dataEngine.findOne(objectName, { filter });
198+
if (!record) {
199+
return;
200+
}
201+
202+
await dataEngine.delete(objectName, { filter: { id: record.id } });
203+
},
204+
205+
async deleteMany({ model, where }) {
206+
const objectName = toObjectName(model);
207+
const filter = convertWhere(where);
208+
209+
// Find all matching records
210+
const records = await dataEngine.find(objectName, { filter });
211+
212+
// Delete each record
213+
for (const record of records) {
214+
await dataEngine.delete(objectName, { filter: { id: record.id } });
215+
}
216+
217+
return records.length;
218+
},
219+
}, {
220+
// Adapter configuration
221+
adapterId: 'objectql',
222+
adapterName: 'ObjectQL',
223+
supportsNumericIds: false,
224+
supportsUUIDs: true,
225+
supportsJSON: true,
226+
supportsDates: true,
227+
supportsBooleans: true,
228+
supportsArrays: false,
229+
// ObjectQL handles ID generation
230+
disableIdGeneration: false,
231+
});
232+
}

0 commit comments

Comments
 (0)