Skip to content

Commit d746197

Browse files
Copilothotlong
andcommitted
Use better-auth native naming conventions for seamless migration
- Changed table names: auth_user → user, auth_session → session, etc. - Changed field names: email_verified → emailVerified, created_at → createdAt, etc. - Removed name conversion logic from adapter (no longer needed) - Updated documentation to reflect better-auth native schema usage - All tests passing (11/11) This ensures existing better-auth databases can migrate without schema changes. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 6080f06 commit d746197

7 files changed

Lines changed: 85 additions & 135 deletions

File tree

packages/plugins/plugin-auth/README.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ Authentication & Identity Plugin for ObjectStack.
3232
### ObjectQL-Based Database Architecture
3333
-**Native ObjectQL Data Persistence** - Uses ObjectQL's IDataEngine interface
3434
-**No Third-Party ORM** - No dependency on drizzle-orm or other ORMs
35+
-**Better-Auth Native Schema** - Uses better-auth's naming conventions for seamless migration
3536
-**Object Definitions** - Auth objects defined using ObjectStack's Object Protocol
36-
- `auth_user` - User accounts
37-
- `auth_session` - Active sessions
38-
- `auth_account` - OAuth provider accounts
39-
- `auth_verification` - Email/phone verification tokens
37+
- `user` - User accounts (better-auth native table name)
38+
- `session` - Active sessions (better-auth native table name)
39+
- `account` - OAuth provider accounts (better-auth native table name)
40+
- `verification` - Email/phone verification tokens (better-auth native table name)
4041
-**ObjectQL Adapter** - Custom adapter bridges better-auth to ObjectQL
4142

42-
The plugin uses [better-auth](https://www.better-auth.com/) for robust, production-ready authentication functionality. All requests are forwarded directly to better-auth's universal handler, ensuring full compatibility with all better-auth features. Data persistence is handled by ObjectQL, adhering to ObjectStack's "Data as Code" philosophy.
43+
The plugin uses [better-auth](https://www.better-auth.com/) for robust, production-ready authentication functionality. All requests are forwarded directly to better-auth's universal handler, ensuring full compatibility with all better-auth features. Data persistence is handled by ObjectQL using **better-auth's native naming conventions** (camelCase) to ensure seamless migration for existing better-auth users.
4344

4445
## Installation
4546

@@ -184,13 +185,16 @@ This architecture provides:
184185
The plugin uses **ObjectQL** for data persistence instead of third-party ORMs:
185186

186187
```typescript
187-
// Object definitions replace ORM schemas
188+
// Object definitions use better-auth's native naming conventions
188189
export const AuthUser = ObjectSchema.create({
189-
name: 'auth_user',
190+
name: 'user', // better-auth native table name
190191
fields: {
191192
id: Field.text({ label: 'User ID', required: true }),
192193
email: Field.email({ label: 'Email', required: true }),
194+
emailVerified: Field.boolean({ label: 'Email Verified' }), // camelCase
193195
name: Field.text({ label: 'Name', required: true }),
196+
createdAt: Field.datetime({ label: 'Created At' }), // camelCase
197+
updatedAt: Field.datetime({ label: 'Updated At' }), // camelCase
194198
// ... other fields
195199
},
196200
indexes: [
@@ -206,18 +210,20 @@ export const AuthUser = ObjectSchema.create({
206210
-**Type-Safe** - Zod-based schemas provide runtime + compile-time safety
207211
-**"Data as Code"** - Object definitions are versioned, declarative code
208212
-**Metadata Driven** - Supports migrations, validation, indexing via metadata
213+
-**Seamless Migration** - Uses better-auth's native naming (camelCase) for easy migration
209214

210215
**Database Objects:**
211-
- `auth_user` - User accounts (email, name, emailVerified, etc.)
212-
- `auth_session` - Active sessions (token, expiresAt, ipAddress, etc.)
213-
- `auth_account` - OAuth provider accounts (providerId, tokens, etc.)
214-
- `auth_verification` - Verification tokens (email, phone verification)
216+
Uses better-auth's native table and field names for compatibility:
217+
- `user` - User accounts (id, email, name, emailVerified, createdAt, etc.)
218+
- `session` - Active sessions (id, token, userId, expiresAt, ipAddress, etc.)
219+
- `account` - OAuth provider accounts (id, providerId, accountId, userId, tokens, etc.)
220+
- `verification` - Verification tokens (id, value, identifier, expiresAt, etc.)
215221

216222
**Adapter:**
217-
The `createObjectQLAdapter()` function bridges better-auth's database interface to ObjectQL's IDataEngine:
223+
The `createObjectQLAdapter()` function bridges better-auth's database interface to ObjectQL's IDataEngine using better-auth's native naming conventions:
218224

219225
```typescript
220-
// Better-auth → ObjectQL Adapter
226+
// Better-auth → ObjectQL Adapter (no name conversion needed)
221227
const adapter = createObjectQLAdapter(dataEngine);
222228

223229
// Better-auth uses this adapter for all database operations

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

Lines changed: 22 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -10,51 +10,21 @@ import type { CleanedWhere } from 'better-auth/adapters';
1010
* This allows better-auth to use ObjectQL for data persistence instead of
1111
* third-party ORMs like drizzle-orm.
1212
*
13+
* Uses better-auth's native naming conventions (camelCase) for seamless migration.
14+
*
1315
* @param dataEngine - ObjectQL data engine instance
1416
* @returns better-auth CustomAdapter
1517
*/
1618
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-
5019
/**
5120
* Convert better-auth where clause to ObjectQL query format
5221
*/
5322
function convertWhere(where: CleanedWhere[]): Record<string, any> {
5423
const filter: Record<string, any> = {};
5524

5625
for (const condition of where) {
57-
const fieldName = toFieldName(condition.field);
26+
// Use field names as-is (no conversion needed)
27+
const fieldName = condition.field;
5828

5929
if (condition.operator === 'eq') {
6030
filter[fieldName] = condition.value;
@@ -78,41 +48,19 @@ export function createObjectQLAdapter(dataEngine: IDataEngine) {
7848
return filter;
7949
}
8050

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-
10351
return {
10452
create: async <T extends Record<string, any>>({ model, data, select: _select }: { model: string; data: T; select?: string[] }): Promise<T> => {
105-
const objectName = toObjectName(model);
106-
const objectData = convertDataToObjectQL(data);
53+
// Use model name as-is (no conversion needed)
54+
const objectName = model;
10755

10856
// Note: select parameter is currently not supported by ObjectQL's insert operation
10957
// The full record is always returned after insertion
110-
const result = await dataEngine.insert(objectName, objectData);
111-
return convertDataFromObjectQL(result) as T;
58+
const result = await dataEngine.insert(objectName, data);
59+
return result as T;
11260
},
11361

11462
findOne: async <T>({ model, where, select, join: _join }: { model: string; where: CleanedWhere[]; select?: string[]; join?: any }): Promise<T | null> => {
115-
const objectName = toObjectName(model);
63+
const objectName = model;
11664
const filter = convertWhere(where);
11765

11866
// Note: join parameter is not currently supported by ObjectQL's findOne operation
@@ -121,21 +69,21 @@ export function createObjectQLAdapter(dataEngine: IDataEngine) {
12169

12270
const result = await dataEngine.findOne(objectName, {
12371
filter,
124-
select: select?.map(toFieldName),
72+
select,
12573
});
12674

127-
return result ? convertDataFromObjectQL(result) as T : null;
75+
return result ? result as T : null;
12876
},
12977

13078
findMany: async <T>({ model, where, limit, offset, sortBy, join: _join }: { model: string; where?: CleanedWhere[]; limit: number; offset?: number; sortBy?: { field: string; direction: 'asc' | 'desc' }; join?: any }): Promise<T[]> => {
131-
const objectName = toObjectName(model);
79+
const objectName = model;
13280
const filter = where ? convertWhere(where) : {};
13381

13482
// Note: join parameter is not currently supported by ObjectQL's find operation
13583
// Joins/populate functionality is planned for future ObjectQL releases
13684

13785
const sort = sortBy ? [{
138-
field: toFieldName(sortBy.field),
86+
field: sortBy.field,
13987
order: sortBy.direction as 'asc' | 'desc',
14088
}] : undefined;
14189

@@ -146,20 +94,19 @@ export function createObjectQLAdapter(dataEngine: IDataEngine) {
14694
sort,
14795
});
14896

149-
return results.map(r => convertDataFromObjectQL(r)) as T[];
97+
return results as T[];
15098
},
15199

152100
count: async ({ model, where }: { model: string; where?: CleanedWhere[] }): Promise<number> => {
153-
const objectName = toObjectName(model);
101+
const objectName = model;
154102
const filter = where ? convertWhere(where) : {};
155103

156104
return await dataEngine.count(objectName, { filter });
157105
},
158106

159107
update: async <T>({ model, where, update }: { model: string; where: CleanedWhere[]; update: Record<string, any> }): Promise<T | null> => {
160-
const objectName = toObjectName(model);
108+
const objectName = model;
161109
const filter = convertWhere(where);
162-
const updateData = convertDataToObjectQL(update);
163110

164111
// Find the record first to get its ID
165112
const record = await dataEngine.findOne(objectName, { filter });
@@ -168,17 +115,16 @@ export function createObjectQLAdapter(dataEngine: IDataEngine) {
168115
}
169116

170117
const result = await dataEngine.update(objectName, {
171-
...updateData,
118+
...update,
172119
id: record.id,
173120
});
174121

175-
return result ? convertDataFromObjectQL(result) as T : null;
122+
return result ? result as T : null;
176123
},
177124

178125
updateMany: async ({ model, where, update }: { model: string; where: CleanedWhere[]; update: Record<string, any> }): Promise<number> => {
179-
const objectName = toObjectName(model);
126+
const objectName = model;
180127
const filter = convertWhere(where);
181-
const updateData = convertDataToObjectQL(update);
182128

183129
// Note: Sequential updates are used here because ObjectQL's IDataEngine interface
184130
// requires an ID for updates. A future optimization could use a bulk update
@@ -190,7 +136,7 @@ export function createObjectQLAdapter(dataEngine: IDataEngine) {
190136
// Update each record
191137
for (const record of records) {
192138
await dataEngine.update(objectName, {
193-
...updateData,
139+
...update,
194140
id: record.id,
195141
});
196142
}
@@ -199,7 +145,7 @@ export function createObjectQLAdapter(dataEngine: IDataEngine) {
199145
},
200146

201147
delete: async ({ model, where }: { model: string; where: CleanedWhere[] }): Promise<void> => {
202-
const objectName = toObjectName(model);
148+
const objectName = model;
203149
const filter = convertWhere(where);
204150

205151
// Note: We need to find the record first to get its ID because ObjectQL's
@@ -214,7 +160,7 @@ export function createObjectQLAdapter(dataEngine: IDataEngine) {
214160
},
215161

216162
deleteMany: async ({ model, where }: { model: string; where: CleanedWhere[] }): Promise<number> => {
217-
const objectName = toObjectName(model);
163+
const objectName = model;
218164
const filter = convertWhere(where);
219165

220166
// Note: Sequential deletes are used here because ObjectQL's delete operation

packages/plugins/plugin-auth/src/objects/auth-account.object.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
55
/**
66
* Auth Account Object
77
*
8-
* Maps to better-auth's Account schema for OAuth providers:
8+
* Uses better-auth's native schema for seamless migration:
99
* - id: string
1010
* - createdAt: Date
1111
* - updatedAt: Date
1212
* - providerId: string (e.g., 'google', 'github')
1313
* - accountId: string (provider's user ID)
14-
* - userId: string (link to auth_user)
14+
* - userId: string (link to user table)
1515
* - accessToken: string | null
1616
* - refreshToken: string | null
1717
* - idToken: string | null
@@ -21,13 +21,13 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
2121
* - password: string | null (for email/password provider)
2222
*/
2323
export const AuthAccount = ObjectSchema.create({
24-
name: 'auth_account',
24+
name: 'account',
2525
label: 'Account',
2626
pluralLabel: 'Accounts',
2727
icon: 'link',
2828
description: 'OAuth and authentication provider accounts',
29-
titleFormat: '{provider_id} - {account_id}',
30-
compactLayout: ['provider_id', 'user_id', 'account_id'],
29+
titleFormat: '{providerId} - {accountId}',
30+
compactLayout: ['providerId', 'userId', 'accountId'],
3131

3232
fields: {
3333
id: Field.text({
@@ -36,57 +36,57 @@ export const AuthAccount = ObjectSchema.create({
3636
readonly: true,
3737
}),
3838

39-
created_at: Field.datetime({
39+
createdAt: Field.datetime({
4040
label: 'Created At',
4141
defaultValue: 'NOW()',
4242
readonly: true,
4343
}),
4444

45-
updated_at: Field.datetime({
45+
updatedAt: Field.datetime({
4646
label: 'Updated At',
4747
defaultValue: 'NOW()',
4848
readonly: true,
4949
}),
5050

51-
provider_id: Field.text({
51+
providerId: Field.text({
5252
label: 'Provider ID',
5353
required: true,
5454
description: 'OAuth provider identifier (google, github, etc.)',
5555
}),
5656

57-
account_id: Field.text({
57+
accountId: Field.text({
5858
label: 'Provider Account ID',
5959
required: true,
6060
description: "User's ID in the provider's system",
6161
}),
6262

63-
user_id: Field.text({
63+
userId: Field.text({
6464
label: 'User ID',
6565
required: true,
66-
description: 'Link to auth_user',
66+
description: 'Link to user table',
6767
}),
6868

69-
access_token: Field.textarea({
69+
accessToken: Field.textarea({
7070
label: 'Access Token',
7171
required: false,
7272
}),
7373

74-
refresh_token: Field.textarea({
74+
refreshToken: Field.textarea({
7575
label: 'Refresh Token',
7676
required: false,
7777
}),
7878

79-
id_token: Field.textarea({
79+
idToken: Field.textarea({
8080
label: 'ID Token',
8181
required: false,
8282
}),
8383

84-
access_token_expires_at: Field.datetime({
84+
accessTokenExpiresAt: Field.datetime({
8585
label: 'Access Token Expires At',
8686
required: false,
8787
}),
8888

89-
refresh_token_expires_at: Field.datetime({
89+
refreshTokenExpiresAt: Field.datetime({
9090
label: 'Refresh Token Expires At',
9191
required: false,
9292
}),
@@ -105,8 +105,8 @@ export const AuthAccount = ObjectSchema.create({
105105

106106
// Database indexes for performance
107107
indexes: [
108-
{ fields: ['user_id'], unique: false },
109-
{ fields: ['provider_id', 'account_id'], unique: true },
108+
{ fields: ['userId'], unique: false },
109+
{ fields: ['providerId', 'accountId'], unique: true },
110110
],
111111

112112
// Enable features

0 commit comments

Comments
 (0)