Skip to content

feat(plugin-auth): use better-auth modelName/fields mapping for snake_case compatibility#897

Merged
hotlong merged 5 commits into
mainfrom
copilot/enhancement-better-auth-model-mapping
Mar 10, 2026
Merged

feat(plugin-auth): use better-auth modelName/fields mapping for snake_case compatibility#897
hotlong merged 5 commits into
mainfrom
copilot/enhancement-better-auth-model-mapping

Conversation

Copilot AI commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

better-auth sends camelCase field names (emailVerified, userId, createdAt) to the adapter, but ObjectStack's protocol layer expects snake_case (email_verified, user_id, created_at). This causes query failures on sign-in, sign-up, and account lookup.

Approach

Leverage better-auth's official modelName / fields schema customization API combined with createAdapterFactory from better-auth/adapters. The factory wrapper handles all camelCase ↔ snake_case transformation automatically — no manual conversion in the adapter.

Changes

  • New auth-schema-config.ts — Mapping constants for all 4 core models (AUTH_USER_CONFIG, AUTH_SESSION_CONFIG, AUTH_ACCOUNT_CONFIG, AUTH_VERIFICATION_CONFIG) plus 7 plugin table schemas (AUTH_ORGANIZATION_SCHEMA, AUTH_MEMBER_SCHEMA, AUTH_INVITATION_SCHEMA, AUTH_TEAM_SCHEMA, AUTH_TEAM_MEMBER_SCHEMA, AUTH_TWO_FACTOR_SCHEMA, AUTH_ORG_SESSION_FIELDS) with helper builders buildOrganizationPluginSchema() and buildTwoFactorPluginSchema()
  • objectql-adapter.ts — New createObjectQLAdapterFactory() using createAdapterFactory; legacy createObjectQLAdapter() retained for backward compat
  • auth-manager.tscreateAuthInstance() now passes model/field config to betterAuth(); createDatabaseConfig() uses the new factory; new buildPluginList() conditionally registers organization, twoFactor, and magicLink plugins based on AuthPluginConfig flags, each with its own snake_case schema mapping
  • Tests — 53 tests covering core schema config validation, plugin schema mappings, factory creation, config propagation, and plugin registration verification (all passing)
  • Docs — README, authentication guide, ROADMAP updated

Plugin Table Coverage

In addition to the 4 core models, all plugin tables are mapped (per issue requirement "需要考虑所有插件用到的表,包括org等"):

Plugin Tables Key Field Mappings
Organization sys_organization, sys_member, sys_invitation, sys_team, sys_team_member organizationIdorganization_id, inviterIdinviter_id, activeOrganizationIdactive_organization_id, etc.
Two-Factor sys_two_factor + user field extension backupCodesbackup_codes, twoFactorEnabledtwo_factor_enabled
Magic Link (reuses verification table) No extra mapping needed

Example

// auth-schema-config.ts
export const AUTH_SESSION_CONFIG = {
  modelName: SystemObjectName.SESSION, // 'sys_session'
  fields: {
    userId: 'user_id',
    expiresAt: 'expires_at',
    ipAddress: 'ip_address',
    userAgent: 'user_agent',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
  },
} as const;

// auth-manager.ts — applied to betterAuth() config
const config: BetterAuthOptions = {
  database: createObjectQLAdapterFactory(dataEngine),
  session: { ...AUTH_SESSION_CONFIG, expiresIn: 604800 },
  user:    { ...AUTH_USER_CONFIG },
  account: { ...AUTH_ACCOUNT_CONFIG },
  verification: { ...AUTH_VERIFICATION_CONFIG },
  plugins: [
    organization({ schema: buildOrganizationPluginSchema() }),
    twoFactor({ schema: buildTwoFactorPluginSchema() }),
  ],
};

The factory reads the schema built by getAuthTables(options) and applies transformInput/transformOutput/transformWhereClause before data reaches the ObjectQL adapter — single-point mapping, no runtime conversion in adapter code.

Original prompt

This section details on the original issue you should resolve

<issue_title>[Enhancement] Use better-auth modelName and fields mapping for ObjectQL adapter (snake_case compatibility)</issue_title>
<issue_description>## Context
Currently, the ObjectQL adapter for better-auth passes field names as-is from better-auth's camelCase queries to ObjectStack's snake_case database schema. This causes query failures (e.g., login, account lookup) when field names don't match (see Bug objectstack-ai/spec#893).

Proposal

Implement Approach B: declare model/field mapping in the better-auth configuration using its official modelName and fields options for each core auth model.
需要考虑所有插件用到的表,包括org等

Details

Instead of runtime camelCase ↔ snake_case conversion in the adapter, leverage better-auth's documented schema customization:

export const auth = betterAuth({
  user: {
    modelName: "sys_user",
    fields: {
      emailVerified: "email_verified",
      createdAt: "created_at",
      updatedAt: "updated_at",
    },
  },
  session: {
    modelName: "sys_session",
    fields: {
      userId: "user_id",
      expiresAt: "expires_at",
      ipAddress: "ip_address",
      userAgent: "user_agent",
      createdAt: "created_at",
      updatedAt: "updated_at",
    },
  },
  account: {
    modelName: "sys_account",
    fields: {
      userId: "user_id",
      providerId: "provider_id",
      accountId: "account_id",
      accessToken: "access_token",
      refreshToken: "refresh_token",
      idToken: "id_token",
      accessTokenExpiresAt: "access_token_expires_at",
      refreshTokenExpiresAt: "refresh_token_expires_at",
      createdAt: "created_at",
      updatedAt: "updated_at",
    },
  },
  verification: {
    modelName: "sys_verification",
    fields: {
      expiresAt: "expires_at",
      createdAt: "created_at",
      updatedAt: "updated_at",
    },
  },
});

This ensures better-auth always uses the correct field names for data persistence and querying via ObjectQL.

Benefits

  • Single-point schema mapping, no runtime conversion
  • Cleaner adapter logic
  • Official/documented API usage
  • Reduces risk of edge-case bugs

Tasks

  • Add modelName and fields for all core models in better-auth config
  • Update auth-manager.ts to construct config accordingly
  • Review/adjust adapter and tests as needed
  • Validate sign-in, sign-up, and all account workflows
  • Document the mapping approach in README

相关问题: #893
</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel

vercel Bot commented Mar 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Mar 10, 2026 3:36am
spec Ready Ready Preview, Comment Mar 10, 2026 3:36am

Request Review

…_case compatibility

- Add auth-schema-config.ts with model/field mapping constants for all 4 core auth models
- Refactor objectql-adapter.ts to use createAdapterFactory from better-auth/adapters
- Update auth-manager.ts to pass user/session/account/verification config with modelName and fields
- Add tests for schema config, factory adapter, and config verification
- Update README and authentication guide documentation
- Keep legacy createObjectQLAdapter for backward compatibility

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
… ROADMAP

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Enhance ObjectQL adapter for better-auth field mapping feat(plugin-auth): use better-auth modelName/fields mapping for snake_case compatibility Mar 10, 2026
@hotlong
hotlong marked this pull request as ready for review March 10, 2026 03:15
Copilot AI review requested due to automatic review settings March 10, 2026 03:15
@hotlong

hotlong commented Mar 10, 2026

Copy link
Copy Markdown
Collaborator

@copilot 仔细评估是否完成了任务中要求的所有开放

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates @objectstack/plugin-auth to use better-auth’s official modelName / fields mapping along with createAdapterFactory, enabling automatic camelCase ↔ snake_case transformations so ObjectQL queries match ObjectStack’s snake_case auth objects.

Changes:

  • Added AUTH_*_CONFIG constants declaring model/field mappings for user/session/account/verification.
  • Introduced createObjectQLAdapterFactory() using createAdapterFactory and updated AuthManager to pass mapping config into betterAuth().
  • Updated tests and documentation to reflect the new mapping approach while retaining the legacy createObjectQLAdapter() for backward compatibility.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/plugins/plugin-auth/src/objectql-adapter.ts Adds factory-based adapter using createAdapterFactory; keeps legacy adapter.
packages/plugins/plugin-auth/src/objectql-adapter.test.ts Adds assertions for mapping constants and minimal factory test.
packages/plugins/plugin-auth/src/index.ts Exports new schema-config module from the package entrypoint.
packages/plugins/plugin-auth/src/auth-schema-config.ts Introduces AUTH_*_CONFIG constants for modelName/fields mapping.
packages/plugins/plugin-auth/src/auth-manager.ts Applies mapping constants to betterAuth() config and uses new adapter factory.
packages/plugins/plugin-auth/src/auth-manager.test.ts Updates tests to validate mapping propagation into betterAuth() config.
packages/plugins/plugin-auth/README.md Documents model/field mapping approach and adapter factory usage.
content/docs/guides/authentication.mdx Updates guide to explain modelName/fields mapping and adapter factory behavior.
ROADMAP.md Notes enhancement in migration section for the new mapping approach.

Comment on lines +233 to +246
```typescript
// Schema mapping constants (auth-schema-config.ts)
import {
AUTH_USER_CONFIG,
AUTH_SESSION_CONFIG,
AUTH_ACCOUNT_CONFIG,
AUTH_VERIFICATION_CONFIG,
} from '@objectstack/plugin-auth';

// Applied to the betterAuth() config:
const auth = betterAuth({
database: (options) => ({
id: 'objectql',
...adapter,
transaction: async (cb) => cb(adapter),
}),
// ... other config
database: createObjectQLAdapterFactory(dataEngine),
user: { ...AUTH_USER_CONFIG },
session: { ...AUTH_SESSION_CONFIG, expiresIn: 604800 },

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the README example, createObjectQLAdapterFactory(dataEngine) is used but not imported. This snippet won’t compile as-is; include createObjectQLAdapterFactory in the import list (or show a separate import) so readers can copy/paste successfully.

Copilot uses AI. Check for mistakes.
Comment on lines +586 to +587
better-auth internally uses camelCase model and field names (`user`, `emailVerified`, `userId`).
The plugin bridges this gap using better-auth's official **`modelName` / `fields` schema customisation API**:

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence says better-auth uses “camelCase model and field names”, but the model names shown (user, session, etc.) are not camelCase—only the field names are. Adjust wording to avoid confusion (e.g., “better-auth uses fixed model names like user and camelCase field names like emailVerified / userId”).

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +114
describe('createObjectQLAdapterFactory', () => {
it('should return a function (adapter factory)', () => {
const mockEngine = {
insert: vi.fn(),
findOne: vi.fn(),
find: vi.fn(),
count: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
} as unknown as IDataEngine;

const factory = createObjectQLAdapterFactory(mockEngine);
expect(typeof factory).toBe('function');
});
});

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createObjectQLAdapterFactory is only tested for returning a function. Since this PR relies on createAdapterFactory for correct modelName/fields transformations, it would be valuable to add a test that invokes the returned factory with a minimal BetterAuthOptions-like object and asserts that adapter calls into dataEngine with the expected transformed model name and snake_case fields/where-clause keys.

Copilot uses AI. Check for mistakes.
Copilot AI and others added 2 commits March 10, 2026 03:28
…agicLink plugins

- Add AUTH_ORGANIZATION_SCHEMA, AUTH_MEMBER_SCHEMA, AUTH_INVITATION_SCHEMA,
  AUTH_TEAM_SCHEMA, AUTH_TEAM_MEMBER_SCHEMA for organization plugin tables
- Add AUTH_TWO_FACTOR_SCHEMA and AUTH_TWO_FACTOR_USER_FIELDS for 2FA plugin
- Add AUTH_ORG_SESSION_FIELDS for org plugin session extensions
- Add buildOrganizationPluginSchema() and buildTwoFactorPluginSchema() helpers
- Wire up plugin registration in AuthManager.buildPluginList() based on
  AuthPluginConfig flags (organization, twoFactor, magicLink)
- Add 14 new tests for plugin schema configs and plugin registration
- Update README documentation with plugin table mappings

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>

Copilot AI commented Mar 10, 2026

Copy link
Copy Markdown
Contributor Author

@copilot 仔细评估是否完成了任务中要求的所有开放

Evaluated all open tasks from the issue. The original implementation only covered the 4 core models (user, session, account, verification). Per the issue requirement "需要考虑所有插件用到的表,包括org等", commit 4930f6e adds:

Organization plugin tables:

  • sys_organization (organization, member, invitation, team, teamMember) — all with snake_case field mappings
  • Session extension fields (active_organization_id, active_team_id)

Two-Factor plugin:

  • sys_two_factor with backup_codes/user_id mappings
  • User field extension (two_factor_enabled)

Magic Link plugin:

  • Registered with default no-op sendMagicLink handler (reuses verification table, no extra mapping needed)

Wiring:

  • AuthManager.buildPluginList() conditionally registers plugins based on AuthPluginConfig flags (organization, twoFactor, magicLink)
  • Each plugin receives its schema option with the correct snake_case mappings via buildOrganizationPluginSchema() / buildTwoFactorPluginSchema() helpers

All 53 tests pass, build succeeds, CodeQL clean.

@hotlong
hotlong merged commit 64b4c2e into main Mar 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement] Use better-auth modelName and fields mapping for ObjectQL adapter (snake_case compatibility)

3 participants