Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork0
🧰 A modular adapter layer for working with any database (Drizzle, Prisma, MongoDB, Kysely & more)
License
productdevbook/unadapter
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
A universal adapter interface for connecting various databases and ORMs with a standardized API.
- 🔄 Standardized interface for common database operations (create, read, update, delete)
- 🛡️ Type-safe operations
- 🔍 Support for complex queries and transformations
- 🌐 Database-agnostic application code
- 🔄 Easy switching between different database providers
- 🗺️ Custom field mapping
- 📊 Support for various data types across different database systems
- 🏗️ Fully customizable schema definition
unadapter provides a consistent interface for database operations, allowing you to switch between different database solutions without changing your application code. This is particularly useful for applications that need database-agnostic operations or might need to switch database providers in the future.
🚧 Development Status
This project is based on the adapter architecture frombetter-auth and is being developed to provide a standalone, ESM-compatible adapter solution that can be used across various open-source projects.
- Initial adapter architecture
- Basic adapters implementation
- Comprehensive documentation
- Performance optimizations
- Additional adapter types
- Integration examples
- Complete abstraction from better-auth and compatibility with all software systems
# Using pnpmpnpm add unadapter# Using npmnpm install unadapter# Using yarnyarn add unadapter
You'll also need to install the specific database driver or ORM you plan to use.
| Adapter | Description | Status |
|---|---|---|
| Memory Adapter | In-memory adapter ideal for development and testing | ✅ Ready |
| Prisma Adapter | For Prisma ORM | ✅ Ready |
| MongoDB Adapter | For MongoDB | ✅ Ready |
| Drizzle Adapter | For Drizzle ORM | ✅ Ready |
| Kysely Adapter | For Kysely SQL query builder | ✅ Ready |
Basic Usage
importtype{PluginSchema}from'unadapter/types'import{createAdapter,createTable,mergePluginSchemas}from'unadapter'import{memoryAdapter}from'unadapter/memory'// Create an in-memory database for testingconstdb={user:[],session:[]}// Define a consistent options interface that can be reusedinterfaceCustomOptions{appName?:stringplugins?:{schema?:PluginSchema}[]user?:{fields?:{name?:stringemail?:stringemailVerified?:stringimage?:stringcreatedAt?:string}}}consttables=createTable<CustomOptions>((options)=>{const{ user, ...pluginTables}=mergePluginSchemas<CustomOptions>(options)||{}return{user:{modelName:'user',fields:{name:{type:'string',required:true,fieldName:options?.user?.fields?.name||'name',sortable:true,},email:{type:'string',unique:true,required:true,fieldName:options?.user?.fields?.email||'email',sortable:true,},emailVerified:{type:'boolean',defaultValue:()=>false,required:true,fieldName:options?.user?.fields?.emailVerified||'emailVerified',},createdAt:{type:'date',defaultValue:()=>newDate(),required:true,fieldName:options?.user?.fields?.createdAt||'createdAt',},updatedAt:{type:'date',defaultValue:()=>newDate(),required:true,fieldName:options?.user?.fields?.updatedAt||'updatedAt',}, ...user?.fields, ...options?.user?.fields,}}}})constadapter=createAdapter(tables,{database:memoryAdapter(db,{}),plugins:[]// Optional plugins})// Now you can use the adapter to perform database operationsconstuser=awaitadapter.create({model:'user',data:{name:'John Doe',email:'john@example.com',emailVerified:true,createdAt:newDate(),updatedAt:newDate()}})// Find the userconstfoundUsers=awaitadapter.findMany({model:'user',where:[{field:'email',value:'john@example.com',operator:'eq',}]})
Using Custom Schema and Plugins
importtype{PluginSchema}from'unadapter/types'import{createAdapter,createTable,mergePluginSchemas}from'unadapter'import{memoryAdapter}from'unadapter/memory'// Create an in-memory database for testingconstdb={users:[],products:[]}// Using the same pattern for CustomOptionsinterfaceCustomOptions{appName?:stringplugins?:{schema?:PluginSchema}[]user?:{fields?:{fullName?:stringemail?:stringisActive?:string}}product?:{fields?:{title?:stringprice?:stringownerId?:string}}}consttables=createTable<CustomOptions>((options)=>{const{ user, product, ...pluginTables}=mergePluginSchemas<CustomOptions>(options)||{}return{user:{modelName:'users',// The actual table/collection name in your databasefields:{fullName:{type:'string',required:true,fieldName:options?.user?.fields?.fullName||'full_name',sortable:true,},email:{type:'string',unique:true,required:true,fieldName:options?.user?.fields?.email||'email_address',},isActive:{type:'boolean',fieldName:options?.user?.fields?.isActive||'is_active',defaultValue:()=>true,},createdAt:{type:'date',fieldName:'created_at',defaultValue:()=>newDate(),}, ...user?.fields, ...options?.user?.fields,}},product:{modelName:'products',fields:{title:{type:'string',required:true,fieldName:options?.product?.fields?.title||'title',},price:{type:'number',required:true,fieldName:options?.product?.fields?.price||'price',},ownerId:{type:'string',references:{model:'user',field:'id',onDelete:'cascade',},required:true,fieldName:options?.product?.fields?.ownerId||'owner_id',}, ...product?.fields, ...options?.product?.fields,}}}})// User profile plugin schemaconstuserProfilePlugin={schema:{user:{modelName:'user',fields:{bio:{type:'string',required:false,fieldName:'bio',},location:{type:'string',required:false,fieldName:'location',}}}}}constadapter=createAdapter(tables,{database:memoryAdapter(db,{}),plugins:[userProfilePlugin],})// Now you can use the adapter with your custom schemaconstuser=awaitadapter.create({model:'user',data:{fullName:'John Doe',email:'john@example.com',bio:'Software developer',location:'New York'}})// Create a product linked to the userconstproduct=awaitadapter.create({model:'product',data:{title:'Awesome Product',price:99.99,ownerId:user.id}})
MongoDB Adapter Example
importtype{PluginSchema}from'unadapter/types'import{createAdapter,createTable,mergePluginSchemas}from'unadapter'import{MongoClient}from'mongodb'import{mongodbAdapter}from'unadapter/mongodb'// Create a database clientconstclient=newMongoClient('mongodb://localhost:27017')awaitclient.connect()constdb=client.db('myDatabase')// Using the same pattern for CustomOptionsinterfaceCustomOptions{appName?:stringplugins?:{schema?:PluginSchema}[]user?:{fields?:{name?:stringemail?:stringsettings?:string}}}consttables=createTable<CustomOptions>((options)=>{const{ user, ...pluginTables}=mergePluginSchemas<CustomOptions>(options)||{}return{user:{modelName:'users',fields:{name:{type:'string',required:true,fieldName:options?.user?.fields?.name||'name',},email:{type:'string',required:true,unique:true,fieldName:options?.user?.fields?.email||'email',},settings:{type:'json',required:false,fieldName:options?.user?.fields?.settings||'settings',},createdAt:{type:'date',defaultValue:()=>newDate(),fieldName:'createdAt',}, ...user?.fields, ...options?.user?.fields,}}}})// Initialize the adapterconstadapter=createAdapter(tables,{database:mongodbAdapter(db,{useNumberId:false}),plugins:[]})// Use the adapterconstuser=awaitadapter.create({model:'user',data:{name:'Jane Doe',email:'jane@example.com',settings:{theme:'dark',notifications:true}}})
Prisma Adapter Example
importtype{PluginSchema}from'unadapter/types'import{createAdapter,createTable,mergePluginSchemas}from'unadapter'import{PrismaClient}from'@prisma/client'import{prismaAdapter}from'unadapter/prisma'// Initialize Prisma clientconstprisma=newPrismaClient()// Using the same pattern for CustomOptionsinterfaceCustomOptions{appName?:stringplugins?:{schema?:PluginSchema}[]user?:{fields?:{name?:stringemail?:stringprofile?:string}}post?:{fields?:{title?:stringcontent?:stringauthorId?:string}}}consttables=createTable<CustomOptions>((options)=>{const{ user, post, ...pluginTables}=mergePluginSchemas<CustomOptions>(options)||{}return{user:{modelName:'User',// Match your Prisma model name (case-sensitive)fields:{name:{type:'string',required:true,fieldName:options?.user?.fields?.name||'name',},email:{type:'string',required:true,unique:true,fieldName:options?.user?.fields?.email||'email',},profile:{type:'json',required:false,fieldName:options?.user?.fields?.profile||'profile',},createdAt:{type:'date',defaultValue:()=>newDate(),fieldName:'createdAt',}, ...user?.fields, ...options?.user?.fields,}},post:{modelName:'Post',fields:{title:{type:'string',required:true,fieldName:options?.post?.fields?.title||'title',},content:{type:'string',required:false,fieldName:options?.post?.fields?.content||'content',},published:{type:'boolean',defaultValue:()=>false,fieldName:'published',},authorId:{type:'string',references:{model:'user',field:'id',onDelete:'cascade',},required:true,fieldName:options?.post?.fields?.authorId||'authorId',}, ...post?.fields, ...options?.post?.fields,}}}})// Initialize the adapterconstadapter=createAdapter(tables,{database:prismaAdapter(prisma,{provider:'postgresql',debugLogs:true,usePlural:false}),plugins:[]})// Use the adapterconstuser=awaitadapter.create({model:'user',data:{name:'John Smith',email:'john.smith@example.com',profile:{bio:'Software developer',location:'New York'}}})
Drizzle Adapter Example
importtype{PluginSchema}from'unadapter/types'import{createAdapter,createTable,mergePluginSchemas}from'unadapter'import{sql}from'drizzle-orm'import{drizzle}from'drizzle-orm/node-postgres'import{pgTable,text,timestamp,uuid,varchar}from'drizzle-orm/pg-core'import{drizzleAdapter}from'unadapter/drizzle'import'dotenv/config'// Define your Drizzle schemaexportconstrole=pgTable('role',{id:uuid('id').primaryKey().default(sql`gen_random_uuid()`),name:varchar('name',{length:255}).notNull(),key:varchar('key',{length:255}).notNull().unique(),type:varchar('type',{length:255}).notNull().default('user'),description:varchar('description',{length:500}).notNull(),userId:uuid('user_id').notNull(),permissions:text('permissions').notNull().default('0'),updatedAt:timestamp('updated_at').notNull().default(sql`now()`),createdAt:timestamp('created_at').notNull().default(sql`now()`),},)// Using the same pattern for CustomOptionsinterfaceCustomOptions{appName?:stringplugins?:{schema?:PluginSchema}[]role?:{fields?:{name?:stringdescription?:stringkey?:stringpermissions?:stringuserId?:string}}}consttables=createTable<CustomOptions>((options)=>{const{ user, role, ...pluginTables}=mergePluginSchemas<CustomOptions>(options)||{}return{role:{modelName:'role',fields:{name:{type:'string',required:true,fieldName:options?.role?.fields?.name||'name',},description:{type:'string',required:true,fieldName:options?.role?.fields?.description||'description',},key:{type:'string',required:true,fieldName:options?.role?.fields?.key||'key',},permissions:{type:'string',required:true,fieldName:options?.role?.fields?.permissions||'permissions',},userId:{type:'string',required:true,references:{model:'user',field:'id',onDelete:'cascade',},fieldName:options?.role?.fields?.userId||'user_id',},createdAt:{type:'date',required:true,defaultValue:newDate(),},updatedAt:{type:'date',required:true,defaultValue:newDate(),}, ...role?.fields, ...options?.role?.fields,},},}})// Initialize the adapter with the Drizzle schemaconstadapter=createAdapter(tables,{database:drizzleAdapter(drizzle(process.env.DATABASE_URL!),{provider:'pg',debugLogs:true,schema:{ role,},},),plugins:[],// Optional plugins})// Use the adapterconstrole=awaitadapter.create({model:'role',data:{name:'Test Role',description:'This is a test role',key:'test_role',permissions:'read,write',userId:'8eea9d01-6c73-4933-bb0f-811cb7d4a862',createdAt:newDate(),updatedAt:newDate(),},})
Kysely Adapter Example
importtype{PluginSchema}from'unadapter/types'import{createAdapter,createTable,mergePluginSchemas}from'unadapter'import{Kysely,PostgresDialect}from'kysely'importpgfrom'pg'import{kyselyAdapter}from'unadapter/kysely'// Create PostgreSQL connection poolconstpool=newpg.Pool({host:'localhost',database:'mydatabase',user:'myuser',password:'mypassword'})// Initialize Kysely with PostgreSQL dialectconstdb=newKysely({dialect:newPostgresDialect({ pool})})// Using the same pattern for CustomOptionsinterfaceCustomOptions{appName?:stringplugins?:{schema?:PluginSchema}[]user?:{fields?:{name?:stringemail?:stringactive?:stringmeta?:string}}article?:{fields?:{title?:stringcontent?:stringauthorId?:string}}}consttables=createTable<CustomOptions>((options)=>{const{ user, article, ...pluginTables}=mergePluginSchemas<CustomOptions>(options)||{}return{user:{modelName:'users',fields:{name:{type:'string',required:true,fieldName:options?.user?.fields?.name||'name',},email:{type:'string',required:true,unique:true,fieldName:options?.user?.fields?.email||'email',},active:{type:'boolean',defaultValue:()=>true,fieldName:options?.user?.fields?.active||'is_active',},meta:{type:'json',required:false,fieldName:options?.user?.fields?.meta||'meta_data',},createdAt:{type:'date',defaultValue:()=>newDate(),fieldName:'created_at',}, ...user?.fields, ...options?.user?.fields,}},article:{modelName:'articles',fields:{title:{type:'string',required:true,fieldName:options?.article?.fields?.title||'title',},content:{type:'string',required:true,fieldName:options?.article?.fields?.content||'content',},authorId:{type:'string',references:{model:'user',field:'id',onDelete:'cascade',},required:true,fieldName:options?.article?.fields?.authorId||'author_id',},tags:{type:'array',required:false,fieldName:'tags',},publishedAt:{type:'date',required:false,fieldName:'published_at',}, ...article?.fields, ...options?.article?.fields,}}}})// Initialize the adapterconstadapter=createAdapter(tables,{database:kyselyAdapter(db,{defaultSchema:'public'}),plugins:[]})// Use the adapterconstuser=awaitadapter.create({model:'user',data:{name:'Robert Chen',email:'robert@example.com',meta:{interests:['programming','reading'],location:'San Francisco'}}})
Adapter Interface
All adapters implement the following interface:
interfaceAdapter{// Create a new recordcreate<T>({model:string,data:Omit<T,'id'>,select?:string[]}):Promise<T>;// Find multiple recordsfindMany<T>({model:string,where?:Where[],limit?:number,sortBy?:{field:string,direction:'asc'|'desc'},offset?:number}):Promise<T[]>;// Update a recordupdate<T>({model:string,where:Where[],update:Record<string,any>}):Promise<T|null>;// Update multiple recordsupdateMany({model:string,where:Where[],update:Record<string,any>}):Promise<number>;// Delete a recorddelete({model:string,where:Where[]}):Promise<void>;// Delete multiple recordsdeleteMany({model:string,where:Where[]}):Promise<number>;// Count recordscount({model:string,where?:Where[]}):Promise<number>;}
Where Clause Interface
TheWhere interface is used for filtering records:
interfaceWhere{field:stringvalue?:anyoperator?:'eq'|'ne'|'gt'|'gte'|'lt'|'lte'|'in'|'contains'|'starts_with'|'ends_with'connector?:'AND'|'OR'}
Field Types and Attributes
When defining your schema, you can use the following field types and attributes:
interfaceFieldAttribute{// The type of the fieldtype:'string'|'number'|'boolean'|'date'|'json'|'array'// Whether this field is requiredrequired?:boolean// Whether this field should be uniqueunique?:boolean// The actual column/field name in the databasefieldName?:string// Whether this field can be sortedsortable?:boolean// Default value functiondefaultValue?:()=>any// Reference to another model (for foreign keys)references?:{model:stringfield:stringonDelete?:'cascade'|'set null'|'restrict'}// Custom transformationstransform?:{input?:(value:any)=>anyoutput?:(value:any)=>any}}
Contributions are welcome! Feel free toopen issues orsubmit pull requests to help improve unadapter.
Development Setup
Clone the repository:
git clone https://github.com/productdevbook/unadapter.gitcd unadapterInstall dependencies:
pnpm install
Run tests:
pnpmtestBuild the project:
pnpm build
This project draws inspiration and core concepts from:
- better-auth - The original adapter architecture that inspired this project
See theLICENSE file for details.
unadapter is a work in progress. Stay tuned for updates!
About
🧰 A modular adapter layer for working with any database (Drizzle, Prisma, MongoDB, Kysely & more)
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Packages0
Uh oh!
There was an error while loading.Please reload this page.