betterauth-dynamodb-adapter 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,13 @@
1
1
  # betterauth-dynamodb-adapter
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/betterauth-dynamodb-adapter.svg)](https://www.npmjs.com/package/betterauth-dynamodb-adapter)
4
+ [![npm downloads](https://img.shields.io/npm/dm/betterauth-dynamodb-adapter.svg)](https://www.npmjs.com/package/betterauth-dynamodb-adapter)
4
5
  [![license](https://img.shields.io/npm/l/betterauth-dynamodb-adapter.svg)](LICENSE)
5
- ![TS](https://img.shields.io/badge/TypeScript-%E2%9C%93-blue)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
7
+ [![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/)
8
+ [![ESLint](https://img.shields.io/badge/ESLint-enabled-brightgreen.svg)](eslint.config.ts)
9
+ [![Tests](https://img.shields.io/badge/Tests-18%2F18%20passing-brightgreen.svg)](src/__tests__/adapter.test.ts)
10
+ [![Code Quality](https://img.shields.io/badge/Code%20Quality-A-brightgreen.svg)](#)
6
11
 
7
12
  A DynamoDB adapter for [Better Auth](https://www.better-auth.com/). Uses a single-table design with composite keys and a GSI for model-based queries.
8
13
 
@@ -53,6 +58,8 @@ Then create a Global Secondary Index:
53
58
 
54
59
  Optionally, enable TTL on `expiresAt` to auto-expire sessions and verification tokens.
55
60
 
61
+ > **TTL compatibility**: Better Auth stores `expiresAt` as an ISO date string, but DynamoDB TTL requires a Unix epoch in seconds. This adapter automatically converts between the two formats — ISO strings are stored as epoch seconds in DynamoDB, and converted back to ISO strings when read.
62
+
56
63
 
57
64
  ### AWS CLI
58
65
 
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- let e=require(`better-auth/adapters`),t=require(`@aws-sdk/client-dynamodb`),n=require(`@aws-sdk/util-dynamodb`);var r=class extends Error{constructor(e,t){super(e),this.operation=t,this.name=`DynamoDBAdapterError`}};const i=i=>{if(!i.tableName||i.tableName.trim().length===0)throw new r(`tableName must not be empty`,`initialization`);let{tableName:a,region:o}=i,s=new t.DynamoDBClient({region:o});function c(e){let{_pk:t,_sk:n,_table:r,...i}=e;return i}function l(e){if(!e||e.length===0)throw new r(`where conditions cannot be empty`,`buildFilter`);let t=[],n={},i={};return e.forEach((e,a)=>{let o=`#w${a}`,s=`:w${a}`;i[o]=e.field;let c;switch(e.operator){case`ne`:n[s]=e.value,c=`${o} <> ${s}`;break;case`gt`:n[s]=e.value,c=`${o} > ${s}`;break;case`gte`:n[s]=e.value,c=`${o} >= ${s}`;break;case`lt`:n[s]=e.value,c=`${o} < ${s}`;break;case`lte`:n[s]=e.value,c=`${o} <= ${s}`;break;case`in`:case`not_in`:{let t=e.value;if(!Array.isArray(t)||t.length===0)throw new r(`${e.operator} operator requires non-empty array`,`buildFilter`);let i=t.map((e,t)=>`:w${a}_${t}`);t.forEach((e,t)=>{n[`:w${a}_${t}`]=e});let s=`${o} IN (${i.join(`,`)})`;c=e.operator===`not_in`?`NOT ${s}`:s;break}case`contains`:n[s]=e.value,c=`contains(${o}, ${s})`;break;case`starts_with`:n[s]=e.value,c=`begins_with(${o}, ${s})`;break;case`ends_with`:n[s]=e.value,c=`contains(${o}, ${s})`;break;default:n[s]=e.value,c=`${o} = ${s}`;break}a>0&&t.push(e.connector===`OR`?`OR`:`AND`),t.push(c)}),{expression:t.join(` `),vals:n,names:i}}async function u(e,i){if(!e||e.trim().length===0)throw new r(`model must not be empty`,`query`);let o={"#_table":`_table`},u={":_table":e},d,f=o,p=u;if(i&&i.length>0){let e=l(i);d=e.expression,f={...o,...e.names},p={...u,...e.vals}}try{return((await s.send(new t.QueryCommand({TableName:a,IndexName:`_table-index`,KeyConditionExpression:`#_table = :_table`,FilterExpression:d,ExpressionAttributeNames:f,ExpressionAttributeValues:(0,n.marshall)(p)}))).Items||[]).map(e=>c((0,n.unmarshall)(e)))}catch(t){throw new r(`Failed to query model "${e}": ${t instanceof Error?t.message:String(t)}`,`query`)}}return(0,e.createAdapterFactory)({config:{adapterId:`dynamodb`,adapterName:`DynamoDB Adapter`,supportsJSON:!1,supportsDates:!1,supportsBooleans:!1,supportsNumericIds:!1},adapter:()=>({create:async({model:e,data:i})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`create`);if(!i||typeof i!=`object`)throw new r(`data must be a valid object`,`create`);try{let r=i.id||crypto.randomUUID(),o={...i,id:r,_pk:`${e}#${r}`,_sk:`${e}#${r}`,_table:e};return await s.send(new t.PutItemCommand({TableName:a,Item:(0,n.marshall)(o,{removeUndefinedValues:!0})})),{...i,id:r}}catch(e){throw new r(`Failed to create item: ${e instanceof Error?e.message:String(e)}`,`create`)}},findOne:async({model:e,where:i})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`findOne`);if(!i||i.length===0)throw new r(`where conditions are required`,`findOne`);try{if(i.length===1&&i[0].field===`id`&&i[0].operator===`eq`){let r=i[0].value,o=await s.send(new t.GetItemCommand({TableName:a,Key:(0,n.marshall)({_pk:`${e}#${r}`,_sk:`${e}#${r}`})}));return o.Item?c((0,n.unmarshall)(o.Item)):null}return(await u(e,i))[0]||null}catch(e){throw new r(`Failed to find item: ${e instanceof Error?e.message:String(e)}`,`findOne`)}},findMany:async({model:e,where:t,limit:n,sortBy:i,offset:a})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`findMany`);if(n<0)throw new r(`limit must be non-negative`,`findMany`);if(a!==void 0&&a<0)throw new r(`offset must be non-negative`,`findMany`);try{let r=await u(e,t);if(i){let e=i.direction===`desc`?-1:1;r.sort((t,n)=>{let r=t[i.field],a=n[i.field];return r==null||a==null?0:r<a?-1*e:r>a?1*e:0})}return a&&(r=r.slice(a)),n&&(r=r.slice(0,n)),r}catch(e){throw new r(`Failed to find items: ${e instanceof Error?e.message:String(e)}`,`findMany`)}},update:async({model:e,where:i,update:o})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`update`);if(!i||i.length===0)throw new r(`where conditions are required`,`update`);if(!o||typeof o!=`object`)throw new r(`update data must be a valid object`,`update`);try{let r=await u(e,i);if(r.length===0)return null;let c=r[0],l=c.id,d=Object.entries(o),f=[],p={},m={};return d.forEach(([e,t],n)=>{e===`id`||e.startsWith(`_`)||(f.push(`#u${n} = :u${n}`),m[`#u${n}`]=e,p[`:u${n}`]=t)}),f.length===0?c:(await s.send(new t.UpdateItemCommand({TableName:a,Key:(0,n.marshall)({_pk:`${e}#${l}`,_sk:`${e}#${l}`}),UpdateExpression:`SET ${f.join(`, `)}`,ExpressionAttributeNames:m,ExpressionAttributeValues:(0,n.marshall)(p,{removeUndefinedValues:!0})})),{...c,...o})}catch(e){throw new r(`Failed to update item: ${e instanceof Error?e.message:String(e)}`,`update`)}},updateMany:async({model:e,where:i,update:o})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`updateMany`);if(!i||i.length===0)throw new r(`where conditions are required`,`updateMany`);try{let r=await u(e,i),c=0;for(let i of r){let r=Object.entries(o),l=[],u={},d={};r.forEach(([e,t],n)=>{e===`id`||e.startsWith(`_`)||(l.push(`#u${n} = :u${n}`),d[`#u${n}`]=e,u[`:u${n}`]=t)}),l.length!==0&&(await s.send(new t.UpdateItemCommand({TableName:a,Key:(0,n.marshall)({_pk:`${e}#${i.id}`,_sk:`${e}#${i.id}`}),UpdateExpression:`SET ${l.join(`, `)}`,ExpressionAttributeNames:d,ExpressionAttributeValues:(0,n.marshall)(u,{removeUndefinedValues:!0})})),c++)}return c}catch(e){throw new r(`Failed to update items: ${e instanceof Error?e.message:String(e)}`,`updateMany`)}},delete:async({model:e,where:i})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`delete`);if(!i||i.length===0)throw new r(`where conditions are required`,`delete`);try{let r=await u(e,i);if(r.length===0)return;await s.send(new t.DeleteItemCommand({TableName:a,Key:(0,n.marshall)({_pk:`${e}#${r[0].id}`,_sk:`${e}#${r[0].id}`})}))}catch(e){throw new r(`Failed to delete item: ${e instanceof Error?e.message:String(e)}`,`delete`)}},deleteMany:async({model:e,where:i})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`deleteMany`);if(!i||i.length===0)throw new r(`where conditions are required`,`deleteMany`);try{let r=await u(e,i);for(let i of r)await s.send(new t.DeleteItemCommand({TableName:a,Key:(0,n.marshall)({_pk:`${e}#${i.id}`,_sk:`${e}#${i.id}`})}));return r.length}catch(e){throw new r(`Failed to delete items: ${e instanceof Error?e.message:String(e)}`,`deleteMany`)}},count:async({model:e,where:t})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`count`);try{return(await u(e,t)).length}catch(e){throw new r(`Failed to count items: ${e instanceof Error?e.message:String(e)}`,`count`)}}})})};module.exports=i;
1
+ let e=require(`better-auth/adapters`),t=require(`@aws-sdk/client-dynamodb`),n=require(`@aws-sdk/util-dynamodb`);var r=class extends Error{constructor(e,t){super(e),this.operation=t,this.name=`DynamoDBAdapterError`}};const i=i=>{if(!i.tableName||i.tableName.trim().length===0)throw new r(`tableName must not be empty`,`initialization`);let{tableName:a,region:o}=i,s=new t.DynamoDBClient({region:o});function c(e){if(!(`expiresAt`in e)||e.expiresAt==null)return e;let t=new Date(e.expiresAt);return isNaN(t.getTime())?e:{...e,expiresAt:Math.floor(t.getTime()/1e3)}}function l(e){if(!(`expiresAt`in e)||e.expiresAt==null)return e;let t=e.expiresAt;return typeof t==`number`?{...e,expiresAt:new Date(t*1e3).toISOString()}:e}function u(e){let{_pk:t,_sk:n,_table:r,...i}=e;return l(i)}function d(e){if(!e||e.length===0)throw new r(`where conditions cannot be empty`,`buildFilter`);let t=[],n={},i={};return e.forEach((e,a)=>{let o=`#w${a}`,s=`:w${a}`;i[o]=e.field;let c;switch(e.operator){case`ne`:n[s]=e.value,c=`${o} <> ${s}`;break;case`gt`:n[s]=e.value,c=`${o} > ${s}`;break;case`gte`:n[s]=e.value,c=`${o} >= ${s}`;break;case`lt`:n[s]=e.value,c=`${o} < ${s}`;break;case`lte`:n[s]=e.value,c=`${o} <= ${s}`;break;case`in`:case`not_in`:{let t=e.value;if(!Array.isArray(t)||t.length===0)throw new r(`${e.operator} operator requires non-empty array`,`buildFilter`);let i=t.map((e,t)=>`:w${a}_${t}`);t.forEach((e,t)=>{n[`:w${a}_${t}`]=e});let s=`${o} IN (${i.join(`,`)})`;c=e.operator===`not_in`?`NOT ${s}`:s;break}case`contains`:n[s]=e.value,c=`contains(${o}, ${s})`;break;case`starts_with`:n[s]=e.value,c=`begins_with(${o}, ${s})`;break;case`ends_with`:n[s]=e.value,c=`contains(${o}, ${s})`;break;default:n[s]=e.value,c=`${o} = ${s}`;break}a>0&&t.push(e.connector===`OR`?`OR`:`AND`),t.push(c)}),{expression:t.join(` `),vals:n,names:i}}async function f(e,i){if(!e||e.trim().length===0)throw new r(`model must not be empty`,`query`);let o={"#_table":`_table`},c={":_table":e},l,f=o,p=c;if(i&&i.length>0){let e=d(i);l=e.expression,f={...o,...e.names},p={...c,...e.vals}}try{return((await s.send(new t.QueryCommand({TableName:a,IndexName:`_table-index`,KeyConditionExpression:`#_table = :_table`,FilterExpression:l,ExpressionAttributeNames:f,ExpressionAttributeValues:(0,n.marshall)(p)}))).Items||[]).map(e=>u((0,n.unmarshall)(e)))}catch(t){throw new r(`Failed to query model "${e}": ${t instanceof Error?t.message:String(t)}`,`query`)}}return(0,e.createAdapterFactory)({config:{adapterId:`dynamodb`,adapterName:`DynamoDB Adapter`,supportsJSON:!1,supportsDates:!1,supportsBooleans:!1,supportsNumericIds:!1},adapter:()=>({create:async({model:e,data:i})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`create`);if(!i||typeof i!=`object`)throw new r(`data must be a valid object`,`create`);try{let r=i.id||crypto.randomUUID(),o={...c({...i,id:r}),_pk:`${e}#${r}`,_sk:`${e}#${r}`,_table:e};return await s.send(new t.PutItemCommand({TableName:a,Item:(0,n.marshall)(o,{removeUndefinedValues:!0})})),{...i,id:r}}catch(e){throw new r(`Failed to create item: ${e instanceof Error?e.message:String(e)}`,`create`)}},findOne:async({model:e,where:i})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`findOne`);if(!i||i.length===0)throw new r(`where conditions are required`,`findOne`);try{if(i.length===1&&i[0].field===`id`&&i[0].operator===`eq`){let r=i[0].value,o=await s.send(new t.GetItemCommand({TableName:a,Key:(0,n.marshall)({_pk:`${e}#${r}`,_sk:`${e}#${r}`})}));return o.Item?u((0,n.unmarshall)(o.Item)):null}return(await f(e,i))[0]||null}catch(e){throw new r(`Failed to find item: ${e instanceof Error?e.message:String(e)}`,`findOne`)}},findMany:async({model:e,where:t,limit:n,sortBy:i,offset:a})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`findMany`);if(n<0)throw new r(`limit must be non-negative`,`findMany`);if(a!==void 0&&a<0)throw new r(`offset must be non-negative`,`findMany`);try{let r=await f(e,t);if(i){let e=i.direction===`desc`?-1:1;r.sort((t,n)=>{let r=t[i.field],a=n[i.field];return r==null||a==null?0:r<a?-1*e:r>a?1*e:0})}return a&&(r=r.slice(a)),n&&(r=r.slice(0,n)),r}catch(e){throw new r(`Failed to find items: ${e instanceof Error?e.message:String(e)}`,`findMany`)}},update:async({model:e,where:i,update:o})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`update`);if(!i||i.length===0)throw new r(`where conditions are required`,`update`);if(!o||typeof o!=`object`)throw new r(`update data must be a valid object`,`update`);try{let r=await f(e,i);if(r.length===0)return null;let l=r[0],u=l.id,d=c(o),p=Object.entries(d),m=[],h={},g={};return p.forEach(([e,t],n)=>{e===`id`||e.startsWith(`_`)||(m.push(`#u${n} = :u${n}`),g[`#u${n}`]=e,h[`:u${n}`]=t)}),m.length===0?l:(await s.send(new t.UpdateItemCommand({TableName:a,Key:(0,n.marshall)({_pk:`${e}#${u}`,_sk:`${e}#${u}`}),UpdateExpression:`SET ${m.join(`, `)}`,ExpressionAttributeNames:g,ExpressionAttributeValues:(0,n.marshall)(h,{removeUndefinedValues:!0})})),{...l,...o})}catch(e){throw new r(`Failed to update item: ${e instanceof Error?e.message:String(e)}`,`update`)}},updateMany:async({model:e,where:i,update:o})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`updateMany`);if(!i||i.length===0)throw new r(`where conditions are required`,`updateMany`);try{let r=await f(e,i),l=0,u=c(o);for(let i of r){let r=Object.entries(u),o=[],c={},d={};r.forEach(([e,t],n)=>{e===`id`||e.startsWith(`_`)||(o.push(`#u${n} = :u${n}`),d[`#u${n}`]=e,c[`:u${n}`]=t)}),o.length!==0&&(await s.send(new t.UpdateItemCommand({TableName:a,Key:(0,n.marshall)({_pk:`${e}#${i.id}`,_sk:`${e}#${i.id}`}),UpdateExpression:`SET ${o.join(`, `)}`,ExpressionAttributeNames:d,ExpressionAttributeValues:(0,n.marshall)(c,{removeUndefinedValues:!0})})),l++)}return l}catch(e){throw new r(`Failed to update items: ${e instanceof Error?e.message:String(e)}`,`updateMany`)}},delete:async({model:e,where:i})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`delete`);if(!i||i.length===0)throw new r(`where conditions are required`,`delete`);try{let r=await f(e,i);if(r.length===0)return;await s.send(new t.DeleteItemCommand({TableName:a,Key:(0,n.marshall)({_pk:`${e}#${r[0].id}`,_sk:`${e}#${r[0].id}`})}))}catch(e){throw new r(`Failed to delete item: ${e instanceof Error?e.message:String(e)}`,`delete`)}},deleteMany:async({model:e,where:i})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`deleteMany`);if(!i||i.length===0)throw new r(`where conditions are required`,`deleteMany`);try{let r=await f(e,i);for(let i of r)await s.send(new t.DeleteItemCommand({TableName:a,Key:(0,n.marshall)({_pk:`${e}#${i.id}`,_sk:`${e}#${i.id}`})}));return r.length}catch(e){throw new r(`Failed to delete items: ${e instanceof Error?e.message:String(e)}`,`deleteMany`)}},count:async({model:e,where:t})=>{if(!e||e.trim().length===0)throw new r(`model must not be empty`,`count`);try{return(await f(e,t)).length}catch(e){throw new r(`Failed to count items: ${e instanceof Error?e.message:String(e)}`,`count`)}}})})};module.exports=i;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createAdapterFactory as e}from"better-auth/adapters";import{DeleteItemCommand as t,DynamoDBClient as n,GetItemCommand as r,PutItemCommand as i,QueryCommand as a,UpdateItemCommand as o}from"@aws-sdk/client-dynamodb";import{marshall as s,unmarshall as c}from"@aws-sdk/util-dynamodb";var l=class extends Error{constructor(e,t){super(e),this.operation=t,this.name=`DynamoDBAdapterError`}};const u=u=>{if(!u.tableName||u.tableName.trim().length===0)throw new l(`tableName must not be empty`,`initialization`);let{tableName:d,region:f}=u,p=new n({region:f});function m(e){let{_pk:t,_sk:n,_table:r,...i}=e;return i}function h(e){if(!e||e.length===0)throw new l(`where conditions cannot be empty`,`buildFilter`);let t=[],n={},r={};return e.forEach((e,i)=>{let a=`#w${i}`,o=`:w${i}`;r[a]=e.field;let s;switch(e.operator){case`ne`:n[o]=e.value,s=`${a} <> ${o}`;break;case`gt`:n[o]=e.value,s=`${a} > ${o}`;break;case`gte`:n[o]=e.value,s=`${a} >= ${o}`;break;case`lt`:n[o]=e.value,s=`${a} < ${o}`;break;case`lte`:n[o]=e.value,s=`${a} <= ${o}`;break;case`in`:case`not_in`:{let t=e.value;if(!Array.isArray(t)||t.length===0)throw new l(`${e.operator} operator requires non-empty array`,`buildFilter`);let r=t.map((e,t)=>`:w${i}_${t}`);t.forEach((e,t)=>{n[`:w${i}_${t}`]=e});let o=`${a} IN (${r.join(`,`)})`;s=e.operator===`not_in`?`NOT ${o}`:o;break}case`contains`:n[o]=e.value,s=`contains(${a}, ${o})`;break;case`starts_with`:n[o]=e.value,s=`begins_with(${a}, ${o})`;break;case`ends_with`:n[o]=e.value,s=`contains(${a}, ${o})`;break;default:n[o]=e.value,s=`${a} = ${o}`;break}i>0&&t.push(e.connector===`OR`?`OR`:`AND`),t.push(s)}),{expression:t.join(` `),vals:n,names:r}}async function g(e,t){if(!e||e.trim().length===0)throw new l(`model must not be empty`,`query`);let n={"#_table":`_table`},r={":_table":e},i,o=n,u=r;if(t&&t.length>0){let e=h(t);i=e.expression,o={...n,...e.names},u={...r,...e.vals}}try{return((await p.send(new a({TableName:d,IndexName:`_table-index`,KeyConditionExpression:`#_table = :_table`,FilterExpression:i,ExpressionAttributeNames:o,ExpressionAttributeValues:s(u)}))).Items||[]).map(e=>m(c(e)))}catch(t){throw new l(`Failed to query model "${e}": ${t instanceof Error?t.message:String(t)}`,`query`)}}return e({config:{adapterId:`dynamodb`,adapterName:`DynamoDB Adapter`,supportsJSON:!1,supportsDates:!1,supportsBooleans:!1,supportsNumericIds:!1},adapter:()=>({create:async({model:e,data:t})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`create`);if(!t||typeof t!=`object`)throw new l(`data must be a valid object`,`create`);try{let n=t.id||crypto.randomUUID(),r={...t,id:n,_pk:`${e}#${n}`,_sk:`${e}#${n}`,_table:e};return await p.send(new i({TableName:d,Item:s(r,{removeUndefinedValues:!0})})),{...t,id:n}}catch(e){throw new l(`Failed to create item: ${e instanceof Error?e.message:String(e)}`,`create`)}},findOne:async({model:e,where:t})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`findOne`);if(!t||t.length===0)throw new l(`where conditions are required`,`findOne`);try{if(t.length===1&&t[0].field===`id`&&t[0].operator===`eq`){let n=t[0].value,i=await p.send(new r({TableName:d,Key:s({_pk:`${e}#${n}`,_sk:`${e}#${n}`})}));return i.Item?m(c(i.Item)):null}return(await g(e,t))[0]||null}catch(e){throw new l(`Failed to find item: ${e instanceof Error?e.message:String(e)}`,`findOne`)}},findMany:async({model:e,where:t,limit:n,sortBy:r,offset:i})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`findMany`);if(n<0)throw new l(`limit must be non-negative`,`findMany`);if(i!==void 0&&i<0)throw new l(`offset must be non-negative`,`findMany`);try{let a=await g(e,t);if(r){let e=r.direction===`desc`?-1:1;a.sort((t,n)=>{let i=t[r.field],a=n[r.field];return i==null||a==null?0:i<a?-1*e:i>a?1*e:0})}return i&&(a=a.slice(i)),n&&(a=a.slice(0,n)),a}catch(e){throw new l(`Failed to find items: ${e instanceof Error?e.message:String(e)}`,`findMany`)}},update:async({model:e,where:t,update:n})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`update`);if(!t||t.length===0)throw new l(`where conditions are required`,`update`);if(!n||typeof n!=`object`)throw new l(`update data must be a valid object`,`update`);try{let r=await g(e,t);if(r.length===0)return null;let i=r[0],a=i.id,c=Object.entries(n),l=[],u={},f={};return c.forEach(([e,t],n)=>{e===`id`||e.startsWith(`_`)||(l.push(`#u${n} = :u${n}`),f[`#u${n}`]=e,u[`:u${n}`]=t)}),l.length===0?i:(await p.send(new o({TableName:d,Key:s({_pk:`${e}#${a}`,_sk:`${e}#${a}`}),UpdateExpression:`SET ${l.join(`, `)}`,ExpressionAttributeNames:f,ExpressionAttributeValues:s(u,{removeUndefinedValues:!0})})),{...i,...n})}catch(e){throw new l(`Failed to update item: ${e instanceof Error?e.message:String(e)}`,`update`)}},updateMany:async({model:e,where:t,update:n})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`updateMany`);if(!t||t.length===0)throw new l(`where conditions are required`,`updateMany`);try{let r=await g(e,t),i=0;for(let t of r){let r=Object.entries(n),a=[],c={},l={};r.forEach(([e,t],n)=>{e===`id`||e.startsWith(`_`)||(a.push(`#u${n} = :u${n}`),l[`#u${n}`]=e,c[`:u${n}`]=t)}),a.length!==0&&(await p.send(new o({TableName:d,Key:s({_pk:`${e}#${t.id}`,_sk:`${e}#${t.id}`}),UpdateExpression:`SET ${a.join(`, `)}`,ExpressionAttributeNames:l,ExpressionAttributeValues:s(c,{removeUndefinedValues:!0})})),i++)}return i}catch(e){throw new l(`Failed to update items: ${e instanceof Error?e.message:String(e)}`,`updateMany`)}},delete:async({model:e,where:n})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`delete`);if(!n||n.length===0)throw new l(`where conditions are required`,`delete`);try{let r=await g(e,n);if(r.length===0)return;await p.send(new t({TableName:d,Key:s({_pk:`${e}#${r[0].id}`,_sk:`${e}#${r[0].id}`})}))}catch(e){throw new l(`Failed to delete item: ${e instanceof Error?e.message:String(e)}`,`delete`)}},deleteMany:async({model:e,where:n})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`deleteMany`);if(!n||n.length===0)throw new l(`where conditions are required`,`deleteMany`);try{let r=await g(e,n);for(let n of r)await p.send(new t({TableName:d,Key:s({_pk:`${e}#${n.id}`,_sk:`${e}#${n.id}`})}));return r.length}catch(e){throw new l(`Failed to delete items: ${e instanceof Error?e.message:String(e)}`,`deleteMany`)}},count:async({model:e,where:t})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`count`);try{return(await g(e,t)).length}catch(e){throw new l(`Failed to count items: ${e instanceof Error?e.message:String(e)}`,`count`)}}})})};export{u as default};
1
+ import{createAdapterFactory as e}from"better-auth/adapters";import{DeleteItemCommand as t,DynamoDBClient as n,GetItemCommand as r,PutItemCommand as i,QueryCommand as a,UpdateItemCommand as o}from"@aws-sdk/client-dynamodb";import{marshall as s,unmarshall as c}from"@aws-sdk/util-dynamodb";var l=class extends Error{constructor(e,t){super(e),this.operation=t,this.name=`DynamoDBAdapterError`}};const u=u=>{if(!u.tableName||u.tableName.trim().length===0)throw new l(`tableName must not be empty`,`initialization`);let{tableName:d,region:f}=u,p=new n({region:f});function m(e){if(!(`expiresAt`in e)||e.expiresAt==null)return e;let t=new Date(e.expiresAt);return isNaN(t.getTime())?e:{...e,expiresAt:Math.floor(t.getTime()/1e3)}}function h(e){if(!(`expiresAt`in e)||e.expiresAt==null)return e;let t=e.expiresAt;return typeof t==`number`?{...e,expiresAt:new Date(t*1e3).toISOString()}:e}function g(e){let{_pk:t,_sk:n,_table:r,...i}=e;return h(i)}function _(e){if(!e||e.length===0)throw new l(`where conditions cannot be empty`,`buildFilter`);let t=[],n={},r={};return e.forEach((e,i)=>{let a=`#w${i}`,o=`:w${i}`;r[a]=e.field;let s;switch(e.operator){case`ne`:n[o]=e.value,s=`${a} <> ${o}`;break;case`gt`:n[o]=e.value,s=`${a} > ${o}`;break;case`gte`:n[o]=e.value,s=`${a} >= ${o}`;break;case`lt`:n[o]=e.value,s=`${a} < ${o}`;break;case`lte`:n[o]=e.value,s=`${a} <= ${o}`;break;case`in`:case`not_in`:{let t=e.value;if(!Array.isArray(t)||t.length===0)throw new l(`${e.operator} operator requires non-empty array`,`buildFilter`);let r=t.map((e,t)=>`:w${i}_${t}`);t.forEach((e,t)=>{n[`:w${i}_${t}`]=e});let o=`${a} IN (${r.join(`,`)})`;s=e.operator===`not_in`?`NOT ${o}`:o;break}case`contains`:n[o]=e.value,s=`contains(${a}, ${o})`;break;case`starts_with`:n[o]=e.value,s=`begins_with(${a}, ${o})`;break;case`ends_with`:n[o]=e.value,s=`contains(${a}, ${o})`;break;default:n[o]=e.value,s=`${a} = ${o}`;break}i>0&&t.push(e.connector===`OR`?`OR`:`AND`),t.push(s)}),{expression:t.join(` `),vals:n,names:r}}async function v(e,t){if(!e||e.trim().length===0)throw new l(`model must not be empty`,`query`);let n={"#_table":`_table`},r={":_table":e},i,o=n,u=r;if(t&&t.length>0){let e=_(t);i=e.expression,o={...n,...e.names},u={...r,...e.vals}}try{return((await p.send(new a({TableName:d,IndexName:`_table-index`,KeyConditionExpression:`#_table = :_table`,FilterExpression:i,ExpressionAttributeNames:o,ExpressionAttributeValues:s(u)}))).Items||[]).map(e=>g(c(e)))}catch(t){throw new l(`Failed to query model "${e}": ${t instanceof Error?t.message:String(t)}`,`query`)}}return e({config:{adapterId:`dynamodb`,adapterName:`DynamoDB Adapter`,supportsJSON:!1,supportsDates:!1,supportsBooleans:!1,supportsNumericIds:!1},adapter:()=>({create:async({model:e,data:t})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`create`);if(!t||typeof t!=`object`)throw new l(`data must be a valid object`,`create`);try{let n=t.id||crypto.randomUUID(),r={...m({...t,id:n}),_pk:`${e}#${n}`,_sk:`${e}#${n}`,_table:e};return await p.send(new i({TableName:d,Item:s(r,{removeUndefinedValues:!0})})),{...t,id:n}}catch(e){throw new l(`Failed to create item: ${e instanceof Error?e.message:String(e)}`,`create`)}},findOne:async({model:e,where:t})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`findOne`);if(!t||t.length===0)throw new l(`where conditions are required`,`findOne`);try{if(t.length===1&&t[0].field===`id`&&t[0].operator===`eq`){let n=t[0].value,i=await p.send(new r({TableName:d,Key:s({_pk:`${e}#${n}`,_sk:`${e}#${n}`})}));return i.Item?g(c(i.Item)):null}return(await v(e,t))[0]||null}catch(e){throw new l(`Failed to find item: ${e instanceof Error?e.message:String(e)}`,`findOne`)}},findMany:async({model:e,where:t,limit:n,sortBy:r,offset:i})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`findMany`);if(n<0)throw new l(`limit must be non-negative`,`findMany`);if(i!==void 0&&i<0)throw new l(`offset must be non-negative`,`findMany`);try{let a=await v(e,t);if(r){let e=r.direction===`desc`?-1:1;a.sort((t,n)=>{let i=t[r.field],a=n[r.field];return i==null||a==null?0:i<a?-1*e:i>a?1*e:0})}return i&&(a=a.slice(i)),n&&(a=a.slice(0,n)),a}catch(e){throw new l(`Failed to find items: ${e instanceof Error?e.message:String(e)}`,`findMany`)}},update:async({model:e,where:t,update:n})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`update`);if(!t||t.length===0)throw new l(`where conditions are required`,`update`);if(!n||typeof n!=`object`)throw new l(`update data must be a valid object`,`update`);try{let r=await v(e,t);if(r.length===0)return null;let i=r[0],a=i.id,c=m(n),l=Object.entries(c),u=[],f={},h={};return l.forEach(([e,t],n)=>{e===`id`||e.startsWith(`_`)||(u.push(`#u${n} = :u${n}`),h[`#u${n}`]=e,f[`:u${n}`]=t)}),u.length===0?i:(await p.send(new o({TableName:d,Key:s({_pk:`${e}#${a}`,_sk:`${e}#${a}`}),UpdateExpression:`SET ${u.join(`, `)}`,ExpressionAttributeNames:h,ExpressionAttributeValues:s(f,{removeUndefinedValues:!0})})),{...i,...n})}catch(e){throw new l(`Failed to update item: ${e instanceof Error?e.message:String(e)}`,`update`)}},updateMany:async({model:e,where:t,update:n})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`updateMany`);if(!t||t.length===0)throw new l(`where conditions are required`,`updateMany`);try{let r=await v(e,t),i=0,a=m(n);for(let t of r){let n=Object.entries(a),r=[],c={},l={};n.forEach(([e,t],n)=>{e===`id`||e.startsWith(`_`)||(r.push(`#u${n} = :u${n}`),l[`#u${n}`]=e,c[`:u${n}`]=t)}),r.length!==0&&(await p.send(new o({TableName:d,Key:s({_pk:`${e}#${t.id}`,_sk:`${e}#${t.id}`}),UpdateExpression:`SET ${r.join(`, `)}`,ExpressionAttributeNames:l,ExpressionAttributeValues:s(c,{removeUndefinedValues:!0})})),i++)}return i}catch(e){throw new l(`Failed to update items: ${e instanceof Error?e.message:String(e)}`,`updateMany`)}},delete:async({model:e,where:n})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`delete`);if(!n||n.length===0)throw new l(`where conditions are required`,`delete`);try{let r=await v(e,n);if(r.length===0)return;await p.send(new t({TableName:d,Key:s({_pk:`${e}#${r[0].id}`,_sk:`${e}#${r[0].id}`})}))}catch(e){throw new l(`Failed to delete item: ${e instanceof Error?e.message:String(e)}`,`delete`)}},deleteMany:async({model:e,where:n})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`deleteMany`);if(!n||n.length===0)throw new l(`where conditions are required`,`deleteMany`);try{let r=await v(e,n);for(let n of r)await p.send(new t({TableName:d,Key:s({_pk:`${e}#${n.id}`,_sk:`${e}#${n.id}`})}));return r.length}catch(e){throw new l(`Failed to delete items: ${e instanceof Error?e.message:String(e)}`,`deleteMany`)}},count:async({model:e,where:t})=>{if(!e||e.trim().length===0)throw new l(`model must not be empty`,`count`);try{return(await v(e,t)).length}catch(e){throw new l(`Failed to count items: ${e instanceof Error?e.message:String(e)}`,`count`)}}})})};export{u as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "betterauth-dynamodb-adapter",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "DynamoDB adapter for Better Auth — single-table design with full CRUD, filtering, and sorting support",
5
5
  "author": "rokku-x",
6
6
  "type": "module",