#
tokens: 43969/50000 2/1098 files (page 57/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 57 of 69. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .github
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── e2e.yml
│       ├── preview.yml
│       └── release.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
│   └── settings.json
├── banner-dark.png
├── banner.png
├── biome.json
├── bump.config.ts
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── demo
│   ├── expo-example
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── app.config.ts
│   │   ├── assets
│   │   │   ├── bg-image.jpeg
│   │   │   ├── fonts
│   │   │   │   └── SpaceMono-Regular.ttf
│   │   │   ├── icon.png
│   │   │   └── images
│   │   │       ├── adaptive-icon.png
│   │   │       ├── favicon.png
│   │   │       ├── logo.png
│   │   │       ├── partial-react-logo.png
│   │   │       ├── react-logo.png
│   │   │       ├── [email protected]
│   │   │       ├── [email protected]
│   │   │       └── splash.png
│   │   ├── babel.config.js
│   │   ├── components.json
│   │   ├── expo-env.d.ts
│   │   ├── index.ts
│   │   ├── metro.config.js
│   │   ├── nativewind-env.d.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── app
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── api
│   │   │   │   │   └── auth
│   │   │   │   │       └── [...route]+api.ts
│   │   │   │   ├── dashboard.tsx
│   │   │   │   ├── forget-password.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── sign-up.tsx
│   │   │   ├── components
│   │   │   │   ├── icons
│   │   │   │   │   └── google.tsx
│   │   │   │   └── ui
│   │   │   │       ├── avatar.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── dialog.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       └── text.tsx
│   │   │   ├── global.css
│   │   │   └── lib
│   │   │       ├── auth-client.ts
│   │   │       ├── auth.ts
│   │   │       ├── icons
│   │   │       │   ├── iconWithClassName.ts
│   │   │       │   └── X.tsx
│   │   │       └── utils.ts
│   │   ├── tailwind.config.js
│   │   └── tsconfig.json
│   └── nextjs
│       ├── .env.example
│       ├── .gitignore
│       ├── app
│       │   ├── (auth)
│       │   │   ├── forget-password
│       │   │   │   └── page.tsx
│       │   │   ├── reset-password
│       │   │   │   └── page.tsx
│       │   │   ├── sign-in
│       │   │   │   ├── loading.tsx
│       │   │   │   └── page.tsx
│       │   │   └── two-factor
│       │   │       ├── otp
│       │   │       │   └── page.tsx
│       │   │       └── page.tsx
│       │   ├── accept-invitation
│       │   │   └── [id]
│       │   │       ├── invitation-error.tsx
│       │   │       └── page.tsx
│       │   ├── admin
│       │   │   └── page.tsx
│       │   ├── api
│       │   │   └── auth
│       │   │       └── [...all]
│       │   │           └── route.ts
│       │   ├── apps
│       │   │   └── register
│       │   │       └── page.tsx
│       │   ├── client-test
│       │   │   └── page.tsx
│       │   ├── dashboard
│       │   │   ├── change-plan.tsx
│       │   │   ├── client.tsx
│       │   │   ├── organization-card.tsx
│       │   │   ├── page.tsx
│       │   │   ├── upgrade-button.tsx
│       │   │   └── user-card.tsx
│       │   ├── device
│       │   │   ├── approve
│       │   │   │   └── page.tsx
│       │   │   ├── denied
│       │   │   │   └── page.tsx
│       │   │   ├── layout.tsx
│       │   │   ├── page.tsx
│       │   │   └── success
│       │   │       └── page.tsx
│       │   ├── favicon.ico
│       │   ├── features.tsx
│       │   ├── fonts
│       │   │   ├── GeistMonoVF.woff
│       │   │   └── GeistVF.woff
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   ├── oauth
│       │   │   └── authorize
│       │   │       ├── concet-buttons.tsx
│       │   │       └── page.tsx
│       │   ├── page.tsx
│       │   └── pricing
│       │       └── page.tsx
│       ├── components
│       │   ├── account-switch.tsx
│       │   ├── blocks
│       │   │   └── pricing.tsx
│       │   ├── logo.tsx
│       │   ├── one-tap.tsx
│       │   ├── sign-in-btn.tsx
│       │   ├── sign-in.tsx
│       │   ├── sign-up.tsx
│       │   ├── theme-provider.tsx
│       │   ├── theme-toggle.tsx
│       │   ├── tier-labels.tsx
│       │   ├── ui
│       │   │   ├── accordion.tsx
│       │   │   ├── alert-dialog.tsx
│       │   │   ├── alert.tsx
│       │   │   ├── aspect-ratio.tsx
│       │   │   ├── avatar.tsx
│       │   │   ├── badge.tsx
│       │   │   ├── breadcrumb.tsx
│       │   │   ├── button.tsx
│       │   │   ├── calendar.tsx
│       │   │   ├── card.tsx
│       │   │   ├── carousel.tsx
│       │   │   ├── chart.tsx
│       │   │   ├── checkbox.tsx
│       │   │   ├── collapsible.tsx
│       │   │   ├── command.tsx
│       │   │   ├── context-menu.tsx
│       │   │   ├── copy-button.tsx
│       │   │   ├── dialog.tsx
│       │   │   ├── drawer.tsx
│       │   │   ├── dropdown-menu.tsx
│       │   │   ├── form.tsx
│       │   │   ├── hover-card.tsx
│       │   │   ├── input-otp.tsx
│       │   │   ├── input.tsx
│       │   │   ├── label.tsx
│       │   │   ├── menubar.tsx
│       │   │   ├── navigation-menu.tsx
│       │   │   ├── pagination.tsx
│       │   │   ├── password-input.tsx
│       │   │   ├── popover.tsx
│       │   │   ├── progress.tsx
│       │   │   ├── radio-group.tsx
│       │   │   ├── resizable.tsx
│       │   │   ├── scroll-area.tsx
│       │   │   ├── select.tsx
│       │   │   ├── separator.tsx
│       │   │   ├── sheet.tsx
│       │   │   ├── skeleton.tsx
│       │   │   ├── slider.tsx
│       │   │   ├── sonner.tsx
│       │   │   ├── switch.tsx
│       │   │   ├── table.tsx
│       │   │   ├── tabs.tsx
│       │   │   ├── tabs2.tsx
│       │   │   ├── textarea.tsx
│       │   │   ├── toast.tsx
│       │   │   ├── toaster.tsx
│       │   │   ├── toggle-group.tsx
│       │   │   ├── toggle.tsx
│       │   │   └── tooltip.tsx
│       │   └── wrapper.tsx
│       ├── components.json
│       ├── hooks
│       │   └── use-toast.ts
│       ├── lib
│       │   ├── auth-client.ts
│       │   ├── auth-types.ts
│       │   ├── auth.ts
│       │   ├── email
│       │   │   ├── invitation.tsx
│       │   │   ├── resend.ts
│       │   │   └── reset-password.tsx
│       │   ├── metadata.ts
│       │   ├── shared.ts
│       │   └── utils.ts
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── proxy.ts
│       ├── public
│       │   ├── __og.png
│       │   ├── _og.png
│       │   ├── favicon
│       │   │   ├── android-chrome-192x192.png
│       │   │   ├── android-chrome-512x512.png
│       │   │   ├── apple-touch-icon.png
│       │   │   ├── favicon-16x16.png
│       │   │   ├── favicon-32x32.png
│       │   │   ├── favicon.ico
│       │   │   ├── light
│       │   │   │   ├── android-chrome-192x192.png
│       │   │   │   ├── android-chrome-512x512.png
│       │   │   │   ├── apple-touch-icon.png
│       │   │   │   ├── favicon-16x16.png
│       │   │   │   ├── favicon-32x32.png
│       │   │   │   ├── favicon.ico
│       │   │   │   └── site.webmanifest
│       │   │   └── site.webmanifest
│       │   ├── logo.svg
│       │   └── og.png
│       ├── README.md
│       ├── tailwind.config.ts
│       ├── tsconfig.json
│       └── turbo.json
├── docker-compose.yml
├── docs
│   ├── .env.example
│   ├── .gitignore
│   ├── app
│   │   ├── api
│   │   │   ├── ai-chat
│   │   │   │   └── route.ts
│   │   │   ├── analytics
│   │   │   │   ├── conversation
│   │   │   │   │   └── route.ts
│   │   │   │   ├── event
│   │   │   │   │   └── route.ts
│   │   │   │   └── feedback
│   │   │   │       └── route.ts
│   │   │   ├── chat
│   │   │   │   └── route.ts
│   │   │   ├── og
│   │   │   │   └── route.tsx
│   │   │   ├── og-release
│   │   │   │   └── route.tsx
│   │   │   ├── search
│   │   │   │   └── route.ts
│   │   │   └── support
│   │   │       └── route.ts
│   │   ├── blog
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── blog-list.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   ├── stat-field.tsx
│   │   │   │   └── support.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── changelogs
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── grid-pattern.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   └── stat-field.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── community
│   │   │   ├── _components
│   │   │   │   ├── header.tsx
│   │   │   │   └── stats.tsx
│   │   │   └── page.tsx
│   │   ├── docs
│   │   │   ├── [[...slug]]
│   │   │   │   ├── page.client.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── layout.tsx
│   │   │   └── lib
│   │   │       └── get-llm-text.ts
│   │   ├── global.css
│   │   ├── layout.config.tsx
│   │   ├── layout.tsx
│   │   ├── llms.txt
│   │   │   ├── [...slug]
│   │   │   │   └── route.ts
│   │   │   └── route.ts
│   │   ├── not-found.tsx
│   │   ├── page.tsx
│   │   ├── reference
│   │   │   └── route.ts
│   │   ├── sitemap.xml
│   │   ├── static.json
│   │   │   └── route.ts
│   │   └── v1
│   │       ├── _components
│   │       │   └── v1-text.tsx
│   │       ├── bg-line.tsx
│   │       └── page.tsx
│   ├── assets
│   │   ├── Geist.ttf
│   │   └── GeistMono.ttf
│   ├── components
│   │   ├── ai-chat-modal.tsx
│   │   ├── anchor-scroll-fix.tsx
│   │   ├── api-method-tabs.tsx
│   │   ├── api-method.tsx
│   │   ├── banner.tsx
│   │   ├── blocks
│   │   │   └── features.tsx
│   │   ├── builder
│   │   │   ├── beam.tsx
│   │   │   ├── code-tabs
│   │   │   │   ├── code-editor.tsx
│   │   │   │   ├── code-tabs.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── tab-bar.tsx
│   │   │   │   └── theme.ts
│   │   │   ├── index.tsx
│   │   │   ├── sign-in.tsx
│   │   │   ├── sign-up.tsx
│   │   │   ├── social-provider.tsx
│   │   │   ├── store.ts
│   │   │   └── tabs.tsx
│   │   ├── display-techstack.tsx
│   │   ├── divider-text.tsx
│   │   ├── docs
│   │   │   ├── docs.client.tsx
│   │   │   ├── docs.tsx
│   │   │   ├── layout
│   │   │   │   ├── nav.tsx
│   │   │   │   ├── theme-toggle.tsx
│   │   │   │   ├── toc-thumb.tsx
│   │   │   │   └── toc.tsx
│   │   │   ├── page.client.tsx
│   │   │   ├── page.tsx
│   │   │   ├── shared.tsx
│   │   │   └── ui
│   │   │       ├── button.tsx
│   │   │       ├── collapsible.tsx
│   │   │       ├── popover.tsx
│   │   │       └── scroll-area.tsx
│   │   ├── endpoint.tsx
│   │   ├── features.tsx
│   │   ├── floating-ai-search.tsx
│   │   ├── fork-button.tsx
│   │   ├── generate-apple-jwt.tsx
│   │   ├── generate-secret.tsx
│   │   ├── github-stat.tsx
│   │   ├── icons.tsx
│   │   ├── landing
│   │   │   ├── gradient-bg.tsx
│   │   │   ├── grid-pattern.tsx
│   │   │   ├── hero.tsx
│   │   │   ├── section-svg.tsx
│   │   │   ├── section.tsx
│   │   │   ├── spotlight.tsx
│   │   │   └── testimonials.tsx
│   │   ├── logo-context-menu.tsx
│   │   ├── logo.tsx
│   │   ├── markdown-renderer.tsx
│   │   ├── markdown.tsx
│   │   ├── mdx
│   │   │   ├── add-to-cursor.tsx
│   │   │   └── database-tables.tsx
│   │   ├── message-feedback.tsx
│   │   ├── mobile-search-icon.tsx
│   │   ├── nav-bar.tsx
│   │   ├── nav-link.tsx
│   │   ├── nav-mobile.tsx
│   │   ├── promo-card.tsx
│   │   ├── resource-card.tsx
│   │   ├── resource-grid.tsx
│   │   ├── resource-section.tsx
│   │   ├── ripple.tsx
│   │   ├── search-dialog.tsx
│   │   ├── side-bar.tsx
│   │   ├── sidebar-content.tsx
│   │   ├── techstack-icons.tsx
│   │   ├── theme-provider.tsx
│   │   ├── theme-toggler.tsx
│   │   └── ui
│   │       ├── accordion.tsx
│   │       ├── alert-dialog.tsx
│   │       ├── alert.tsx
│   │       ├── aside-link.tsx
│   │       ├── aspect-ratio.tsx
│   │       ├── avatar.tsx
│   │       ├── background-beams.tsx
│   │       ├── background-boxes.tsx
│   │       ├── badge.tsx
│   │       ├── breadcrumb.tsx
│   │       ├── button.tsx
│   │       ├── calendar.tsx
│   │       ├── callout.tsx
│   │       ├── card.tsx
│   │       ├── carousel.tsx
│   │       ├── chart.tsx
│   │       ├── checkbox.tsx
│   │       ├── code-block.tsx
│   │       ├── collapsible.tsx
│   │       ├── command.tsx
│   │       ├── context-menu.tsx
│   │       ├── dialog.tsx
│   │       ├── drawer.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── dynamic-code-block.tsx
│   │       ├── fade-in.tsx
│   │       ├── form.tsx
│   │       ├── hover-card.tsx
│   │       ├── input-otp.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── menubar.tsx
│   │       ├── navigation-menu.tsx
│   │       ├── pagination.tsx
│   │       ├── popover.tsx
│   │       ├── progress.tsx
│   │       ├── radio-group.tsx
│   │       ├── resizable.tsx
│   │       ├── scroll-area.tsx
│   │       ├── select.tsx
│   │       ├── separator.tsx
│   │       ├── sheet.tsx
│   │       ├── sidebar.tsx
│   │       ├── skeleton.tsx
│   │       ├── slider.tsx
│   │       ├── sonner.tsx
│   │       ├── sparkles.tsx
│   │       ├── switch.tsx
│   │       ├── table.tsx
│   │       ├── tabs.tsx
│   │       ├── textarea.tsx
│   │       ├── toggle-group.tsx
│   │       ├── toggle.tsx
│   │       ├── tooltip-docs.tsx
│   │       ├── tooltip.tsx
│   │       └── use-copy-button.tsx
│   ├── components.json
│   ├── content
│   │   ├── blogs
│   │   │   ├── 0-supabase-auth-to-planetscale-migration.mdx
│   │   │   ├── 1-3.mdx
│   │   │   ├── authjs-joins-better-auth.mdx
│   │   │   └── seed-round.mdx
│   │   ├── changelogs
│   │   │   ├── 1-2.mdx
│   │   │   └── 1.0.mdx
│   │   └── docs
│   │       ├── adapters
│   │       │   ├── community-adapters.mdx
│   │       │   ├── drizzle.mdx
│   │       │   ├── mongo.mdx
│   │       │   ├── mssql.mdx
│   │       │   ├── mysql.mdx
│   │       │   ├── other-relational-databases.mdx
│   │       │   ├── postgresql.mdx
│   │       │   ├── prisma.mdx
│   │       │   └── sqlite.mdx
│   │       ├── authentication
│   │       │   ├── apple.mdx
│   │       │   ├── atlassian.mdx
│   │       │   ├── cognito.mdx
│   │       │   ├── discord.mdx
│   │       │   ├── dropbox.mdx
│   │       │   ├── email-password.mdx
│   │       │   ├── facebook.mdx
│   │       │   ├── figma.mdx
│   │       │   ├── github.mdx
│   │       │   ├── gitlab.mdx
│   │       │   ├── google.mdx
│   │       │   ├── huggingface.mdx
│   │       │   ├── kakao.mdx
│   │       │   ├── kick.mdx
│   │       │   ├── line.mdx
│   │       │   ├── linear.mdx
│   │       │   ├── linkedin.mdx
│   │       │   ├── microsoft.mdx
│   │       │   ├── naver.mdx
│   │       │   ├── notion.mdx
│   │       │   ├── other-social-providers.mdx
│   │       │   ├── paypal.mdx
│   │       │   ├── reddit.mdx
│   │       │   ├── roblox.mdx
│   │       │   ├── salesforce.mdx
│   │       │   ├── slack.mdx
│   │       │   ├── spotify.mdx
│   │       │   ├── tiktok.mdx
│   │       │   ├── twitch.mdx
│   │       │   ├── twitter.mdx
│   │       │   ├── vk.mdx
│   │       │   └── zoom.mdx
│   │       ├── basic-usage.mdx
│   │       ├── comparison.mdx
│   │       ├── concepts
│   │       │   ├── api.mdx
│   │       │   ├── cli.mdx
│   │       │   ├── client.mdx
│   │       │   ├── cookies.mdx
│   │       │   ├── database.mdx
│   │       │   ├── email.mdx
│   │       │   ├── hooks.mdx
│   │       │   ├── oauth.mdx
│   │       │   ├── plugins.mdx
│   │       │   ├── rate-limit.mdx
│   │       │   ├── session-management.mdx
│   │       │   ├── typescript.mdx
│   │       │   └── users-accounts.mdx
│   │       ├── examples
│   │       │   ├── astro.mdx
│   │       │   ├── next-js.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   └── svelte-kit.mdx
│   │       ├── guides
│   │       │   ├── auth0-migration-guide.mdx
│   │       │   ├── browser-extension-guide.mdx
│   │       │   ├── clerk-migration-guide.mdx
│   │       │   ├── create-a-db-adapter.mdx
│   │       │   ├── next-auth-migration-guide.mdx
│   │       │   ├── optimizing-for-performance.mdx
│   │       │   ├── saml-sso-with-okta.mdx
│   │       │   ├── supabase-migration-guide.mdx
│   │       │   └── your-first-plugin.mdx
│   │       ├── installation.mdx
│   │       ├── integrations
│   │       │   ├── astro.mdx
│   │       │   ├── convex.mdx
│   │       │   ├── elysia.mdx
│   │       │   ├── expo.mdx
│   │       │   ├── express.mdx
│   │       │   ├── fastify.mdx
│   │       │   ├── hono.mdx
│   │       │   ├── lynx.mdx
│   │       │   ├── nestjs.mdx
│   │       │   ├── next.mdx
│   │       │   ├── nitro.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   ├── solid-start.mdx
│   │       │   ├── svelte-kit.mdx
│   │       │   ├── tanstack.mdx
│   │       │   └── waku.mdx
│   │       ├── introduction.mdx
│   │       ├── meta.json
│   │       ├── plugins
│   │       │   ├── 2fa.mdx
│   │       │   ├── admin.mdx
│   │       │   ├── anonymous.mdx
│   │       │   ├── api-key.mdx
│   │       │   ├── autumn.mdx
│   │       │   ├── bearer.mdx
│   │       │   ├── captcha.mdx
│   │       │   ├── community-plugins.mdx
│   │       │   ├── device-authorization.mdx
│   │       │   ├── dodopayments.mdx
│   │       │   ├── dub.mdx
│   │       │   ├── email-otp.mdx
│   │       │   ├── generic-oauth.mdx
│   │       │   ├── have-i-been-pwned.mdx
│   │       │   ├── jwt.mdx
│   │       │   ├── last-login-method.mdx
│   │       │   ├── magic-link.mdx
│   │       │   ├── mcp.mdx
│   │       │   ├── multi-session.mdx
│   │       │   ├── oauth-proxy.mdx
│   │       │   ├── oidc-provider.mdx
│   │       │   ├── one-tap.mdx
│   │       │   ├── one-time-token.mdx
│   │       │   ├── open-api.mdx
│   │       │   ├── organization.mdx
│   │       │   ├── passkey.mdx
│   │       │   ├── phone-number.mdx
│   │       │   ├── polar.mdx
│   │       │   ├── siwe.mdx
│   │       │   ├── sso.mdx
│   │       │   ├── stripe.mdx
│   │       │   └── username.mdx
│   │       └── reference
│   │           ├── contributing.mdx
│   │           ├── faq.mdx
│   │           ├── options.mdx
│   │           ├── resources.mdx
│   │           ├── security.mdx
│   │           └── telemetry.mdx
│   ├── hooks
│   │   └── use-mobile.ts
│   ├── ignore-build.sh
│   ├── lib
│   │   ├── blog.ts
│   │   ├── chat
│   │   │   └── inkeep-qa-schema.ts
│   │   ├── constants.ts
│   │   ├── export-search-indexes.ts
│   │   ├── inkeep-analytics.ts
│   │   ├── is-active.ts
│   │   ├── metadata.ts
│   │   ├── source.ts
│   │   └── utils.ts
│   ├── next.config.js
│   ├── package.json
│   ├── postcss.config.js
│   ├── proxy.ts
│   ├── public
│   │   ├── avatars
│   │   │   └── beka.jpg
│   │   ├── blogs
│   │   │   ├── authjs-joins.png
│   │   │   ├── seed-round.png
│   │   │   └── supabase-ps.png
│   │   ├── branding
│   │   │   ├── better-auth-brand-assets.zip
│   │   │   ├── better-auth-logo-dark.png
│   │   │   ├── better-auth-logo-dark.svg
│   │   │   ├── better-auth-logo-light.png
│   │   │   ├── better-auth-logo-light.svg
│   │   │   ├── better-auth-logo-wordmark-dark.png
│   │   │   ├── better-auth-logo-wordmark-dark.svg
│   │   │   ├── better-auth-logo-wordmark-light.png
│   │   │   └── better-auth-logo-wordmark-light.svg
│   │   ├── extension-id.png
│   │   ├── favicon
│   │   │   ├── android-chrome-192x192.png
│   │   │   ├── android-chrome-512x512.png
│   │   │   ├── apple-touch-icon.png
│   │   │   ├── favicon-16x16.png
│   │   │   ├── favicon-32x32.png
│   │   │   ├── favicon.ico
│   │   │   ├── light
│   │   │   │   ├── android-chrome-192x192.png
│   │   │   │   ├── android-chrome-512x512.png
│   │   │   │   ├── apple-touch-icon.png
│   │   │   │   ├── favicon-16x16.png
│   │   │   │   ├── favicon-32x32.png
│   │   │   │   ├── favicon.ico
│   │   │   │   └── site.webmanifest
│   │   │   └── site.webmanifest
│   │   ├── images
│   │   │   └── blogs
│   │   │       └── better auth (1).png
│   │   ├── logo.png
│   │   ├── logo.svg
│   │   ├── LogoDark.webp
│   │   ├── LogoLight.webp
│   │   ├── og.png
│   │   ├── open-api-reference.png
│   │   ├── people-say
│   │   │   ├── code-with-antonio.jpg
│   │   │   ├── dagmawi-babi.png
│   │   │   ├── dax.png
│   │   │   ├── dev-ed.png
│   │   │   ├── egoist.png
│   │   │   ├── guillermo-rauch.png
│   │   │   ├── jonathan-wilke.png
│   │   │   ├── josh-tried-coding.jpg
│   │   │   ├── kitze.jpg
│   │   │   ├── lazar-nikolov.png
│   │   │   ├── nizzy.png
│   │   │   ├── omar-mcadam.png
│   │   │   ├── ryan-vogel.jpg
│   │   │   ├── saltyatom.jpg
│   │   │   ├── sebastien-chopin.png
│   │   │   ├── shreyas-mididoddi.png
│   │   │   ├── tech-nerd.png
│   │   │   ├── theo.png
│   │   │   ├── vybhav-bhargav.png
│   │   │   └── xavier-pladevall.jpg
│   │   ├── plus.svg
│   │   ├── release-og
│   │   │   ├── 1-2.png
│   │   │   ├── 1-3.png
│   │   │   └── changelog-og.png
│   │   └── v1-og.png
│   ├── README.md
│   ├── scripts
│   │   ├── endpoint-to-doc
│   │   │   ├── index.ts
│   │   │   ├── input.ts
│   │   │   ├── output.mdx
│   │   │   └── readme.md
│   │   └── sync-orama.ts
│   ├── source.config.ts
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   └── turbo.json
├── e2e
│   ├── integration
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── solid-vinxi
│   │   │   ├── .gitignore
│   │   │   ├── app.config.ts
│   │   │   ├── e2e
│   │   │   │   ├── test.spec.ts
│   │   │   │   └── utils.ts
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── app.tsx
│   │   │   │   ├── entry-client.tsx
│   │   │   │   ├── entry-server.tsx
│   │   │   │   ├── global.d.ts
│   │   │   │   ├── lib
│   │   │   │   │   ├── auth-client.ts
│   │   │   │   │   └── auth.ts
│   │   │   │   └── routes
│   │   │   │       ├── [...404].tsx
│   │   │   │       ├── api
│   │   │   │       │   └── auth
│   │   │   │       │       └── [...all].ts
│   │   │   │       └── index.tsx
│   │   │   └── tsconfig.json
│   │   ├── test-utils
│   │   │   ├── package.json
│   │   │   └── src
│   │   │       └── playwright.ts
│   │   └── vanilla-node
│   │       ├── e2e
│   │       │   ├── app.ts
│   │       │   ├── domain.spec.ts
│   │       │   ├── postgres-js.spec.ts
│   │       │   ├── test.spec.ts
│   │       │   └── utils.ts
│   │       ├── index.html
│   │       ├── package.json
│   │       ├── src
│   │       │   ├── main.ts
│   │       │   └── vite-env.d.ts
│   │       ├── tsconfig.json
│   │       └── vite.config.ts
│   └── smoke
│       ├── package.json
│       ├── test
│       │   ├── bun.spec.ts
│       │   ├── cloudflare.spec.ts
│       │   ├── deno.spec.ts
│       │   ├── fixtures
│       │   │   ├── bun-simple.ts
│       │   │   ├── cloudflare
│       │   │   │   ├── .gitignore
│       │   │   │   ├── drizzle
│       │   │   │   │   ├── 0000_clean_vector.sql
│       │   │   │   │   └── meta
│       │   │   │   │       ├── _journal.json
│       │   │   │   │       └── 0000_snapshot.json
│       │   │   │   ├── drizzle.config.ts
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── auth-schema.ts
│       │   │   │   │   ├── db.ts
│       │   │   │   │   └── index.ts
│       │   │   │   ├── test
│       │   │   │   │   ├── apply-migrations.ts
│       │   │   │   │   ├── env.d.ts
│       │   │   │   │   └── index.test.ts
│       │   │   │   ├── tsconfig.json
│       │   │   │   ├── vitest.config.ts
│       │   │   │   ├── worker-configuration.d.ts
│       │   │   │   └── wrangler.json
│       │   │   ├── deno-simple.ts
│       │   │   ├── tsconfig-decelration
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── demo.ts
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-exact-optional-property-types
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── index.ts
│       │   │   │   │   ├── organization.ts
│       │   │   │   │   ├── user-additional-fields.ts
│       │   │   │   │   └── username.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-isolated-module-bundler
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-verbatim-module-syntax-node10
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   └── vite
│       │   │       ├── package.json
│       │   │       ├── src
│       │   │       │   ├── client.ts
│       │   │       │   └── server.ts
│       │   │       ├── tsconfig.json
│       │   │       └── vite.config.ts
│       │   ├── ssr.ts
│       │   ├── typecheck.spec.ts
│       │   └── vite.spec.ts
│       └── tsconfig.json
├── LICENSE.md
├── package.json
├── packages
│   ├── better-auth
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── __snapshots__
│   │   │   │   └── init.test.ts.snap
│   │   │   ├── adapters
│   │   │   │   ├── adapter-factory
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── __snapshots__
│   │   │   │   │   │   │   └── adapter-factory.test.ts.snap
│   │   │   │   │   │   └── adapter-factory.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── create-test-suite.ts
│   │   │   │   ├── drizzle-adapter
│   │   │   │   │   ├── drizzle-adapter.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── adapter.drizzle.mysql.test.ts
│   │   │   │   │       ├── adapter.drizzle.pg.test.ts
│   │   │   │   │       ├── adapter.drizzle.sqlite.test.ts
│   │   │   │   │       └── generate-schema.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely-adapter
│   │   │   │   │   ├── bun-sqlite-dialect.ts
│   │   │   │   │   ├── dialect.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── kysely-adapter.ts
│   │   │   │   │   ├── node-sqlite-dialect.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── adapter.kysely.mssql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.mysql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.pg.test.ts
│   │   │   │   │   │   ├── adapter.kysely.sqlite.test.ts
│   │   │   │   │   │   └── node-sqlite-dialect.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── memory-adapter
│   │   │   │   │   ├── adapter.memory.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── memory-adapter.ts
│   │   │   │   ├── mongodb-adapter
│   │   │   │   │   ├── adapter.mongo-db.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mongodb-adapter.ts
│   │   │   │   ├── prisma-adapter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── prisma-adapter.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── base.prisma
│   │   │   │   │       ├── generate-auth-config.ts
│   │   │   │   │       ├── generate-prisma-schema.ts
│   │   │   │   │       ├── get-prisma-client.ts
│   │   │   │   │       ├── prisma.mysql.test.ts
│   │   │   │   │       ├── prisma.pg.test.ts
│   │   │   │   │       ├── prisma.sqlite.test.ts
│   │   │   │   │       └── push-prisma-schema.ts
│   │   │   │   ├── test-adapter.ts
│   │   │   │   ├── test.ts
│   │   │   │   ├── tests
│   │   │   │   │   ├── auth-flow.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── normal.ts
│   │   │   │   │   ├── number-id.ts
│   │   │   │   │   ├── performance.ts
│   │   │   │   │   └── transactions.ts
│   │   │   │   └── utils.ts
│   │   │   ├── api
│   │   │   │   ├── check-endpoint-conflicts.test.ts
│   │   │   │   ├── index.test.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── middlewares
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── origin-check.test.ts
│   │   │   │   │   └── origin-check.ts
│   │   │   │   ├── rate-limiter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── rate-limiter.test.ts
│   │   │   │   ├── routes
│   │   │   │   │   ├── account.test.ts
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── callback.ts
│   │   │   │   │   ├── email-verification.test.ts
│   │   │   │   │   ├── email-verification.ts
│   │   │   │   │   ├── error.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── ok.ts
│   │   │   │   │   ├── reset-password.test.ts
│   │   │   │   │   ├── reset-password.ts
│   │   │   │   │   ├── session-api.test.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── sign-in.test.ts
│   │   │   │   │   ├── sign-in.ts
│   │   │   │   │   ├── sign-out.test.ts
│   │   │   │   │   ├── sign-out.ts
│   │   │   │   │   ├── sign-up.test.ts
│   │   │   │   │   ├── sign-up.ts
│   │   │   │   │   ├── update-user.test.ts
│   │   │   │   │   └── update-user.ts
│   │   │   │   ├── to-auth-endpoints.test.ts
│   │   │   │   └── to-auth-endpoints.ts
│   │   │   ├── auth.test.ts
│   │   │   ├── auth.ts
│   │   │   ├── call.test.ts
│   │   │   ├── client
│   │   │   │   ├── client-ssr.test.ts
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── config.ts
│   │   │   │   ├── fetch-plugins.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── lynx
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── lynx-store.ts
│   │   │   │   ├── parser.ts
│   │   │   │   ├── path-to-object.ts
│   │   │   │   ├── plugins
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── infer-plugin.ts
│   │   │   │   ├── proxy.ts
│   │   │   │   ├── query.ts
│   │   │   │   ├── react
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── react-store.ts
│   │   │   │   ├── session-atom.ts
│   │   │   │   ├── solid
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── solid-store.ts
│   │   │   │   ├── svelte
│   │   │   │   │   └── index.ts
│   │   │   │   ├── test-plugin.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── url.test.ts
│   │   │   │   ├── vanilla.ts
│   │   │   │   └── vue
│   │   │   │       ├── index.ts
│   │   │   │       └── vue-store.ts
│   │   │   ├── cookies
│   │   │   │   ├── check-cookies.ts
│   │   │   │   ├── cookie-utils.ts
│   │   │   │   ├── cookies.test.ts
│   │   │   │   └── index.ts
│   │   │   ├── crypto
│   │   │   │   ├── buffer.ts
│   │   │   │   ├── hash.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt.ts
│   │   │   │   ├── password.test.ts
│   │   │   │   ├── password.ts
│   │   │   │   └── random.ts
│   │   │   ├── db
│   │   │   │   ├── db.test.ts
│   │   │   │   ├── field.ts
│   │   │   │   ├── get-migration.ts
│   │   │   │   ├── get-schema.ts
│   │   │   │   ├── get-tables.test.ts
│   │   │   │   ├── get-tables.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── internal-adapter.test.ts
│   │   │   │   ├── internal-adapter.ts
│   │   │   │   ├── schema.ts
│   │   │   │   ├── secondary-storage.test.ts
│   │   │   │   ├── to-zod.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── with-hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── init.test.ts
│   │   │   ├── init.ts
│   │   │   ├── integrations
│   │   │   │   ├── next-js.ts
│   │   │   │   ├── node.ts
│   │   │   │   ├── react-start.ts
│   │   │   │   ├── solid-start.ts
│   │   │   │   └── svelte-kit.ts
│   │   │   ├── oauth2
│   │   │   │   ├── index.ts
│   │   │   │   ├── link-account.test.ts
│   │   │   │   ├── link-account.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── utils.ts
│   │   │   ├── plugins
│   │   │   │   ├── access
│   │   │   │   │   ├── access.test.ts
│   │   │   │   │   ├── access.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── additional-fields
│   │   │   │   │   ├── additional-fields.test.ts
│   │   │   │   │   └── client.ts
│   │   │   │   ├── admin
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── admin.test.ts
│   │   │   │   │   ├── admin.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── anonymous
│   │   │   │   │   ├── anon.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── api-key
│   │   │   │   │   ├── api-key.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── create-api-key.ts
│   │   │   │   │   │   ├── delete-all-expired-api-keys.ts
│   │   │   │   │   │   ├── delete-api-key.ts
│   │   │   │   │   │   ├── get-api-key.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── list-api-keys.ts
│   │   │   │   │   │   ├── update-api-key.ts
│   │   │   │   │   │   └── verify-api-key.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── bearer
│   │   │   │   │   ├── bearer.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── captcha
│   │   │   │   │   ├── captcha.test.ts
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-handlers
│   │   │   │   │       ├── captchafox.ts
│   │   │   │   │       ├── cloudflare-turnstile.ts
│   │   │   │   │       ├── google-recaptcha.ts
│   │   │   │   │       ├── h-captcha.ts
│   │   │   │   │       └── index.ts
│   │   │   │   ├── custom-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-session.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── device-authorization
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── device-authorization.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── schema.ts
│   │   │   │   ├── email-otp
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── email-otp.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── generic-oauth
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── generic-oauth.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── haveibeenpwned
│   │   │   │   │   ├── haveibeenpwned.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── jwt.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── sign.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── last-login-method
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-prefix.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── last-login-method.test.ts
│   │   │   │   ├── magic-link
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── magic-link.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── mcp
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mcp.test.ts
│   │   │   │   ├── multi-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── multi-session.test.ts
│   │   │   │   ├── oauth-proxy
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── oauth-proxy.test.ts
│   │   │   │   ├── oidc-provider
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── oidc.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── ui.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── one-tap
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── one-time-token
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── one-time-token.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── open-api
│   │   │   │   │   ├── generator.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── logo.ts
│   │   │   │   │   └── open-api.test.ts
│   │   │   │   ├── organization
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── call.ts
│   │   │   │   │   ├── client.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── organization-hook.test.ts
│   │   │   │   │   ├── organization.test.ts
│   │   │   │   │   ├── organization.ts
│   │   │   │   │   ├── permission.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── crud-access-control.test.ts
│   │   │   │   │   │   ├── crud-access-control.ts
│   │   │   │   │   │   ├── crud-invites.ts
│   │   │   │   │   │   ├── crud-members.test.ts
│   │   │   │   │   │   ├── crud-members.ts
│   │   │   │   │   │   ├── crud-org.test.ts
│   │   │   │   │   │   ├── crud-org.ts
│   │   │   │   │   │   └── crud-team.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── team.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── passkey
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── passkey.test.ts
│   │   │   │   ├── phone-number
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── phone-number-error.ts
│   │   │   │   │   └── phone-number.test.ts
│   │   │   │   ├── siwe
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── siwe.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── two-factor
│   │   │   │   │   ├── backup-codes
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── constant.ts
│   │   │   │   │   ├── error-code.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── otp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── totp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── two-factor.test.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-two-factor.ts
│   │   │   │   └── username
│   │   │   │       ├── client.ts
│   │   │   │       ├── error-codes.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── schema.ts
│   │   │   │       └── username.test.ts
│   │   │   ├── social-providers
│   │   │   │   └── index.ts
│   │   │   ├── social.test.ts
│   │   │   ├── test-utils
│   │   │   │   ├── headers.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── test-instance.ts
│   │   │   ├── types
│   │   │   │   ├── adapter.ts
│   │   │   │   ├── api.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── plugins.ts
│   │   │   │   └── types.test.ts
│   │   │   └── utils
│   │   │       ├── await-object.ts
│   │   │       ├── boolean.ts
│   │   │       ├── clone.ts
│   │   │       ├── constants.ts
│   │   │       ├── date.ts
│   │   │       ├── ensure-utc.ts
│   │   │       ├── get-request-ip.ts
│   │   │       ├── hashing.ts
│   │   │       ├── hide-metadata.ts
│   │   │       ├── id.ts
│   │   │       ├── import-util.ts
│   │   │       ├── index.ts
│   │   │       ├── is-atom.ts
│   │   │       ├── is-promise.ts
│   │   │       ├── json.ts
│   │   │       ├── merger.ts
│   │   │       ├── middleware-response.ts
│   │   │       ├── misc.ts
│   │   │       ├── password.ts
│   │   │       ├── plugin-helper.ts
│   │   │       ├── shim.ts
│   │   │       ├── time.ts
│   │   │       ├── url.ts
│   │   │       └── wildcard.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── generate.ts
│   │   │   │   ├── info.ts
│   │   │   │   ├── init.ts
│   │   │   │   ├── login.ts
│   │   │   │   ├── mcp.ts
│   │   │   │   ├── migrate.ts
│   │   │   │   └── secret.ts
│   │   │   ├── generators
│   │   │   │   ├── auth-config.ts
│   │   │   │   ├── drizzle.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely.ts
│   │   │   │   ├── prisma.ts
│   │   │   │   └── types.ts
│   │   │   ├── index.ts
│   │   │   └── utils
│   │   │       ├── add-svelte-kit-env-modules.ts
│   │   │       ├── check-package-managers.ts
│   │   │       ├── format-ms.ts
│   │   │       ├── get-config.ts
│   │   │       ├── get-package-info.ts
│   │   │       ├── get-tsconfig-info.ts
│   │   │       └── install-dependencies.ts
│   │   ├── test
│   │   │   ├── __snapshots__
│   │   │   │   ├── auth-schema-mysql-enum.txt
│   │   │   │   ├── auth-schema-mysql-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey.txt
│   │   │   │   ├── auth-schema-mysql.txt
│   │   │   │   ├── auth-schema-number-id.txt
│   │   │   │   ├── auth-schema-pg-enum.txt
│   │   │   │   ├── auth-schema-pg-passkey.txt
│   │   │   │   ├── auth-schema-sqlite-enum.txt
│   │   │   │   ├── auth-schema-sqlite-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey.txt
│   │   │   │   ├── auth-schema-sqlite.txt
│   │   │   │   ├── auth-schema.txt
│   │   │   │   ├── migrations.sql
│   │   │   │   ├── schema-mongodb.prisma
│   │   │   │   ├── schema-mysql-custom.prisma
│   │   │   │   ├── schema-mysql.prisma
│   │   │   │   ├── schema-numberid.prisma
│   │   │   │   └── schema.prisma
│   │   │   ├── generate-all-db.test.ts
│   │   │   ├── generate.test.ts
│   │   │   ├── get-config.test.ts
│   │   │   ├── info.test.ts
│   │   │   └── migrate.test.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.json
│   │   └── tsdown.config.ts
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── api
│   │   │   │   └── index.ts
│   │   │   ├── async_hooks
│   │   │   │   └── index.ts
│   │   │   ├── context
│   │   │   │   ├── endpoint-context.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── transaction.ts
│   │   │   ├── db
│   │   │   │   ├── adapter
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── plugin.ts
│   │   │   │   ├── schema
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── shared.ts
│   │   │   │   │   ├── user.ts
│   │   │   │   │   └── verification.ts
│   │   │   │   └── type.ts
│   │   │   ├── env
│   │   │   │   ├── color-depth.ts
│   │   │   │   ├── env-impl.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── logger.test.ts
│   │   │   │   └── logger.ts
│   │   │   ├── error
│   │   │   │   ├── codes.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── oauth2
│   │   │   │   ├── client-credentials-token.ts
│   │   │   │   ├── create-authorization-url.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── oauth-provider.ts
│   │   │   │   ├── refresh-access-token.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── validate-authorization-code.ts
│   │   │   ├── social-providers
│   │   │   │   ├── apple.ts
│   │   │   │   ├── atlassian.ts
│   │   │   │   ├── cognito.ts
│   │   │   │   ├── discord.ts
│   │   │   │   ├── dropbox.ts
│   │   │   │   ├── facebook.ts
│   │   │   │   ├── figma.ts
│   │   │   │   ├── github.ts
│   │   │   │   ├── gitlab.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── huggingface.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kakao.ts
│   │   │   │   ├── kick.ts
│   │   │   │   ├── line.ts
│   │   │   │   ├── linear.ts
│   │   │   │   ├── linkedin.ts
│   │   │   │   ├── microsoft-entra-id.ts
│   │   │   │   ├── naver.ts
│   │   │   │   ├── notion.ts
│   │   │   │   ├── paypal.ts
│   │   │   │   ├── reddit.ts
│   │   │   │   ├── roblox.ts
│   │   │   │   ├── salesforce.ts
│   │   │   │   ├── slack.ts
│   │   │   │   ├── spotify.ts
│   │   │   │   ├── tiktok.ts
│   │   │   │   ├── twitch.ts
│   │   │   │   ├── twitter.ts
│   │   │   │   ├── vk.ts
│   │   │   │   └── zoom.ts
│   │   │   ├── types
│   │   │   │   ├── context.ts
│   │   │   │   ├── cookie.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── init-options.ts
│   │   │   │   ├── plugin-client.ts
│   │   │   │   └── plugin.ts
│   │   │   └── utils
│   │   │       ├── error-codes.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── expo
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── expo.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── sso
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── index.ts
│   │   │   ├── oidc.test.ts
│   │   │   └── saml.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── stripe
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── schema.ts
│   │   │   ├── stripe.test.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── telemetry
│       ├── package.json
│       ├── src
│       │   ├── detectors
│       │   │   ├── detect-auth-config.ts
│       │   │   ├── detect-database.ts
│       │   │   ├── detect-framework.ts
│       │   │   ├── detect-project-info.ts
│       │   │   ├── detect-runtime.ts
│       │   │   └── detect-system-info.ts
│       │   ├── index.ts
│       │   ├── project-id.ts
│       │   ├── telemetry.test.ts
│       │   ├── types.ts
│       │   └── utils
│       │       ├── hash.ts
│       │       ├── id.ts
│       │       ├── import-util.ts
│       │       └── package-json.ts
│       ├── tsconfig.json
│       └── tsdown.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── SECURITY.md
├── tsconfig.json
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/packages/better-auth/src/adapters/adapter-factory/index.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { safeJSONParse } from "../../utils/json";
   2 | import { withApplyDefault } from "../../adapters/utils";
   3 | import { getAuthTables } from "../../db/get-tables";
   4 | import type { BetterAuthOptions } from "@better-auth/core";
   5 | import { generateId as defaultGenerateId } from "../../utils";
   6 | import type {
   7 | 	AdapterFactoryConfig,
   8 | 	AdapterFactoryOptions,
   9 | 	AdapterTestDebugLogs,
  10 | } from "./types";
  11 | import type { DBFieldAttribute } from "@better-auth/core/db";
  12 | import { logger, TTY_COLORS, getColorDepth } from "@better-auth/core/env";
  13 | import type {
  14 | 	DBAdapter,
  15 | 	DBTransactionAdapter,
  16 | 	Where,
  17 | 	CleanedWhere,
  18 | } from "@better-auth/core/db/adapter";
  19 | import { BetterAuthError } from "@better-auth/core/error";
  20 | export * from "./types";
  21 | 
  22 | let debugLogs: { instance: string; args: any[] }[] = [];
  23 | let transactionId = -1;
  24 | 
  25 | const createAsIsTransaction =
  26 | 	(adapter: DBAdapter<BetterAuthOptions>) =>
  27 | 	<R>(fn: (trx: DBTransactionAdapter<BetterAuthOptions>) => Promise<R>) =>
  28 | 		fn(adapter);
  29 | 
  30 | export type AdapterFactory = (
  31 | 	options: BetterAuthOptions,
  32 | ) => DBAdapter<BetterAuthOptions>;
  33 | 
  34 | export const createAdapterFactory =
  35 | 	({
  36 | 		adapter: customAdapter,
  37 | 		config: cfg,
  38 | 	}: AdapterFactoryOptions): AdapterFactory =>
  39 | 	(options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => {
  40 | 		const uniqueAdapterFactoryInstanceId = Math.random()
  41 | 			.toString(36)
  42 | 			.substring(2, 15);
  43 | 
  44 | 		const config = {
  45 | 			...cfg,
  46 | 			supportsBooleans: cfg.supportsBooleans ?? true,
  47 | 			supportsDates: cfg.supportsDates ?? true,
  48 | 			supportsJSON: cfg.supportsJSON ?? false,
  49 | 			adapterName: cfg.adapterName ?? cfg.adapterId,
  50 | 			supportsNumericIds: cfg.supportsNumericIds ?? true,
  51 | 			transaction: cfg.transaction ?? false,
  52 | 			disableTransformInput: cfg.disableTransformInput ?? false,
  53 | 			disableTransformOutput: cfg.disableTransformOutput ?? false,
  54 | 		} satisfies AdapterFactoryConfig;
  55 | 
  56 | 		if (
  57 | 			options.advanced?.database?.useNumberId === true &&
  58 | 			config.supportsNumericIds === false
  59 | 		) {
  60 | 			throw new BetterAuthError(
  61 | 				`[${config.adapterName}] Your database or database adapter does not support numeric ids. Please disable "useNumberId" in your config.`,
  62 | 			);
  63 | 		}
  64 | 
  65 | 		// End-user's Better-Auth instance's schema
  66 | 		const schema = getAuthTables(options);
  67 | 
  68 | 		/**
  69 | 		 * This function helps us get the default field name from the schema defined by devs.
  70 | 		 * Often times, the user will be using the `fieldName` which could had been customized by the users.
  71 | 		 * This function helps us get the actual field name useful to match against the schema. (eg: schema[model].fields[field])
  72 | 		 *
  73 | 		 * If it's still unclear what this does:
  74 | 		 *
  75 | 		 * 1. User can define a custom fieldName.
  76 | 		 * 2. When using a custom fieldName, doing something like `schema[model].fields[field]` will not work.
  77 | 		 */
  78 | 		const getDefaultFieldName = ({
  79 | 			field,
  80 | 			model: unsafe_model,
  81 | 		}: {
  82 | 			model: string;
  83 | 			field: string;
  84 | 		}) => {
  85 | 			// Plugin `schema`s can't define their own `id`. Better-auth auto provides `id` to every schema model.
  86 | 			// Given this, we can't just check if the `field` (that being `id`) is within the schema's fields, since it is never defined.
  87 | 			// So we check if the `field` is `id` and if so, we return `id` itself. Otherwise, we return the `field` from the schema.
  88 | 			if (field === "id" || field === "_id") {
  89 | 				return "id";
  90 | 			}
  91 | 			const model = getDefaultModelName(unsafe_model); // Just to make sure the model name is correct.
  92 | 
  93 | 			let f = schema[model]?.fields[field];
  94 | 			if (!f) {
  95 | 				const result = Object.entries(schema[model]!.fields!).find(
  96 | 					([_, f]) => f.fieldName === field,
  97 | 				);
  98 | 				if (result) {
  99 | 					f = result[1];
 100 | 					field = result[0];
 101 | 				}
 102 | 			}
 103 | 			if (!f) {
 104 | 				debugLog(`Field ${field} not found in model ${model}`);
 105 | 				debugLog(`Schema:`, schema);
 106 | 				throw new BetterAuthError(`Field ${field} not found in model ${model}`);
 107 | 			}
 108 | 			return field;
 109 | 		};
 110 | 
 111 | 		/**
 112 | 		 * This function helps us get the default model name from the schema defined by devs.
 113 | 		 * Often times, the user will be using the `modelName` which could had been customized by the users.
 114 | 		 * This function helps us get the actual model name useful to match against the schema. (eg: schema[model])
 115 | 		 *
 116 | 		 * If it's still unclear what this does:
 117 | 		 *
 118 | 		 * 1. User can define a custom modelName.
 119 | 		 * 2. When using a custom modelName, doing something like `schema[model]` will not work.
 120 | 		 * 3. Using this function helps us get the actual model name based on the user's defined custom modelName.
 121 | 		 */
 122 | 		const getDefaultModelName = (model: string) => {
 123 | 			// It's possible this `model` could had applied `usePlural`.
 124 | 			// Thus we'll try the search but without the trailing `s`.
 125 | 			if (config.usePlural && model.charAt(model.length - 1) === "s") {
 126 | 				let pluralessModel = model.slice(0, -1);
 127 | 				let m = schema[pluralessModel] ? pluralessModel : undefined;
 128 | 				if (!m) {
 129 | 					m = Object.entries(schema).find(
 130 | 						([_, f]) => f.modelName === pluralessModel,
 131 | 					)?.[0];
 132 | 				}
 133 | 
 134 | 				if (m) {
 135 | 					return m;
 136 | 				}
 137 | 			}
 138 | 
 139 | 			let m = schema[model] ? model : undefined;
 140 | 			if (!m) {
 141 | 				m = Object.entries(schema).find(([_, f]) => f.modelName === model)?.[0];
 142 | 			}
 143 | 
 144 | 			if (!m) {
 145 | 				debugLog(`Model "${model}" not found in schema`);
 146 | 				debugLog(`Schema:`, schema);
 147 | 				throw new BetterAuthError(`Model "${model}" not found in schema`);
 148 | 			}
 149 | 			return m;
 150 | 		};
 151 | 
 152 | 		/**
 153 | 		 * Users can overwrite the default model of some tables. This function helps find the correct model name.
 154 | 		 * Furthermore, if the user passes `usePlural` as true in their adapter config,
 155 | 		 * then we should return the model name ending with an `s`.
 156 | 		 */
 157 | 		const getModelName = (model: string) => {
 158 | 			const defaultModelKey = getDefaultModelName(model);
 159 | 			const usePlural = config && config.usePlural;
 160 | 			const useCustomModelName =
 161 | 				schema &&
 162 | 				schema[defaultModelKey] &&
 163 | 				schema[defaultModelKey].modelName !== model;
 164 | 
 165 | 			if (useCustomModelName) {
 166 | 				return usePlural
 167 | 					? `${schema[defaultModelKey]!.modelName}s`
 168 | 					: schema[defaultModelKey]!.modelName;
 169 | 			}
 170 | 
 171 | 			return usePlural ? `${model}s` : model;
 172 | 		};
 173 | 		/**
 174 | 		 * Get the field name which is expected to be saved in the database based on the user's schema.
 175 | 		 *
 176 | 		 * This function is useful if you need to save the field name to the database.
 177 | 		 *
 178 | 		 * For example, if the user has defined a custom field name for the `user` model, then you can use this function to get the actual field name from the schema.
 179 | 		 */
 180 | 		function getFieldName({
 181 | 			model: model_name,
 182 | 			field: field_name,
 183 | 		}: {
 184 | 			model: string;
 185 | 			field: string;
 186 | 		}) {
 187 | 			const model = getDefaultModelName(model_name);
 188 | 			const field = getDefaultFieldName({ model, field: field_name });
 189 | 
 190 | 			return schema[model]?.fields[field]?.fieldName || field;
 191 | 		}
 192 | 
 193 | 		const debugLog = (...args: any[]) => {
 194 | 			if (config.debugLogs === true || typeof config.debugLogs === "object") {
 195 | 				// If we're running adapter tests, we'll keep debug logs in memory, then print them out if a test fails.
 196 | 				if (
 197 | 					typeof config.debugLogs === "object" &&
 198 | 					"isRunningAdapterTests" in config.debugLogs
 199 | 				) {
 200 | 					if (config.debugLogs.isRunningAdapterTests) {
 201 | 						args.shift(); // Removes the {method: "..."} object from the args array.
 202 | 						debugLogs.push({ instance: uniqueAdapterFactoryInstanceId, args });
 203 | 					}
 204 | 					return;
 205 | 				}
 206 | 
 207 | 				if (
 208 | 					typeof config.debugLogs === "object" &&
 209 | 					config.debugLogs.logCondition &&
 210 | 					!config.debugLogs.logCondition?.()
 211 | 				) {
 212 | 					return;
 213 | 				}
 214 | 
 215 | 				if (typeof args[0] === "object" && "method" in args[0]) {
 216 | 					const method = args.shift().method;
 217 | 					// Make sure the method is enabled in the config.
 218 | 					if (typeof config.debugLogs === "object") {
 219 | 						if (method === "create" && !config.debugLogs.create) {
 220 | 							return;
 221 | 						} else if (method === "update" && !config.debugLogs.update) {
 222 | 							return;
 223 | 						} else if (
 224 | 							method === "updateMany" &&
 225 | 							!config.debugLogs.updateMany
 226 | 						) {
 227 | 							return;
 228 | 						} else if (method === "findOne" && !config.debugLogs.findOne) {
 229 | 							return;
 230 | 						} else if (method === "findMany" && !config.debugLogs.findMany) {
 231 | 							return;
 232 | 						} else if (method === "delete" && !config.debugLogs.delete) {
 233 | 							return;
 234 | 						} else if (
 235 | 							method === "deleteMany" &&
 236 | 							!config.debugLogs.deleteMany
 237 | 						) {
 238 | 							return;
 239 | 						} else if (method === "count" && !config.debugLogs.count) {
 240 | 							return;
 241 | 						}
 242 | 					}
 243 | 					logger.info(`[${config.adapterName}]`, ...args);
 244 | 				} else {
 245 | 					logger.info(`[${config.adapterName}]`, ...args);
 246 | 				}
 247 | 			}
 248 | 		};
 249 | 
 250 | 		const idField = ({
 251 | 			customModelName,
 252 | 			forceAllowId,
 253 | 		}: {
 254 | 			customModelName?: string;
 255 | 			forceAllowId?: boolean;
 256 | 		}) => {
 257 | 			const shouldGenerateId =
 258 | 				!config.disableIdGeneration &&
 259 | 				!options.advanced?.database?.useNumberId &&
 260 | 				!forceAllowId;
 261 | 			const model = getDefaultModelName(customModelName ?? "id");
 262 | 			return {
 263 | 				type: options.advanced?.database?.useNumberId ? "number" : "string",
 264 | 				required: shouldGenerateId ? true : false,
 265 | 				...(shouldGenerateId
 266 | 					? {
 267 | 							defaultValue() {
 268 | 								if (config.disableIdGeneration) return undefined;
 269 | 								const useNumberId = options.advanced?.database?.useNumberId;
 270 | 								let generateId = options.advanced?.database?.generateId;
 271 | 								if (options.advanced?.generateId !== undefined) {
 272 | 									logger.warn(
 273 | 										"Your Better Auth config includes advanced.generateId which is deprecated. Please use advanced.database.generateId instead. This will be removed in future releases.",
 274 | 									);
 275 | 									generateId = options.advanced?.generateId;
 276 | 								}
 277 | 								if (generateId === false || useNumberId) return undefined;
 278 | 								if (generateId) {
 279 | 									return generateId({
 280 | 										model,
 281 | 									});
 282 | 								}
 283 | 								if (config.customIdGenerator) {
 284 | 									return config.customIdGenerator({ model });
 285 | 								}
 286 | 								return defaultGenerateId();
 287 | 							},
 288 | 						}
 289 | 					: {}),
 290 | 			} satisfies DBFieldAttribute;
 291 | 		};
 292 | 
 293 | 		const getFieldAttributes = ({
 294 | 			model,
 295 | 			field,
 296 | 		}: {
 297 | 			model: string;
 298 | 			field: string;
 299 | 		}) => {
 300 | 			const defaultModelName = getDefaultModelName(model);
 301 | 			const defaultFieldName = getDefaultFieldName({
 302 | 				field: field,
 303 | 				model: defaultModelName,
 304 | 			});
 305 | 
 306 | 			const fields = schema[defaultModelName]!.fields;
 307 | 			fields.id = idField({ customModelName: defaultModelName });
 308 | 			const fieldAttributes = fields[defaultFieldName];
 309 | 			if (!fieldAttributes) {
 310 | 				throw new BetterAuthError(`Field ${field} not found in model ${model}`);
 311 | 			}
 312 | 			return fieldAttributes;
 313 | 		};
 314 | 
 315 | 		const transformInput = async (
 316 | 			data: Record<string, any>,
 317 | 			defaultModelName: string,
 318 | 			action: "create" | "update",
 319 | 			forceAllowId?: boolean,
 320 | 		) => {
 321 | 			const transformedData: Record<string, any> = {};
 322 | 			const fields = schema[defaultModelName]!.fields;
 323 | 
 324 | 			const newMappedKeys = config.mapKeysTransformInput ?? {};
 325 | 			if (
 326 | 				!config.disableIdGeneration &&
 327 | 				!options.advanced?.database?.useNumberId
 328 | 			) {
 329 | 				fields.id = idField({
 330 | 					customModelName: defaultModelName,
 331 | 					forceAllowId: forceAllowId && "id" in data,
 332 | 				});
 333 | 			}
 334 | 			for (const field in fields) {
 335 | 				const value = data[field];
 336 | 				const fieldAttributes = fields[field];
 337 | 
 338 | 				let newFieldName: string =
 339 | 					newMappedKeys[field] || fields[field]!.fieldName || field;
 340 | 				if (
 341 | 					value === undefined &&
 342 | 					((fieldAttributes!.defaultValue === undefined &&
 343 | 						!fieldAttributes!.transform?.input &&
 344 | 						!(action === "update" && fieldAttributes!.onUpdate)) ||
 345 | 						(action === "update" && !fieldAttributes!.onUpdate))
 346 | 				) {
 347 | 					continue;
 348 | 				}
 349 | 				// If the value is undefined, but the fieldAttr provides a `defaultValue`, then we'll use that.
 350 | 				let newValue = withApplyDefault(value, fieldAttributes!, action);
 351 | 
 352 | 				// If the field attr provides a custom transform input, then we'll let it handle the value transformation.
 353 | 				// Afterwards, we'll continue to apply the default transformations just to make sure it saves in the correct format.
 354 | 				if (fieldAttributes!.transform?.input) {
 355 | 					newValue = await fieldAttributes!.transform.input(newValue);
 356 | 				}
 357 | 
 358 | 				if (
 359 | 					fieldAttributes!.references?.field === "id" &&
 360 | 					options.advanced?.database?.useNumberId
 361 | 				) {
 362 | 					if (Array.isArray(newValue)) {
 363 | 						newValue = newValue.map((x) => (x !== null ? Number(x) : null));
 364 | 					} else {
 365 | 						newValue = newValue !== null ? Number(newValue) : null;
 366 | 					}
 367 | 				} else if (
 368 | 					config.supportsJSON === false &&
 369 | 					typeof newValue === "object" &&
 370 | 					fieldAttributes!.type === "json"
 371 | 				) {
 372 | 					newValue = JSON.stringify(newValue);
 373 | 				} else if (
 374 | 					config.supportsDates === false &&
 375 | 					newValue instanceof Date &&
 376 | 					fieldAttributes!.type === "date"
 377 | 				) {
 378 | 					newValue = newValue.toISOString();
 379 | 				} else if (
 380 | 					config.supportsBooleans === false &&
 381 | 					typeof newValue === "boolean"
 382 | 				) {
 383 | 					newValue = newValue ? 1 : 0;
 384 | 				}
 385 | 
 386 | 				if (config.customTransformInput) {
 387 | 					newValue = config.customTransformInput({
 388 | 						data: newValue,
 389 | 						action,
 390 | 						field: newFieldName,
 391 | 						fieldAttributes: fieldAttributes!,
 392 | 						model: defaultModelName,
 393 | 						schema,
 394 | 						options,
 395 | 					});
 396 | 				}
 397 | 
 398 | 				if (newValue !== undefined) {
 399 | 					transformedData[newFieldName] = newValue;
 400 | 				}
 401 | 			}
 402 | 			return transformedData;
 403 | 		};
 404 | 
 405 | 		const transformOutput = async (
 406 | 			data: Record<string, any> | null,
 407 | 			unsafe_model: string,
 408 | 			select: string[] = [],
 409 | 		) => {
 410 | 			if (!data) return null;
 411 | 			const newMappedKeys = config.mapKeysTransformOutput ?? {};
 412 | 			const transformedData: Record<string, any> = {};
 413 | 			const tableSchema = schema[unsafe_model]!.fields;
 414 | 			const idKey = Object.entries(newMappedKeys).find(
 415 | 				([_, v]) => v === "id",
 416 | 			)?.[0];
 417 | 			tableSchema[idKey ?? "id"] = {
 418 | 				type: options.advanced?.database?.useNumberId ? "number" : "string",
 419 | 			};
 420 | 			for (const key in tableSchema) {
 421 | 				if (select.length && !select.includes(key)) {
 422 | 					continue;
 423 | 				}
 424 | 				const field = tableSchema[key];
 425 | 				if (field) {
 426 | 					const originalKey = field.fieldName || key;
 427 | 
 428 | 					// If the field is mapped, we'll use the mapped key. Otherwise, we'll use the original key.
 429 | 					let newValue =
 430 | 						data[
 431 | 							Object.entries(newMappedKeys).find(
 432 | 								([_, v]) => v === originalKey,
 433 | 							)?.[0] || originalKey
 434 | 						];
 435 | 
 436 | 					if (field.transform?.output) {
 437 | 						newValue = await field.transform.output(newValue);
 438 | 					}
 439 | 
 440 | 					let newFieldName: string = newMappedKeys[key] || key;
 441 | 
 442 | 					if (originalKey === "id" || field.references?.field === "id") {
 443 | 						// Even if `useNumberId` is true, we must always return a string `id` output.
 444 | 						if (typeof newValue !== "undefined" && newValue !== null)
 445 | 							newValue = String(newValue);
 446 | 					} else if (
 447 | 						config.supportsJSON === false &&
 448 | 						typeof newValue === "string" &&
 449 | 						field.type === "json"
 450 | 					) {
 451 | 						newValue = safeJSONParse(newValue);
 452 | 					} else if (
 453 | 						config.supportsDates === false &&
 454 | 						typeof newValue === "string" &&
 455 | 						field.type === "date"
 456 | 					) {
 457 | 						newValue = new Date(newValue);
 458 | 					} else if (
 459 | 						config.supportsBooleans === false &&
 460 | 						typeof newValue === "number" &&
 461 | 						field.type === "boolean"
 462 | 					) {
 463 | 						newValue = newValue === 1;
 464 | 					}
 465 | 
 466 | 					if (config.customTransformOutput) {
 467 | 						newValue = config.customTransformOutput({
 468 | 							data: newValue,
 469 | 							field: newFieldName,
 470 | 							fieldAttributes: field,
 471 | 							select,
 472 | 							model: unsafe_model,
 473 | 							schema,
 474 | 							options,
 475 | 						});
 476 | 					}
 477 | 
 478 | 					transformedData[newFieldName] = newValue;
 479 | 				}
 480 | 			}
 481 | 			return transformedData as any;
 482 | 		};
 483 | 
 484 | 		const transformWhereClause = <W extends Where[] | undefined>({
 485 | 			model,
 486 | 			where,
 487 | 		}: {
 488 | 			where: W;
 489 | 			model: string;
 490 | 		}): W extends undefined ? undefined : CleanedWhere[] => {
 491 | 			if (!where) return undefined as any;
 492 | 			const newMappedKeys = config.mapKeysTransformInput ?? {};
 493 | 
 494 | 			return where.map((w) => {
 495 | 				const {
 496 | 					field: unsafe_field,
 497 | 					value,
 498 | 					operator = "eq",
 499 | 					connector = "AND",
 500 | 				} = w;
 501 | 				if (operator === "in") {
 502 | 					if (!Array.isArray(value)) {
 503 | 						throw new BetterAuthError("Value must be an array");
 504 | 					}
 505 | 				}
 506 | 
 507 | 				let newValue = value;
 508 | 
 509 | 				const defaultModelName = getDefaultModelName(model);
 510 | 				const defaultFieldName = getDefaultFieldName({
 511 | 					field: unsafe_field,
 512 | 					model,
 513 | 				});
 514 | 				const fieldName: string =
 515 | 					newMappedKeys[defaultFieldName] ||
 516 | 					getFieldName({
 517 | 						field: defaultFieldName,
 518 | 						model: defaultModelName,
 519 | 					});
 520 | 
 521 | 				const fieldAttr = getFieldAttributes({
 522 | 					field: defaultFieldName,
 523 | 					model: defaultModelName,
 524 | 				});
 525 | 
 526 | 				if (
 527 | 					defaultFieldName === "id" ||
 528 | 					fieldAttr!.references?.field === "id"
 529 | 				) {
 530 | 					if (options.advanced?.database?.useNumberId) {
 531 | 						if (Array.isArray(value)) {
 532 | 							newValue = value.map(Number);
 533 | 						} else {
 534 | 							newValue = Number(value);
 535 | 						}
 536 | 					}
 537 | 				}
 538 | 
 539 | 				if (
 540 | 					fieldAttr.type === "date" &&
 541 | 					value instanceof Date &&
 542 | 					!config.supportsDates
 543 | 				) {
 544 | 					newValue = value.toISOString();
 545 | 				}
 546 | 
 547 | 				if (
 548 | 					fieldAttr.type === "boolean" &&
 549 | 					typeof value === "boolean" &&
 550 | 					!config.supportsBooleans
 551 | 				) {
 552 | 					newValue = value ? 1 : 0;
 553 | 				}
 554 | 
 555 | 				if (
 556 | 					fieldAttr.type === "json" &&
 557 | 					typeof value === "object" &&
 558 | 					!config.supportsJSON
 559 | 				) {
 560 | 					try {
 561 | 						const stringifiedJSON = JSON.stringify(value);
 562 | 						newValue = stringifiedJSON;
 563 | 					} catch (error) {
 564 | 						throw new Error(
 565 | 							`Failed to stringify JSON value for field ${fieldName}`,
 566 | 							{ cause: error },
 567 | 						);
 568 | 					}
 569 | 				}
 570 | 
 571 | 				return {
 572 | 					operator,
 573 | 					connector,
 574 | 					field: fieldName,
 575 | 					value: newValue,
 576 | 				} satisfies CleanedWhere;
 577 | 			}) as any;
 578 | 		};
 579 | 
 580 | 		const adapterInstance = customAdapter({
 581 | 			options,
 582 | 			schema,
 583 | 			debugLog,
 584 | 			getFieldName,
 585 | 			getModelName,
 586 | 			getDefaultModelName,
 587 | 			getDefaultFieldName,
 588 | 			getFieldAttributes,
 589 | 			transformInput,
 590 | 			transformOutput,
 591 | 			transformWhereClause,
 592 | 		});
 593 | 
 594 | 		let lazyLoadTransaction:
 595 | 			| DBAdapter<BetterAuthOptions>["transaction"]
 596 | 			| null = null;
 597 | 		const adapter: DBAdapter<BetterAuthOptions> = {
 598 | 			transaction: async (cb) => {
 599 | 				if (!lazyLoadTransaction) {
 600 | 					if (!config.transaction) {
 601 | 						if (
 602 | 							typeof config.debugLogs === "object" &&
 603 | 							"isRunningAdapterTests" in config.debugLogs &&
 604 | 							config.debugLogs.isRunningAdapterTests
 605 | 						) {
 606 | 							// hide warning in adapter tests
 607 | 						} else {
 608 | 							logger.warn(
 609 | 								`[${config.adapterName}] - Transactions are not supported. Executing operations sequentially.`,
 610 | 							);
 611 | 						}
 612 | 						lazyLoadTransaction = createAsIsTransaction(adapter);
 613 | 					} else {
 614 | 						logger.debug(
 615 | 							`[${config.adapterName}] - Using provided transaction implementation.`,
 616 | 						);
 617 | 						lazyLoadTransaction = config.transaction;
 618 | 					}
 619 | 				}
 620 | 				return lazyLoadTransaction(cb);
 621 | 			},
 622 | 			create: async <T extends Record<string, any>, R = T>({
 623 | 				data: unsafeData,
 624 | 				model: unsafeModel,
 625 | 				select,
 626 | 				forceAllowId = false,
 627 | 			}: {
 628 | 				model: string;
 629 | 				data: T;
 630 | 				select?: string[];
 631 | 				forceAllowId?: boolean;
 632 | 			}): Promise<R> => {
 633 | 				transactionId++;
 634 | 				let thisTransactionId = transactionId;
 635 | 				const model = getModelName(unsafeModel);
 636 | 				unsafeModel = getDefaultModelName(unsafeModel);
 637 | 				if ("id" in unsafeData && !forceAllowId) {
 638 | 					logger.warn(
 639 | 						`[${config.adapterName}] - You are trying to create a record with an id. This is not allowed as we handle id generation for you, unless you pass in the \`forceAllowId\` parameter. The id will be ignored.`,
 640 | 					);
 641 | 					const err = new Error();
 642 | 					const stack = err.stack
 643 | 						?.split("\n")
 644 | 						.filter((_, i) => i !== 1)
 645 | 						.join("\n")
 646 | 						.replace("Error:", "Create method with `id` being called at:");
 647 | 					console.log(stack);
 648 | 					//@ts-expect-error
 649 | 					unsafeData.id = undefined;
 650 | 				}
 651 | 				debugLog(
 652 | 					{ method: "create" },
 653 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(1, 4)}`,
 654 | 					`${formatMethod("create")} ${formatAction("Unsafe Input")}:`,
 655 | 					{ model, data: unsafeData },
 656 | 				);
 657 | 				let data = unsafeData;
 658 | 				if (!config.disableTransformInput) {
 659 | 					data = (await transformInput(
 660 | 						unsafeData,
 661 | 						unsafeModel,
 662 | 						"create",
 663 | 						forceAllowId,
 664 | 					)) as T;
 665 | 				}
 666 | 				debugLog(
 667 | 					{ method: "create" },
 668 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(2, 4)}`,
 669 | 					`${formatMethod("create")} ${formatAction("Parsed Input")}:`,
 670 | 					{ model, data },
 671 | 				);
 672 | 				const res = await adapterInstance.create<T>({ data, model });
 673 | 				debugLog(
 674 | 					{ method: "create" },
 675 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(3, 4)}`,
 676 | 					`${formatMethod("create")} ${formatAction("DB Result")}:`,
 677 | 					{ model, res },
 678 | 				);
 679 | 				let transformed = res as any;
 680 | 				if (!config.disableTransformOutput) {
 681 | 					transformed = await transformOutput(res as any, unsafeModel, select);
 682 | 				}
 683 | 				debugLog(
 684 | 					{ method: "create" },
 685 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(4, 4)}`,
 686 | 					`${formatMethod("create")} ${formatAction("Parsed Result")}:`,
 687 | 					{ model, data: transformed },
 688 | 				);
 689 | 				return transformed;
 690 | 			},
 691 | 			update: async <T>({
 692 | 				model: unsafeModel,
 693 | 				where: unsafeWhere,
 694 | 				update: unsafeData,
 695 | 			}: {
 696 | 				model: string;
 697 | 				where: Where[];
 698 | 				update: Record<string, any>;
 699 | 			}): Promise<T | null> => {
 700 | 				transactionId++;
 701 | 				let thisTransactionId = transactionId;
 702 | 				unsafeModel = getDefaultModelName(unsafeModel);
 703 | 				const model = getModelName(unsafeModel);
 704 | 				const where = transformWhereClause({
 705 | 					model: unsafeModel,
 706 | 					where: unsafeWhere,
 707 | 				});
 708 | 				debugLog(
 709 | 					{ method: "update" },
 710 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(1, 4)}`,
 711 | 					`${formatMethod("update")} ${formatAction("Unsafe Input")}:`,
 712 | 					{ model, data: unsafeData },
 713 | 				);
 714 | 				let data = unsafeData as T;
 715 | 				if (!config.disableTransformInput) {
 716 | 					data = (await transformInput(unsafeData, unsafeModel, "update")) as T;
 717 | 				}
 718 | 				debugLog(
 719 | 					{ method: "update" },
 720 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(2, 4)}`,
 721 | 					`${formatMethod("update")} ${formatAction("Parsed Input")}:`,
 722 | 					{ model, data },
 723 | 				);
 724 | 				const res = await adapterInstance.update<T>({
 725 | 					model,
 726 | 					where,
 727 | 					update: data,
 728 | 				});
 729 | 				debugLog(
 730 | 					{ method: "update" },
 731 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(3, 4)}`,
 732 | 					`${formatMethod("update")} ${formatAction("DB Result")}:`,
 733 | 					{ model, data: res },
 734 | 				);
 735 | 				let transformed = res as any;
 736 | 				if (!config.disableTransformOutput) {
 737 | 					transformed = await transformOutput(res as any, unsafeModel);
 738 | 				}
 739 | 				debugLog(
 740 | 					{ method: "update" },
 741 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(4, 4)}`,
 742 | 					`${formatMethod("update")} ${formatAction("Parsed Result")}:`,
 743 | 					{ model, data: transformed },
 744 | 				);
 745 | 				return transformed;
 746 | 			},
 747 | 			updateMany: async ({
 748 | 				model: unsafeModel,
 749 | 				where: unsafeWhere,
 750 | 				update: unsafeData,
 751 | 			}: {
 752 | 				model: string;
 753 | 				where: Where[];
 754 | 				update: Record<string, any>;
 755 | 			}) => {
 756 | 				transactionId++;
 757 | 				let thisTransactionId = transactionId;
 758 | 				const model = getModelName(unsafeModel);
 759 | 				const where = transformWhereClause({
 760 | 					model: unsafeModel,
 761 | 					where: unsafeWhere,
 762 | 				});
 763 | 				unsafeModel = getDefaultModelName(unsafeModel);
 764 | 				debugLog(
 765 | 					{ method: "updateMany" },
 766 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(1, 4)}`,
 767 | 					`${formatMethod("updateMany")} ${formatAction("Unsafe Input")}:`,
 768 | 					{ model, data: unsafeData },
 769 | 				);
 770 | 				let data = unsafeData;
 771 | 				if (!config.disableTransformInput) {
 772 | 					data = await transformInput(unsafeData, unsafeModel, "update");
 773 | 				}
 774 | 				debugLog(
 775 | 					{ method: "updateMany" },
 776 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(2, 4)}`,
 777 | 					`${formatMethod("updateMany")} ${formatAction("Parsed Input")}:`,
 778 | 					{ model, data },
 779 | 				);
 780 | 
 781 | 				const updatedCount = await adapterInstance.updateMany({
 782 | 					model,
 783 | 					where,
 784 | 					update: data,
 785 | 				});
 786 | 				debugLog(
 787 | 					{ method: "updateMany" },
 788 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(3, 4)}`,
 789 | 					`${formatMethod("updateMany")} ${formatAction("DB Result")}:`,
 790 | 					{ model, data: updatedCount },
 791 | 				);
 792 | 				debugLog(
 793 | 					{ method: "updateMany" },
 794 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(4, 4)}`,
 795 | 					`${formatMethod("updateMany")} ${formatAction("Parsed Result")}:`,
 796 | 					{ model, data: updatedCount },
 797 | 				);
 798 | 				return updatedCount;
 799 | 			},
 800 | 			findOne: async <T extends Record<string, any>>({
 801 | 				model: unsafeModel,
 802 | 				where: unsafeWhere,
 803 | 				select,
 804 | 			}: {
 805 | 				model: string;
 806 | 				where: Where[];
 807 | 				select?: string[];
 808 | 			}) => {
 809 | 				transactionId++;
 810 | 				let thisTransactionId = transactionId;
 811 | 				const model = getModelName(unsafeModel);
 812 | 				const where = transformWhereClause({
 813 | 					model: unsafeModel,
 814 | 					where: unsafeWhere,
 815 | 				});
 816 | 				unsafeModel = getDefaultModelName(unsafeModel);
 817 | 				debugLog(
 818 | 					{ method: "findOne" },
 819 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(1, 3)}`,
 820 | 					`${formatMethod("findOne")}:`,
 821 | 					{ model, where, select },
 822 | 				);
 823 | 				const res = await adapterInstance.findOne<T>({
 824 | 					model,
 825 | 					where,
 826 | 					select,
 827 | 				});
 828 | 				debugLog(
 829 | 					{ method: "findOne" },
 830 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(2, 3)}`,
 831 | 					`${formatMethod("findOne")} ${formatAction("DB Result")}:`,
 832 | 					{ model, data: res },
 833 | 				);
 834 | 				let transformed = res as any;
 835 | 				if (!config.disableTransformOutput) {
 836 | 					transformed = await transformOutput(res as any, unsafeModel, select);
 837 | 				}
 838 | 				debugLog(
 839 | 					{ method: "findOne" },
 840 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(3, 3)}`,
 841 | 					`${formatMethod("findOne")} ${formatAction("Parsed Result")}:`,
 842 | 					{ model, data: transformed },
 843 | 				);
 844 | 				return transformed;
 845 | 			},
 846 | 			findMany: async <T extends Record<string, any>>({
 847 | 				model: unsafeModel,
 848 | 				where: unsafeWhere,
 849 | 				limit: unsafeLimit,
 850 | 				sortBy,
 851 | 				offset,
 852 | 			}: {
 853 | 				model: string;
 854 | 				where?: Where[];
 855 | 				limit?: number;
 856 | 				sortBy?: { field: string; direction: "asc" | "desc" };
 857 | 				offset?: number;
 858 | 			}) => {
 859 | 				transactionId++;
 860 | 				let thisTransactionId = transactionId;
 861 | 				const limit =
 862 | 					unsafeLimit ??
 863 | 					options.advanced?.database?.defaultFindManyLimit ??
 864 | 					100;
 865 | 				const model = getModelName(unsafeModel);
 866 | 				const where = transformWhereClause({
 867 | 					model: unsafeModel,
 868 | 					where: unsafeWhere,
 869 | 				});
 870 | 				unsafeModel = getDefaultModelName(unsafeModel);
 871 | 				debugLog(
 872 | 					{ method: "findMany" },
 873 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(1, 3)}`,
 874 | 					`${formatMethod("findMany")}:`,
 875 | 					{ model, where, limit, sortBy, offset },
 876 | 				);
 877 | 				const res = await adapterInstance.findMany<T>({
 878 | 					model,
 879 | 					where,
 880 | 					limit: limit,
 881 | 					sortBy,
 882 | 					offset,
 883 | 				});
 884 | 				debugLog(
 885 | 					{ method: "findMany" },
 886 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(2, 3)}`,
 887 | 					`${formatMethod("findMany")} ${formatAction("DB Result")}:`,
 888 | 					{ model, data: res },
 889 | 				);
 890 | 				let transformed = res as any;
 891 | 				if (!config.disableTransformOutput) {
 892 | 					transformed = await Promise.all(
 893 | 						res.map(async (r) => await transformOutput(r as any, unsafeModel)),
 894 | 					);
 895 | 				}
 896 | 				debugLog(
 897 | 					{ method: "findMany" },
 898 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(3, 3)}`,
 899 | 					`${formatMethod("findMany")} ${formatAction("Parsed Result")}:`,
 900 | 					{ model, data: transformed },
 901 | 				);
 902 | 				return transformed;
 903 | 			},
 904 | 			delete: async ({
 905 | 				model: unsafeModel,
 906 | 				where: unsafeWhere,
 907 | 			}: {
 908 | 				model: string;
 909 | 				where: Where[];
 910 | 			}) => {
 911 | 				transactionId++;
 912 | 				let thisTransactionId = transactionId;
 913 | 				const model = getModelName(unsafeModel);
 914 | 				const where = transformWhereClause({
 915 | 					model: unsafeModel,
 916 | 					where: unsafeWhere,
 917 | 				});
 918 | 				unsafeModel = getDefaultModelName(unsafeModel);
 919 | 				debugLog(
 920 | 					{ method: "delete" },
 921 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(1, 2)}`,
 922 | 					`${formatMethod("delete")}:`,
 923 | 					{ model, where },
 924 | 				);
 925 | 				await adapterInstance.delete({
 926 | 					model,
 927 | 					where,
 928 | 				});
 929 | 				debugLog(
 930 | 					{ method: "delete" },
 931 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(2, 2)}`,
 932 | 					`${formatMethod("delete")} ${formatAction("DB Result")}:`,
 933 | 					{ model },
 934 | 				);
 935 | 			},
 936 | 			deleteMany: async ({
 937 | 				model: unsafeModel,
 938 | 				where: unsafeWhere,
 939 | 			}: {
 940 | 				model: string;
 941 | 				where: Where[];
 942 | 			}) => {
 943 | 				transactionId++;
 944 | 				let thisTransactionId = transactionId;
 945 | 				const model = getModelName(unsafeModel);
 946 | 				const where = transformWhereClause({
 947 | 					model: unsafeModel,
 948 | 					where: unsafeWhere,
 949 | 				});
 950 | 				unsafeModel = getDefaultModelName(unsafeModel);
 951 | 				debugLog(
 952 | 					{ method: "deleteMany" },
 953 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(1, 2)}`,
 954 | 					`${formatMethod("deleteMany")} ${formatAction("DeleteMany")}:`,
 955 | 					{ model, where },
 956 | 				);
 957 | 				const res = await adapterInstance.deleteMany({
 958 | 					model,
 959 | 					where,
 960 | 				});
 961 | 				debugLog(
 962 | 					{ method: "deleteMany" },
 963 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(2, 2)}`,
 964 | 					`${formatMethod("deleteMany")} ${formatAction("DB Result")}:`,
 965 | 					{ model, data: res },
 966 | 				);
 967 | 				return res;
 968 | 			},
 969 | 			count: async ({
 970 | 				model: unsafeModel,
 971 | 				where: unsafeWhere,
 972 | 			}: {
 973 | 				model: string;
 974 | 				where?: Where[];
 975 | 			}) => {
 976 | 				transactionId++;
 977 | 				let thisTransactionId = transactionId;
 978 | 				const model = getModelName(unsafeModel);
 979 | 				const where = transformWhereClause({
 980 | 					model: unsafeModel,
 981 | 					where: unsafeWhere,
 982 | 				});
 983 | 				unsafeModel = getDefaultModelName(unsafeModel);
 984 | 				debugLog(
 985 | 					{ method: "count" },
 986 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(1, 2)}`,
 987 | 					`${formatMethod("count")}:`,
 988 | 					{
 989 | 						model,
 990 | 						where,
 991 | 					},
 992 | 				);
 993 | 				const res = await adapterInstance.count({
 994 | 					model,
 995 | 					where,
 996 | 				});
 997 | 				debugLog(
 998 | 					{ method: "count" },
 999 | 					`${formatTransactionId(thisTransactionId)} ${formatStep(2, 2)}`,
1000 | 					`${formatMethod("count")}:`,
1001 | 					{
1002 | 						model,
1003 | 						data: res,
1004 | 					},
1005 | 				);
1006 | 				return res;
1007 | 			},
1008 | 			createSchema: adapterInstance.createSchema
1009 | 				? async (_, file) => {
1010 | 						const tables = getAuthTables(options);
1011 | 
1012 | 						if (
1013 | 							options.secondaryStorage &&
1014 | 							!options.session?.storeSessionInDatabase
1015 | 						) {
1016 | 							// biome-ignore lint/performance/noDelete: If the user has enabled secondaryStorage, as well as not specifying to store session table in DB, then createSchema shouldn't generate schema table.
1017 | 							delete tables.session;
1018 | 						}
1019 | 
1020 | 						if (
1021 | 							options.rateLimit &&
1022 | 							options.rateLimit.storage === "database" &&
1023 | 							// rate-limit will default to enabled in production,
1024 | 							// and given storage is database, it will try to use the rate-limit table,
1025 | 							// so we should make sure to generate rate-limit table schema
1026 | 							(typeof options.rateLimit.enabled === "undefined" ||
1027 | 								// and of course if they forcefully set to true, then they want rate-limit,
1028 | 								// thus we should also generate rate-limit table schema
1029 | 								options.rateLimit.enabled === true)
1030 | 						) {
1031 | 							tables.ratelimit = {
1032 | 								modelName: options.rateLimit.modelName ?? "ratelimit",
1033 | 								fields: {
1034 | 									key: {
1035 | 										type: "string",
1036 | 										unique: true,
1037 | 										required: true,
1038 | 										fieldName: options.rateLimit.fields?.key ?? "key",
1039 | 									},
1040 | 									count: {
1041 | 										type: "number",
1042 | 										required: true,
1043 | 										fieldName: options.rateLimit.fields?.count ?? "count",
1044 | 									},
1045 | 									lastRequest: {
1046 | 										type: "number",
1047 | 										required: true,
1048 | 										bigint: true,
1049 | 										defaultValue: () => Date.now(),
1050 | 										fieldName:
1051 | 											options.rateLimit.fields?.lastRequest ?? "lastRequest",
1052 | 									},
1053 | 								},
1054 | 							};
1055 | 						}
1056 | 						return adapterInstance.createSchema!({ file, tables });
1057 | 					}
1058 | 				: undefined,
1059 | 			options: {
1060 | 				adapterConfig: config,
1061 | 				...(adapterInstance.options ?? {}),
1062 | 			},
1063 | 			id: config.adapterId,
1064 | 
1065 | 			// Secretly export values ONLY if this adapter has enabled adapter-test-debug-logs.
1066 | 			// This would then be used during our adapter-tests to help print debug logs if a test fails.
1067 | 			//@ts-expect-error - ^^
1068 | 			...(config.debugLogs?.isRunningAdapterTests
1069 | 				? {
1070 | 						adapterTestDebugLogs: {
1071 | 							resetDebugLogs() {
1072 | 								debugLogs = debugLogs.filter(
1073 | 									(log) => log.instance !== uniqueAdapterFactoryInstanceId,
1074 | 								);
1075 | 							},
1076 | 							printDebugLogs() {
1077 | 								const separator = `─`.repeat(80);
1078 | 								const logs = debugLogs.filter(
1079 | 									(log) => log.instance === uniqueAdapterFactoryInstanceId,
1080 | 								);
1081 | 								if (logs.length === 0) {
1082 | 									return;
1083 | 								}
1084 | 
1085 | 								//`${colors.fg.blue}|${colors.reset} `,
1086 | 								let log: any[] = logs
1087 | 									.reverse()
1088 | 									.map((log) => {
1089 | 										log.args[0] = `\n${log.args[0]}`;
1090 | 										return [...log.args, "\n"];
1091 | 									})
1092 | 									.reduce(
1093 | 										(prev, curr) => {
1094 | 											return [...curr, ...prev];
1095 | 										},
1096 | 										[`\n${separator}`],
1097 | 									);
1098 | 
1099 | 								console.log(...log);
1100 | 							},
1101 | 						} satisfies AdapterTestDebugLogs,
1102 | 					}
1103 | 				: {}),
1104 | 		};
1105 | 		return adapter;
1106 | 	};
1107 | 
1108 | function formatTransactionId(transactionId: number) {
1109 | 	if (getColorDepth() < 8) {
1110 | 		return `#${transactionId}`;
1111 | 	}
1112 | 	return `${TTY_COLORS.fg.magenta}#${transactionId}${TTY_COLORS.reset}`;
1113 | }
1114 | 
1115 | function formatStep(step: number, total: number) {
1116 | 	return `${TTY_COLORS.bg.black}${TTY_COLORS.fg.yellow}[${step}/${total}]${TTY_COLORS.reset}`;
1117 | }
1118 | 
1119 | function formatMethod(method: string) {
1120 | 	return `${TTY_COLORS.bright}${method}${TTY_COLORS.reset}`;
1121 | }
1122 | 
1123 | function formatAction(action: string) {
1124 | 	return `${TTY_COLORS.dim}(${action})${TTY_COLORS.reset}`;
1125 | }
1126 | 
1127 | /**
1128 |  * @deprecated Use `createAdapterFactory` instead. This export will be removed in a future version.
1129 |  */
1130 | export const createAdapter = createAdapterFactory;
1131 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/adapters/tests/normal.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { expect } from "vitest";
   2 | import { createTestSuite } from "../create-test-suite";
   3 | import type { User } from "../../types";
   4 | import type { BetterAuthPlugin } from "@better-auth/core";
   5 | 
   6 | /**
   7 |  * This test suite tests the basic CRUD operations of the adapter.
   8 |  */
   9 | export const normalTestSuite = createTestSuite("normal", {}, (helpers) => {
  10 | 	const tests = getNormalTestSuiteTests(helpers);
  11 | 	return {
  12 | 		"init - tests": async () => {
  13 | 			const opts = helpers.getBetterAuthOptions();
  14 | 			expect(opts.advanced?.database?.useNumberId).toBe(undefined);
  15 | 		},
  16 | 		...tests,
  17 | 	};
  18 | });
  19 | 
  20 | export const getNormalTestSuiteTests = ({
  21 | 	adapter,
  22 | 	generate,
  23 | 	insertRandom,
  24 | 	modifyBetterAuthOptions,
  25 | 	sortModels,
  26 | 	customIdGenerator,
  27 | 	getBetterAuthOptions,
  28 | }: Parameters<Parameters<typeof createTestSuite>[2]>[0]) => {
  29 | 	return {
  30 | 		"create - should create a model": async () => {
  31 | 			const user = await generate("user");
  32 | 			const result = await adapter.create<User>({
  33 | 				model: "user",
  34 | 				data: user,
  35 | 				forceAllowId: true,
  36 | 			});
  37 | 			const options = getBetterAuthOptions();
  38 | 			if (options.advanced?.database?.useNumberId) {
  39 | 				expect(typeof result.id).toEqual("string");
  40 | 				user.id = result.id;
  41 | 			} else {
  42 | 				expect(typeof result.id).toEqual("string");
  43 | 			}
  44 | 			expect(result).toEqual(user);
  45 | 		},
  46 | 		"create - should always return an id": async () => {
  47 | 			const { id: _, ...user } = await generate("user");
  48 | 			const res = await adapter.create<User>({
  49 | 				model: "user",
  50 | 				data: user,
  51 | 			});
  52 | 			expect(res).toHaveProperty("id");
  53 | 			expect(typeof res.id).toEqual("string");
  54 | 		},
  55 | 		"create - should use generateId if provided": async () => {
  56 | 			const ID = (await customIdGenerator?.()) || "MOCK-ID";
  57 | 			await modifyBetterAuthOptions(
  58 | 				{
  59 | 					advanced: {
  60 | 						database: {
  61 | 							generateId: () => ID,
  62 | 						},
  63 | 					},
  64 | 				},
  65 | 				false,
  66 | 			);
  67 | 			const { id: _, ...user } = await generate("user");
  68 | 			const res = await adapter.create<User>({
  69 | 				model: "user",
  70 | 				data: user,
  71 | 			});
  72 | 			expect(res.id).toEqual(ID);
  73 | 			const findResult = await adapter.findOne<User>({
  74 | 				model: "user",
  75 | 				where: [{ field: "id", value: res.id }],
  76 | 			});
  77 | 			expect(findResult).toEqual(res);
  78 | 		},
  79 | 		"create - should return null for nullable foreign keys": async () => {
  80 | 			await modifyBetterAuthOptions(
  81 | 				{
  82 | 					plugins: [
  83 | 						{
  84 | 							id: "nullable-test",
  85 | 							schema: {
  86 | 								testModel: {
  87 | 									fields: {
  88 | 										nullableReference: {
  89 | 											type: "string",
  90 | 											references: { field: "id", model: "user" },
  91 | 											required: false,
  92 | 										},
  93 | 									},
  94 | 								},
  95 | 							},
  96 | 						} satisfies BetterAuthPlugin,
  97 | 					],
  98 | 				},
  99 | 				true,
 100 | 			);
 101 | 			const { nullableReference } = await adapter.create<{
 102 | 				nullableReference: string | null;
 103 | 			}>({
 104 | 				model: "testModel",
 105 | 				data: { nullableReference: null },
 106 | 				forceAllowId: true,
 107 | 			});
 108 | 			expect(nullableReference).toBeNull();
 109 | 		},
 110 | 		"findOne - should find a model": async () => {
 111 | 			const [user] = await insertRandom("user");
 112 | 			const result = await adapter.findOne<User>({
 113 | 				model: "user",
 114 | 				where: [{ field: "id", value: user.id }],
 115 | 			});
 116 | 			expect(result).toEqual(user);
 117 | 		},
 118 | 		"findOne - should find a model using a reference field": async () => {
 119 | 			const [user, session] = await insertRandom("session");
 120 | 			const result = await adapter.findOne<User>({
 121 | 				model: "session",
 122 | 				where: [{ field: "userId", value: user.id }],
 123 | 			});
 124 | 			expect(result).toEqual(session);
 125 | 		},
 126 | 		"findOne - should not throw on record not found": async () => {
 127 | 			const result = await adapter.findOne<User>({
 128 | 				model: "user",
 129 | 				where: [{ field: "id", value: "100000" }],
 130 | 			});
 131 | 			expect(result).toBeNull();
 132 | 		},
 133 | 		"findOne - should find a model without id": async () => {
 134 | 			const [user] = await insertRandom("user");
 135 | 			const result = await adapter.findOne<User>({
 136 | 				model: "user",
 137 | 				where: [{ field: "email", value: user.email }],
 138 | 			});
 139 | 			expect(result).toEqual(user);
 140 | 		},
 141 | 		"findOne - should find a model with modified field name": async () => {
 142 | 			await modifyBetterAuthOptions(
 143 | 				{
 144 | 					user: {
 145 | 						fields: {
 146 | 							email: "email_address",
 147 | 						},
 148 | 					},
 149 | 				},
 150 | 				true,
 151 | 			);
 152 | 			const [user] = await insertRandom("user");
 153 | 			const result = await adapter.findOne<User>({
 154 | 				model: "user",
 155 | 				where: [{ field: "email", value: user.email }],
 156 | 			});
 157 | 			expect(result).toEqual(user);
 158 | 			expect(result?.email).toEqual(user.email);
 159 | 			expect(true).toEqual(true);
 160 | 		},
 161 | 		"findOne - should find a model with modified model name": async () => {
 162 | 			await modifyBetterAuthOptions(
 163 | 				{
 164 | 					user: {
 165 | 						modelName: "user_custom",
 166 | 					},
 167 | 				},
 168 | 				true,
 169 | 			);
 170 | 			const [user] = await insertRandom("user");
 171 | 			expect(user).toBeDefined();
 172 | 			expect(user).toHaveProperty("id");
 173 | 			expect(user).toHaveProperty("name");
 174 | 			const result = await adapter.findOne<User>({
 175 | 				model: "user",
 176 | 				where: [{ field: "email", value: user.email }],
 177 | 			});
 178 | 			expect(result).toEqual(user);
 179 | 			expect(result?.email).toEqual(user.email);
 180 | 			expect(true).toEqual(true);
 181 | 		},
 182 | 		"findOne - should find a model with additional fields": async () => {
 183 | 			await modifyBetterAuthOptions(
 184 | 				{
 185 | 					user: {
 186 | 						additionalFields: {
 187 | 							customField: {
 188 | 								type: "string",
 189 | 								input: false,
 190 | 								required: true,
 191 | 								defaultValue: "default-value",
 192 | 							},
 193 | 						},
 194 | 					},
 195 | 				},
 196 | 				true,
 197 | 			);
 198 | 			const [user_] = await insertRandom("user");
 199 | 			const user = user_ as User & { customField: string };
 200 | 			expect(user).toHaveProperty("customField");
 201 | 			expect(user.customField).toBe("default-value");
 202 | 			const result = await adapter.findOne<User & { customField: string }>({
 203 | 				model: "user",
 204 | 				where: [{ field: "customField", value: user.customField }],
 205 | 			});
 206 | 			expect(result).toEqual(user);
 207 | 			expect(result?.customField).toEqual("default-value");
 208 | 		},
 209 | 		"findOne - should select fields": async () => {
 210 | 			const [user] = await insertRandom("user");
 211 | 			const result = await adapter.findOne<Pick<User, "email" | "name">>({
 212 | 				model: "user",
 213 | 				where: [{ field: "id", value: user.id }],
 214 | 				select: ["email", "name"],
 215 | 			});
 216 | 			expect(result).toEqual({ email: user.email, name: user.name });
 217 | 		},
 218 | 		"findOne - should find model with date field": async () => {
 219 | 			const [user] = await insertRandom("user");
 220 | 			const result = await adapter.findOne<User>({
 221 | 				model: "user",
 222 | 				where: [{ field: "createdAt", value: user.createdAt, operator: "eq" }],
 223 | 			});
 224 | 			expect(result).toEqual(user);
 225 | 			expect(result?.createdAt).toBeInstanceOf(Date);
 226 | 			expect(result?.createdAt).toEqual(user.createdAt);
 227 | 		},
 228 | 		"findMany - should find many models with date fields": async () => {
 229 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 230 | 			const youngestUser = users.sort(
 231 | 				(a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
 232 | 			)[0]!;
 233 | 			const result = await adapter.findMany<User>({
 234 | 				model: "user",
 235 | 				where: [
 236 | 					{ field: "createdAt", value: youngestUser.createdAt, operator: "lt" },
 237 | 				],
 238 | 			});
 239 | 			expect(sortModels(result)).toEqual(
 240 | 				sortModels(
 241 | 					users.filter((user) => user.createdAt < youngestUser.createdAt),
 242 | 				),
 243 | 			);
 244 | 		},
 245 | 		"findMany - should find many models": async () => {
 246 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 247 | 			const result = await adapter.findMany<User>({
 248 | 				model: "user",
 249 | 			});
 250 | 			expect(sortModels(result)).toEqual(sortModels(users));
 251 | 		},
 252 | 		"findMany - should return an empty array when no models are found":
 253 | 			async () => {
 254 | 				const result = await adapter.findMany<User>({
 255 | 					model: "user",
 256 | 					where: [{ field: "id", value: "100000" }],
 257 | 				});
 258 | 				expect(result).toEqual([]);
 259 | 			},
 260 | 		"findMany - should find many models with starts_with operator":
 261 | 			async () => {
 262 | 				const users = (await insertRandom("user", 3)).map((x) => x[0]);
 263 | 				const result = await adapter.findMany<User>({
 264 | 					model: "user",
 265 | 					where: [{ field: "name", value: "user", operator: "starts_with" }],
 266 | 				});
 267 | 				expect(sortModels(result)).toEqual(sortModels(users));
 268 | 			},
 269 | 		"findMany - starts_with should not interpret regex patterns": async () => {
 270 | 			// Create a user whose name literally starts with the regex-like prefix
 271 | 			const userTemplate = await generate("user");
 272 | 			const literalRegexUser = await adapter.create<User>({
 273 | 				model: "user",
 274 | 				data: {
 275 | 					...userTemplate,
 276 | 					name: ".*danger",
 277 | 				},
 278 | 				forceAllowId: true,
 279 | 			});
 280 | 
 281 | 			// Also create some normal users that do NOT start with ".*"
 282 | 			await insertRandom("user", 3);
 283 | 
 284 | 			const result = await adapter.findMany<User>({
 285 | 				model: "user",
 286 | 				where: [{ field: "name", value: ".*", operator: "starts_with" }],
 287 | 			});
 288 | 
 289 | 			// Should only match the literal ".*" prefix, not treat it as a regex matching everything
 290 | 			expect(result.length).toBe(1);
 291 | 			expect(result[0]!.id).toBe(literalRegexUser.id);
 292 | 			expect(result[0]!.name.startsWith(".*")).toBe(true);
 293 | 		},
 294 | 		"findMany - ends_with should not interpret regex patterns": async () => {
 295 | 			// Create a user whose name literally ends with the regex-like suffix
 296 | 			const userTemplate = await generate("user");
 297 | 			const literalRegexUser = await adapter.create<User>({
 298 | 				model: "user",
 299 | 				data: {
 300 | 					...userTemplate,
 301 | 					name: "danger.*",
 302 | 				},
 303 | 				forceAllowId: true,
 304 | 			});
 305 | 
 306 | 			// Also create some normal users that do NOT end with ".*"
 307 | 			await insertRandom("user", 3);
 308 | 
 309 | 			const result = await adapter.findMany<User>({
 310 | 				model: "user",
 311 | 				where: [{ field: "name", value: ".*", operator: "ends_with" }],
 312 | 			});
 313 | 
 314 | 			// Should only match the literal ".*" suffix, not treat it as a regex matching everything
 315 | 			expect(result.length).toBe(1);
 316 | 			expect(result[0]!.id).toBe(literalRegexUser.id);
 317 | 			expect(result[0]!.name.endsWith(".*")).toBe(true);
 318 | 		},
 319 | 		"findMany - contains should not interpret regex patterns": async () => {
 320 | 			// Create a user whose name literally contains the regex-like pattern
 321 | 			const userTemplate = await generate("user");
 322 | 			const literalRegexUser = await adapter.create<User>({
 323 | 				model: "user",
 324 | 				data: {
 325 | 					...userTemplate,
 326 | 					name: "prefix-.*-suffix",
 327 | 				},
 328 | 				forceAllowId: true,
 329 | 			});
 330 | 
 331 | 			// Also create some normal users that do NOT contain ".*"
 332 | 			await insertRandom("user", 3);
 333 | 
 334 | 			const result = await adapter.findMany<User>({
 335 | 				model: "user",
 336 | 				where: [{ field: "name", value: ".*", operator: "contains" }],
 337 | 			});
 338 | 
 339 | 			// Should only match the literal substring ".*", not treat it as a regex matching everything
 340 | 			expect(result.length).toBe(1);
 341 | 			expect(result[0]!.id).toBe(literalRegexUser.id);
 342 | 			expect(result[0]!.name.includes(".*")).toBe(true);
 343 | 		},
 344 | 		"findMany - should find many models with ends_with operator": async () => {
 345 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 346 | 			for (const user of users) {
 347 | 				const res = await adapter.update<User>({
 348 | 					model: "user",
 349 | 					where: [{ field: "id", value: user.id }],
 350 | 					update: { name: user.name.toLowerCase() }, // make name lowercase
 351 | 				});
 352 | 				if (!res) throw new Error("No result");
 353 | 				let u = users.find((u) => u.id === user.id)!;
 354 | 				u.name = res.name;
 355 | 				u.updatedAt = res.updatedAt;
 356 | 			}
 357 | 			const ends_with = users[0]!.name.slice(-1);
 358 | 			const result = await adapter.findMany<User>({
 359 | 				model: "user",
 360 | 				where: [
 361 | 					{
 362 | 						field: "name",
 363 | 						value: ends_with,
 364 | 						operator: "ends_with",
 365 | 					},
 366 | 				],
 367 | 			});
 368 | 			const expectedResult = sortModels(
 369 | 				users.filter((user) => user.name.endsWith(ends_with)),
 370 | 			);
 371 | 			if (result.length !== expectedResult.length) {
 372 | 				console.log(`Result length: ${result.length}`);
 373 | 				console.log(sortModels(result));
 374 | 				console.log("--------------------------------");
 375 | 				console.log(
 376 | 					`Expected result length: ${expectedResult.length} - key: ${JSON.stringify(ends_with)}`,
 377 | 				);
 378 | 				console.log(expectedResult);
 379 | 			}
 380 | 			expect(sortModels(result)).toEqual(expectedResult);
 381 | 		},
 382 | 		"findMany - should find many models with contains operator": async () => {
 383 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 384 | 
 385 | 			// if this check fails, the test will fail.
 386 | 			// insertRandom needs to generate emails that contain `@email.com`
 387 | 			expect(users[0]!.email).toContain("@email.com");
 388 | 
 389 | 			const result = await adapter.findMany<User>({
 390 | 				model: "user",
 391 | 				where: [
 392 | 					{
 393 | 						field: "email",
 394 | 						value: "mail", // all emails contains `@email.com` from `insertRandom`
 395 | 						operator: "contains",
 396 | 					},
 397 | 				],
 398 | 			});
 399 | 			expect(sortModels(result)).toEqual(sortModels(users));
 400 | 		},
 401 | 		"findMany - should handle multiple where conditions with different operators":
 402 | 			async () => {
 403 | 				const testData = [
 404 | 					{ name: "john doe", email: "[email protected]" },
 405 | 					{ name: "jane smith", email: "[email protected]" },
 406 | 				];
 407 | 
 408 | 				const createdUsers: User[] = [];
 409 | 				for (const data of testData) {
 410 | 					const user = await adapter.create({
 411 | 						model: "user",
 412 | 						data: {
 413 | 							...generate("user"),
 414 | 							...data,
 415 | 						},
 416 | 						forceAllowId: true,
 417 | 					});
 418 | 					createdUsers.push(user as User);
 419 | 				}
 420 | 
 421 | 				const result = await adapter.findMany<User>({
 422 | 					model: "user",
 423 | 					where: [
 424 | 						{
 425 | 							field: "email",
 426 | 							value: "[email protected]",
 427 | 							operator: "eq",
 428 | 							connector: "AND",
 429 | 						},
 430 | 						{
 431 | 							field: "name",
 432 | 							value: "john",
 433 | 							operator: "contains",
 434 | 							connector: "AND",
 435 | 						},
 436 | 					],
 437 | 				});
 438 | 				expect(result.length).toBe(1);
 439 | 				expect(result[0]!.email).toBe("[email protected]");
 440 | 				expect(result[0]!.name).toBe("john doe");
 441 | 
 442 | 				const result2 = await adapter.findMany<User>({
 443 | 					model: "user",
 444 | 					where: [
 445 | 						{
 446 | 							field: "email",
 447 | 							value: "gmail",
 448 | 							operator: "contains",
 449 | 							connector: "AND",
 450 | 						},
 451 | 						{
 452 | 							field: "name",
 453 | 							value: "jane",
 454 | 							operator: "contains",
 455 | 							connector: "AND",
 456 | 						},
 457 | 					],
 458 | 				});
 459 | 
 460 | 				expect(result2.length).toBe(1);
 461 | 				expect(result2[0]!.email).toBe("[email protected]");
 462 | 				expect(result2[0]!.name).toBe("jane smith");
 463 | 
 464 | 				const result3 = await adapter.findMany<User>({
 465 | 					model: "user",
 466 | 					where: [
 467 | 						{
 468 | 							field: "email",
 469 | 							value: "john",
 470 | 							operator: "starts_with",
 471 | 							connector: "AND",
 472 | 						},
 473 | 						{
 474 | 							field: "name",
 475 | 							value: "john",
 476 | 							operator: "contains",
 477 | 							connector: "AND",
 478 | 						},
 479 | 					],
 480 | 				});
 481 | 
 482 | 				expect(result3.length).toBe(1);
 483 | 				expect(result3[0]!.email).toBe("[email protected]");
 484 | 				expect(result3[0]!.name).toBe("john doe");
 485 | 			},
 486 | 		"findMany - should find many models with contains operator (using symbol)":
 487 | 			async () => {
 488 | 				const users = (await insertRandom("user", 3)).map((x) => x[0]);
 489 | 				const result = await adapter.findMany<User>({
 490 | 					model: "user",
 491 | 					where: [{ field: "email", value: "@", operator: "contains" }],
 492 | 				});
 493 | 				expect(sortModels(result)).toEqual(sortModels(users));
 494 | 			},
 495 | 		"findMany - should find many models with eq operator": async () => {
 496 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 497 | 			const result = await adapter.findMany<User>({
 498 | 				model: "user",
 499 | 				where: [{ field: "email", value: users[0]!.email, operator: "eq" }],
 500 | 			});
 501 | 			expect(sortModels(result)).toEqual(sortModels([users[0]!]));
 502 | 		},
 503 | 		"findMany - should find many models with ne operator": async () => {
 504 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 505 | 			const result = await adapter.findMany<User>({
 506 | 				model: "user",
 507 | 				where: [{ field: "email", value: users[0]!.email, operator: "ne" }],
 508 | 			});
 509 | 			expect(sortModels(result)).toEqual(sortModels(users.slice(1)));
 510 | 		},
 511 | 		"findMany - should find many models with gt operator": async () => {
 512 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 513 | 			const oldestUser = users.sort(
 514 | 				(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
 515 | 			)[0]!;
 516 | 			const result = await adapter.findMany<User>({
 517 | 				model: "user",
 518 | 				where: [
 519 | 					{
 520 | 						field: "createdAt",
 521 | 						value: oldestUser.createdAt,
 522 | 						operator: "gt",
 523 | 					},
 524 | 				],
 525 | 			});
 526 | 			const expectedResult = sortModels(
 527 | 				users.filter((user) => user.createdAt > oldestUser.createdAt),
 528 | 			);
 529 | 			expect(result.length).not.toBe(0);
 530 | 			expect(sortModels(result)).toEqual(expectedResult);
 531 | 		},
 532 | 		"findMany - should find many models with gte operator": async () => {
 533 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 534 | 			const oldestUser = users.sort(
 535 | 				(a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
 536 | 			)[0]!;
 537 | 			const result = await adapter.findMany<User>({
 538 | 				model: "user",
 539 | 				where: [
 540 | 					{
 541 | 						field: "createdAt",
 542 | 						value: oldestUser.createdAt,
 543 | 						operator: "gte",
 544 | 					},
 545 | 				],
 546 | 			});
 547 | 			const expectedResult = users.filter(
 548 | 				(user) => user.createdAt >= oldestUser.createdAt,
 549 | 			);
 550 | 			expect(result.length).not.toBe(0);
 551 | 			expect(sortModels(result)).toEqual(sortModels(expectedResult));
 552 | 		},
 553 | 		"findMany - should find many models with lte operator": async () => {
 554 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 555 | 			const result = await adapter.findMany<User>({
 556 | 				model: "user",
 557 | 				where: [
 558 | 					{ field: "createdAt", value: users[0]!.createdAt, operator: "lte" },
 559 | 				],
 560 | 			});
 561 | 			const expectedResult = users.filter(
 562 | 				(user) => user.createdAt <= users[0]!.createdAt,
 563 | 			);
 564 | 			expect(sortModels(result)).toEqual(sortModels(expectedResult));
 565 | 		},
 566 | 		"findMany - should find many models with lt operator": async () => {
 567 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 568 | 			const result = await adapter.findMany<User>({
 569 | 				model: "user",
 570 | 				where: [
 571 | 					{ field: "createdAt", value: users[0]!.createdAt, operator: "lt" },
 572 | 				],
 573 | 			});
 574 | 			const expectedResult = users.filter(
 575 | 				(user) => user.createdAt < users[0]!.createdAt,
 576 | 			);
 577 | 			expect(sortModels(result)).toEqual(sortModels(expectedResult));
 578 | 		},
 579 | 		"findMany - should find many models with in operator": async () => {
 580 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 581 | 			const result = await adapter.findMany<User>({
 582 | 				model: "user",
 583 | 				where: [
 584 | 					{
 585 | 						field: "id",
 586 | 						value: [users[0]!.id, users[1]!.id],
 587 | 						operator: "in",
 588 | 					},
 589 | 				],
 590 | 			});
 591 | 			const expectedResult = users.filter(
 592 | 				(user) => user.id === users[0]!.id || user.id === users[1]!.id,
 593 | 			);
 594 | 			expect(sortModels(result)).toEqual(sortModels(expectedResult));
 595 | 		},
 596 | 		"findMany - should find many models with not_in operator": async () => {
 597 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 598 | 			const result = await adapter.findMany<User>({
 599 | 				model: "user",
 600 | 				where: [
 601 | 					{
 602 | 						field: "id",
 603 | 						value: [users[0]!.id, users[1]!.id],
 604 | 						operator: "not_in",
 605 | 					},
 606 | 				],
 607 | 			});
 608 | 			expect(sortModels(result)).toEqual([users[2]]);
 609 | 		},
 610 | 		"findMany - should find many models with sortBy": async () => {
 611 | 			let n = -1;
 612 | 			await modifyBetterAuthOptions(
 613 | 				{
 614 | 					user: {
 615 | 						additionalFields: {
 616 | 							numericField: {
 617 | 								type: "number",
 618 | 								defaultValue() {
 619 | 									return n++;
 620 | 								},
 621 | 							},
 622 | 						},
 623 | 					},
 624 | 				},
 625 | 				true,
 626 | 			);
 627 | 			const users = (await insertRandom("user", 5)).map(
 628 | 				(x) => x[0],
 629 | 			) as (User & { numericField: number })[];
 630 | 			const result = await adapter.findMany<User & { numericField: number }>({
 631 | 				model: "user",
 632 | 				sortBy: { field: "numericField", direction: "asc" },
 633 | 			});
 634 | 			const expectedResult = users
 635 | 				.map((x) => x.numericField)
 636 | 				.sort((a, b) => a - b);
 637 | 			try {
 638 | 				expect(result.map((x) => x.numericField)).toEqual(expectedResult);
 639 | 			} catch (error) {
 640 | 				console.log(`--------------------------------`);
 641 | 				console.log(`result:`);
 642 | 				console.log(result.map((x) => x.id));
 643 | 				console.log(`expected result:`);
 644 | 				console.log(expectedResult);
 645 | 				console.log(`--------------------------------`);
 646 | 				throw error;
 647 | 			}
 648 | 			const options = getBetterAuthOptions();
 649 | 			if (options.advanced?.database?.useNumberId) {
 650 | 				expect(Number(users[0]!.id)).not.toBeNaN();
 651 | 			}
 652 | 		},
 653 | 		"findMany - should find many models with limit": async () => {
 654 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 655 | 			const result = await adapter.findMany<User>({
 656 | 				model: "user",
 657 | 				limit: 1,
 658 | 			});
 659 | 			expect(result.length).toEqual(1);
 660 | 			expect(users.find((x) => x.id === result[0]!.id)).not.toBeNull();
 661 | 		},
 662 | 		"findMany - should find many models with offset": async () => {
 663 | 			// Note: The returned rows are ordered in no particular order
 664 | 			// This is because databases return rows in whatever order is fastest for the query.
 665 | 			const count = 10;
 666 | 			await insertRandom("user", count);
 667 | 			const result = await adapter.findMany<User>({
 668 | 				model: "user",
 669 | 				offset: 2,
 670 | 			});
 671 | 			expect(result.length).toEqual(count - 2);
 672 | 		},
 673 | 		"findMany - should find many models with limit and offset": async () => {
 674 | 			// Note: The returned rows are ordered in no particular order
 675 | 			// This is because databases return rows in whatever order is fastest for the query.
 676 | 			const count = 5;
 677 | 			await insertRandom("user", count);
 678 | 			const result = await adapter.findMany<User>({
 679 | 				model: "user",
 680 | 				limit: 2,
 681 | 				offset: 2,
 682 | 			});
 683 | 			expect(result.length).toEqual(2);
 684 | 			expect(result).toBeInstanceOf(Array);
 685 | 			result.forEach((user) => {
 686 | 				expect(user).toHaveProperty("id");
 687 | 				expect(user).toHaveProperty("name");
 688 | 				expect(user).toHaveProperty("email");
 689 | 			});
 690 | 		},
 691 | 		"findMany - should find many models with sortBy and offset": async () => {
 692 | 			let n = -1;
 693 | 			await modifyBetterAuthOptions(
 694 | 				{
 695 | 					user: {
 696 | 						additionalFields: {
 697 | 							numericField: {
 698 | 								type: "number",
 699 | 								defaultValue() {
 700 | 									return n++;
 701 | 								},
 702 | 							},
 703 | 						},
 704 | 					},
 705 | 				},
 706 | 				true,
 707 | 			);
 708 | 			const users = (await insertRandom("user", 5)).map(
 709 | 				(x) => x[0],
 710 | 			) as (User & { numericField: number })[];
 711 | 			const result = await adapter.findMany<User>({
 712 | 				model: "user",
 713 | 				sortBy: { field: "numericField", direction: "asc" },
 714 | 				offset: 2,
 715 | 			});
 716 | 			expect(result).toHaveLength(3);
 717 | 			expect(result).toEqual(
 718 | 				users.sort((a, b) => a.numericField - b.numericField).slice(2),
 719 | 			);
 720 | 		},
 721 | 		"findMany - should find many models with sortBy and limit": async () => {
 722 | 			let n = -1;
 723 | 			await modifyBetterAuthOptions(
 724 | 				{
 725 | 					user: {
 726 | 						additionalFields: {
 727 | 							numericField: {
 728 | 								type: "number",
 729 | 								defaultValue() {
 730 | 									return n++;
 731 | 								},
 732 | 							},
 733 | 						},
 734 | 					},
 735 | 				},
 736 | 				true,
 737 | 			);
 738 | 			const users = (await insertRandom("user", 5)).map(
 739 | 				(x) => x[0],
 740 | 			) as (User & { numericField: number })[];
 741 | 			const result = await adapter.findMany<User>({
 742 | 				model: "user",
 743 | 				sortBy: { field: "numericField", direction: "asc" },
 744 | 				limit: 2,
 745 | 			});
 746 | 			expect(result).toEqual(
 747 | 				users.sort((a, b) => a.numericField - b.numericField).slice(0, 2),
 748 | 			);
 749 | 		},
 750 | 		"findMany - should find many models with sortBy and limit and offset":
 751 | 			async () => {
 752 | 				let n = -1;
 753 | 				await modifyBetterAuthOptions(
 754 | 					{
 755 | 						user: {
 756 | 							additionalFields: {
 757 | 								numericField: {
 758 | 									type: "number",
 759 | 									defaultValue() {
 760 | 										return n++;
 761 | 									},
 762 | 								},
 763 | 							},
 764 | 						},
 765 | 					},
 766 | 					true,
 767 | 				);
 768 | 				const users = (await insertRandom("user", 5)).map(
 769 | 					(x) => x[0],
 770 | 				) as (User & { numericField: number })[];
 771 | 				const result = await adapter.findMany<User>({
 772 | 					model: "user",
 773 | 					sortBy: { field: "numericField", direction: "asc" },
 774 | 					limit: 2,
 775 | 					offset: 2,
 776 | 				});
 777 | 				expect(result.length).toBe(2);
 778 | 				expect(result).toEqual(
 779 | 					users.sort((a, b) => a.numericField - b.numericField).slice(2, 4),
 780 | 				);
 781 | 			},
 782 | 		"findMany - should find many models with sortBy and limit and offset and where":
 783 | 			async () => {
 784 | 				let n = -1;
 785 | 				await modifyBetterAuthOptions(
 786 | 					{
 787 | 						user: {
 788 | 							additionalFields: {
 789 | 								numericField: {
 790 | 									type: "number",
 791 | 									defaultValue() {
 792 | 										return n++;
 793 | 									},
 794 | 								},
 795 | 							},
 796 | 						},
 797 | 					},
 798 | 					true,
 799 | 				);
 800 | 				let users = (await insertRandom("user", 10)).map(
 801 | 					(x) => x[0],
 802 | 				) as (User & { numericField: number })[];
 803 | 
 804 | 				// update the last three users to end with "last"
 805 | 				let i = -1;
 806 | 				for (const user of users) {
 807 | 					i++;
 808 | 					if (i < 5) continue;
 809 | 					const result = await adapter.update<User>({
 810 | 						model: "user",
 811 | 						where: [{ field: "id", value: user.id }],
 812 | 						update: { name: user.name + "-last" },
 813 | 					});
 814 | 					if (!result) throw new Error("No result");
 815 | 					users[i]!.name = result.name;
 816 | 					users[i]!.updatedAt = result.updatedAt;
 817 | 				}
 818 | 
 819 | 				const result = await adapter.findMany<User & { numericField: number }>({
 820 | 					model: "user",
 821 | 					sortBy: { field: "numericField", direction: "asc" },
 822 | 					limit: 2,
 823 | 					offset: 2,
 824 | 					where: [{ field: "name", value: "last", operator: "ends_with" }],
 825 | 				});
 826 | 
 827 | 				// Order of operation for most DBs:
 828 | 				// FROM → WHERE → SORT BY → OFFSET → LIMIT
 829 | 
 830 | 				let expectedResult: any[] = [];
 831 | 				expectedResult = users
 832 | 					.filter((user) => user.name.endsWith("last"))
 833 | 					.sort((a, b) => a.numericField - b.numericField)
 834 | 					.slice(2, 4);
 835 | 
 836 | 				try {
 837 | 					expect(result.length).toBe(2);
 838 | 					expect(result).toEqual(expectedResult);
 839 | 				} catch (error) {
 840 | 					console.log(`--------------------------------`);
 841 | 					console.log(`results:`);
 842 | 					console.log(result.map((x) => x.id));
 843 | 					console.log(`expected results, sorted:`);
 844 | 					console.log(
 845 | 						users
 846 | 							.filter((x) => x.name.toString().endsWith("last"))
 847 | 							.map((x) => x.numericField)
 848 | 							.sort((a, b) => a - b),
 849 | 					);
 850 | 					console.log(`expected results, sorted + offset:`);
 851 | 					console.log(
 852 | 						users
 853 | 							.filter((x) => x.name.toString().endsWith("last"))
 854 | 							.map((x) => x.numericField)
 855 | 							.sort((a, b) => a - b)
 856 | 							.slice(2, 4),
 857 | 					);
 858 | 					console.log(`--------------------------------`);
 859 | 					console.log("FAIL", error);
 860 | 					console.log(`--------------------------------`);
 861 | 					throw error;
 862 | 				}
 863 | 			},
 864 | 		"update - should update a model": async () => {
 865 | 			const [user] = await insertRandom("user");
 866 | 			const result = await adapter.update<User>({
 867 | 				model: "user",
 868 | 				where: [{ field: "id", value: user.id }],
 869 | 				update: { name: "test-name" },
 870 | 			});
 871 | 			const expectedResult = {
 872 | 				...user,
 873 | 				name: "test-name",
 874 | 			};
 875 | 			// because of `onUpdate` hook, the updatedAt field will be different
 876 | 			result!.updatedAt = user.updatedAt;
 877 | 			expect(result).toEqual(expectedResult);
 878 | 			const findResult = await adapter.findOne<User>({
 879 | 				model: "user",
 880 | 				where: [{ field: "id", value: user.id }],
 881 | 			});
 882 | 			// because of `onUpdate` hook, the updatedAt field will be different
 883 | 			findResult!.updatedAt = user.updatedAt;
 884 | 			expect(findResult).toEqual(expectedResult);
 885 | 		},
 886 | 		"updateMany - should update all models when where is empty": async () => {
 887 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 888 | 			await adapter.updateMany({
 889 | 				model: "user",
 890 | 				where: [],
 891 | 				update: { name: "test-name" },
 892 | 			});
 893 | 			const result = await adapter.findMany<User>({
 894 | 				model: "user",
 895 | 			});
 896 | 			expect(sortModels(result)).toEqual(
 897 | 				sortModels(users).map((user, i) => ({
 898 | 					...user,
 899 | 					name: "test-name",
 900 | 					updatedAt: sortModels(result)[i]!.updatedAt,
 901 | 				})),
 902 | 			);
 903 | 		},
 904 | 		"updateMany - should update many models with a specific where":
 905 | 			async () => {
 906 | 				const users = (await insertRandom("user", 3)).map((x) => x[0]);
 907 | 				await adapter.updateMany({
 908 | 					model: "user",
 909 | 					where: [{ field: "id", value: users[0]!.id }],
 910 | 					update: { name: "test-name" },
 911 | 				});
 912 | 				const result = await adapter.findOne<User>({
 913 | 					model: "user",
 914 | 					where: [{ field: "id", value: users[0]!.id }],
 915 | 				});
 916 | 				expect(result).toEqual({
 917 | 					...users[0],
 918 | 					name: "test-name",
 919 | 					updatedAt: result!.updatedAt,
 920 | 				});
 921 | 			},
 922 | 		"updateMany - should update many models with a multiple where":
 923 | 			async () => {
 924 | 				const users = (await insertRandom("user", 3)).map((x) => x[0]);
 925 | 				await adapter.updateMany({
 926 | 					model: "user",
 927 | 					where: [
 928 | 						{ field: "id", value: users[0]!.id, connector: "OR" },
 929 | 						{ field: "id", value: users[1]!.id, connector: "OR" },
 930 | 					],
 931 | 					update: { name: "test-name" },
 932 | 				});
 933 | 				const result = await adapter.findOne<User>({
 934 | 					model: "user",
 935 | 					where: [{ field: "id", value: users[0]!.id }],
 936 | 				});
 937 | 				expect(result).toEqual({
 938 | 					...users[0],
 939 | 					name: "test-name",
 940 | 					updatedAt: result!.updatedAt,
 941 | 				});
 942 | 			},
 943 | 		"delete - should delete a model": async () => {
 944 | 			const [user] = await insertRandom("user");
 945 | 			await adapter.delete({
 946 | 				model: "user",
 947 | 				where: [{ field: "id", value: user.id }],
 948 | 			});
 949 | 			const result = await adapter.findOne<User>({
 950 | 				model: "user",
 951 | 				where: [{ field: "id", value: user.id }],
 952 | 			});
 953 | 			expect(result).toBeNull();
 954 | 		},
 955 | 		"delete - should not throw on record not found": async () => {
 956 | 			await expect(
 957 | 				adapter.delete({
 958 | 					model: "user",
 959 | 					where: [{ field: "id", value: "100000" }],
 960 | 				}),
 961 | 			).resolves.not.toThrow();
 962 | 		},
 963 | 		"deleteMany - should delete many models": async () => {
 964 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
 965 | 			await adapter.deleteMany({
 966 | 				model: "user",
 967 | 				where: [
 968 | 					{ field: "id", value: users[0]!.id, connector: "OR" },
 969 | 					{ field: "id", value: users[1]!.id, connector: "OR" },
 970 | 				],
 971 | 			});
 972 | 			const result = await adapter.findMany<User>({
 973 | 				model: "user",
 974 | 			});
 975 | 			expect(sortModels(result)).toEqual(sortModels(users.slice(2)));
 976 | 		},
 977 | 		"deleteMany - starts_with should not interpret regex patterns":
 978 | 			async () => {
 979 | 				// Create a user whose name literally starts with the regex-like prefix
 980 | 				const userTemplate = await generate("user");
 981 | 				const literalRegexUser = await adapter.create<User>({
 982 | 					model: "user",
 983 | 					data: {
 984 | 						...userTemplate,
 985 | 						name: ".*danger",
 986 | 					},
 987 | 					forceAllowId: true,
 988 | 				});
 989 | 
 990 | 				// Also create some normal users that do NOT start with ".*"
 991 | 				const normalUsers = (await insertRandom("user", 3)).map((x) => x[0]);
 992 | 
 993 | 				await adapter.deleteMany({
 994 | 					model: "user",
 995 | 					where: [{ field: "name", value: ".*", operator: "starts_with" }],
 996 | 				});
 997 | 
 998 | 				// The literal ".*danger" user should be deleted
 999 | 				const deleted = await adapter.findOne<User>({
1000 | 					model: "user",
1001 | 					where: [{ field: "id", value: literalRegexUser.id }],
1002 | 				});
1003 | 				expect(deleted).toBeNull();
1004 | 
1005 | 				// Normal users should remain
1006 | 				for (const user of normalUsers) {
1007 | 					const stillThere = await adapter.findOne<User>({
1008 | 						model: "user",
1009 | 						where: [{ field: "id", value: user.id }],
1010 | 					});
1011 | 					expect(stillThere).not.toBeNull();
1012 | 				}
1013 | 			},
1014 | 		"deleteMany - ends_with should not interpret regex patterns": async () => {
1015 | 			// Create a user whose name literally ends with the regex-like suffix
1016 | 			const userTemplate = await generate("user");
1017 | 			const literalRegexUser = await adapter.create<User>({
1018 | 				model: "user",
1019 | 				data: {
1020 | 					...userTemplate,
1021 | 					name: "danger.*",
1022 | 				},
1023 | 				forceAllowId: true,
1024 | 			});
1025 | 
1026 | 			const normalUsers = (await insertRandom("user", 3)).map((x) => x[0]);
1027 | 
1028 | 			await adapter.deleteMany({
1029 | 				model: "user",
1030 | 				where: [{ field: "name", value: ".*", operator: "ends_with" }],
1031 | 			});
1032 | 
1033 | 			const deleted = await adapter.findOne<User>({
1034 | 				model: "user",
1035 | 				where: [{ field: "id", value: literalRegexUser.id }],
1036 | 			});
1037 | 			expect(deleted).toBeNull();
1038 | 
1039 | 			for (const user of normalUsers) {
1040 | 				const stillThere = await adapter.findOne<User>({
1041 | 					model: "user",
1042 | 					where: [{ field: "id", value: user.id }],
1043 | 				});
1044 | 				expect(stillThere).not.toBeNull();
1045 | 			}
1046 | 		},
1047 | 		"deleteMany - contains should not interpret regex patterns": async () => {
1048 | 			// Create a user whose name literally contains the regex-like pattern
1049 | 			const userTemplate = await generate("user");
1050 | 			const literalRegexUser = await adapter.create<User>({
1051 | 				model: "user",
1052 | 				data: {
1053 | 					...userTemplate,
1054 | 					name: "prefix-.*-suffix",
1055 | 				},
1056 | 				forceAllowId: true,
1057 | 			});
1058 | 
1059 | 			const normalUsers = (await insertRandom("user", 3)).map((x) => x[0]);
1060 | 
1061 | 			await adapter.deleteMany({
1062 | 				model: "user",
1063 | 				where: [{ field: "name", value: ".*", operator: "contains" }],
1064 | 			});
1065 | 
1066 | 			const deleted = await adapter.findOne<User>({
1067 | 				model: "user",
1068 | 				where: [{ field: "id", value: literalRegexUser.id }],
1069 | 			});
1070 | 			expect(deleted).toBeNull();
1071 | 
1072 | 			for (const user of normalUsers) {
1073 | 				const stillThere = await adapter.findOne<User>({
1074 | 					model: "user",
1075 | 					where: [{ field: "id", value: user.id }],
1076 | 				});
1077 | 				expect(stillThere).not.toBeNull();
1078 | 			}
1079 | 		},
1080 | 		"deleteMany - should delete many models with numeric values": async () => {
1081 | 			let i = 0;
1082 | 			await modifyBetterAuthOptions(
1083 | 				{
1084 | 					user: {
1085 | 						additionalFields: {
1086 | 							numericField: {
1087 | 								type: "number",
1088 | 								defaultValue() {
1089 | 									return i++;
1090 | 								},
1091 | 							},
1092 | 						},
1093 | 					},
1094 | 				},
1095 | 				true,
1096 | 			);
1097 | 			const users = (await insertRandom("user", 3)).map(
1098 | 				(x) => x[0],
1099 | 			) as (User & { numericField: number })[];
1100 | 			if (!users[0] || !users[1] || !users[2]) {
1101 | 				expect(false).toBe(true);
1102 | 				throw new Error("Users not found");
1103 | 			}
1104 | 			expect(users[0].numericField).toEqual(0);
1105 | 			expect(users[1].numericField).toEqual(1);
1106 | 			expect(users[2].numericField).toEqual(2);
1107 | 
1108 | 			await adapter.deleteMany({
1109 | 				model: "user",
1110 | 				where: [
1111 | 					{
1112 | 						field: "numericField",
1113 | 						value: users[0].numericField,
1114 | 						operator: "gt",
1115 | 					},
1116 | 				],
1117 | 			});
1118 | 
1119 | 			const result = await adapter.findMany<User>({
1120 | 				model: "user",
1121 | 			});
1122 | 			expect(result).toEqual([users[0]]);
1123 | 		},
1124 | 		"deleteMany - should delete many models with boolean values": async () => {
1125 | 			const users = (await insertRandom("user", 3)).map((x) => x[0]);
1126 | 			// in this test, we have 3 users, two of which have emailVerified set to true and one to false
1127 | 			// delete all that has emailVerified set to true, and expect users[1] to be the only one left
1128 | 			if (!users[0] || !users[1] || !users[2]) {
1129 | 				expect(false).toBe(true);
1130 | 				throw new Error("Users not found");
1131 | 			}
1132 | 			await adapter.updateMany({
1133 | 				model: "user",
1134 | 				where: [],
1135 | 				update: { emailVerified: true },
1136 | 			});
1137 | 			await adapter.update({
1138 | 				model: "user",
1139 | 				where: [{ field: "id", value: users[1].id }],
1140 | 				update: { emailVerified: false },
1141 | 			});
1142 | 			await adapter.deleteMany({
1143 | 				model: "user",
1144 | 				where: [{ field: "emailVerified", value: true }],
1145 | 			});
1146 | 			const result = await adapter.findMany<User>({
1147 | 				model: "user",
1148 | 			});
1149 | 			expect(result).toHaveLength(1);
1150 | 			expect(result.find((user) => user.id === users[0]?.id)).toBeUndefined();
1151 | 			expect(result.find((user) => user.id === users[1]?.id)).toBeDefined();
1152 | 			expect(result.find((user) => user.id === users[2]?.id)).toBeUndefined();
1153 | 		},
1154 | 		"count - should count many models": async () => {
1155 | 			const users = await insertRandom("user", 15);
1156 | 			const result = await adapter.count({
1157 | 				model: "user",
1158 | 			});
1159 | 			expect(result).toEqual(users.length);
1160 | 		},
1161 | 		"count - should return 0 with no rows to count": async () => {
1162 | 			const result = await adapter.count({
1163 | 				model: "user",
1164 | 			});
1165 | 			expect(result).toEqual(0);
1166 | 		},
1167 | 		"count - should count with where clause": async () => {
1168 | 			const users = (await insertRandom("user", 15)).map((x) => x[0]);
1169 | 			const result = await adapter.count({
1170 | 				model: "user",
1171 | 				where: [
1172 | 					{ field: "id", value: users[2]!.id, connector: "OR" },
1173 | 					{ field: "id", value: users[3]!.id, connector: "OR" },
1174 | 				],
1175 | 			});
1176 | 			expect(result).toEqual(2);
1177 | 		},
1178 | 		"update - should correctly return record when updating a field used in where clause":
1179 | 			async () => {
1180 | 				// This tests the fix for MySQL where updating a field that's in the where clause
1181 | 				// would previously fail to find the record using the old value
1182 | 				const [user] = await insertRandom("user");
1183 | 				const originalEmail = user.email;
1184 | 
1185 | 				// Update the email, using the old email in the where clause
1186 | 				const result = await adapter.update<User>({
1187 | 					model: "user",
1188 | 					where: [{ field: "email", value: originalEmail }],
1189 | 					update: { email: "[email protected]" },
1190 | 				});
1191 | 
1192 | 				// Should return the updated record with the new email
1193 | 				expect(result).toBeDefined();
1194 | 				expect(result!.email).toBe("[email protected]");
1195 | 				expect(result!.id).toBe(user.id);
1196 | 
1197 | 				// Verify the update persisted by finding with new email
1198 | 				const foundUser = await adapter.findOne<User>({
1199 | 					model: "user",
1200 | 					where: [{ field: "email", value: "[email protected]" }],
1201 | 				});
1202 | 				expect(foundUser).toBeDefined();
1203 | 				expect(foundUser!.id).toBe(user.id);
1204 | 
1205 | 				// Old email should not exist
1206 | 				const oldUser = await adapter.findOne<User>({
1207 | 					model: "user",
1208 | 					where: [{ field: "email", value: originalEmail }],
1209 | 				});
1210 | 				expect(oldUser).toBeNull();
1211 | 			},
1212 | 
1213 | 		"update - should handle updating multiple fields including where clause field":
1214 | 			async () => {
1215 | 				const [user] = await insertRandom("user");
1216 | 				const originalEmail = user.email;
1217 | 
1218 | 				const result = await adapter.update<User>({
1219 | 					model: "user",
1220 | 					where: [{ field: "email", value: originalEmail }],
1221 | 					update: {
1222 | 						email: "[email protected]",
1223 | 						name: "Updated Name",
1224 | 						emailVerified: true,
1225 | 					},
1226 | 				});
1227 | 
1228 | 				expect(result!.email).toBe("[email protected]");
1229 | 				expect(result!.name).toBe("Updated Name");
1230 | 				expect(result!.emailVerified).toBe(true);
1231 | 				expect(result!.id).toBe(user.id);
1232 | 			},
1233 | 
1234 | 		"update - should work when updated field is not in where clause":
1235 | 			async () => {
1236 | 				// Regression test: ensure normal updates still work
1237 | 				const [user] = await insertRandom("user");
1238 | 
1239 | 				const result = await adapter.update<User>({
1240 | 					model: "user",
1241 | 					where: [{ field: "email", value: user.email }],
1242 | 					update: { name: "Updated Name Only" },
1243 | 				});
1244 | 
1245 | 				expect(result!.name).toBe("Updated Name Only");
1246 | 				expect(result!.email).toBe(user.email); // Should remain unchanged
1247 | 				expect(result!.id).toBe(user.id);
1248 | 			},
1249 | 	};
1250 | };
1251 | 
```
Page 57/69FirstPrevNextLast