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 | ```