데이터베이스 어댑터 생성

createAdapter를 사용하여 Better-Auth용 사용자 정의 데이터베이스 어댑터를 만드는 방법을 배웁니다.

우리의 createAdapter 함수는 매우 유연하도록 설계되었으며, 이해하고 사용하기 쉽게 만들기 위해 최선을 다했습니다. 우리의 목표는 데이터베이스 로직 작성에 집중할 수 있도록 하고, 어댑터가 Better-Auth와 어떻게 작동하는지에 대해 걱정할 필요가 없도록 하는 것입니다.

사용자 정의 스키마 구성, 사용자 정의 ID 생성, 안전한 JSON 파싱 등 모든 것이 createAdapter 함수에 의해 처리됩니다. 데이터베이스 로직만 제공하면 createAdapter 함수가 나머지를 처리합니다.

빠른 시작

준비하기

  1. createAdapter를 가져옵니다.
  2. 어댑터 설정 옵션을 나타내는 CustomAdapterConfig 인터페이스를 생성합니다.
  3. 어댑터를 생성합니다!
import { createAdapter, type DBAdapterDebugLogOption } from "better-auth/adapters";

// 사용자 정의 어댑터 설정 옵션
interface CustomAdapterConfig {
  /**
   * 어댑터 문제를 디버그하는 데 도움이 됩니다.
   */
  debugLogs?: DBAdapterDebugLogOption;
  /**
   * 스키마의 테이블 이름이 복수형인 경우.
   */
  usePlural?: boolean;
}

export const myAdapter = (config: CustomAdapterConfig = {}) =>
  createAdapter({
    // ...
  });

어댑터 구성

config 객체는 주로 Better-Auth에 어댑터에 대한 정보를 제공하는 데 사용됩니다. 우리는 어댑터 함수에서 작성해야 하는 코드의 양을 최소화하려고 노력하며, 이러한 config 옵션은 이를 돕는 데 사용됩니다.

// ...
export const myAdapter = (config: CustomAdapterConfig = {}) =>
  createAdapter({
    config: {
      adapterId: "custom-adapter", // 어댑터의 고유 식별자.
      adapterName: "Custom Adapter", // 어댑터의 이름.
      usePlural: config.usePlural ?? false, // 스키마의 테이블 이름이 복수형인지 여부.
      debugLogs: config.debugLogs ?? false, // 디버그 로그를 활성화할지 여부.
      supportsJSON: false, // 데이터베이스가 JSON을 지원하는지 여부. (기본값: false)
      supportsDates: true, // 데이터베이스가 날짜를 지원하는지 여부. (기본값: true)
      supportsBooleans: true, // 데이터베이스가 불린을 지원하는지 여부. (기본값: true)
      supportsNumericIds: true, // 데이터베이스가 자동 증가 숫자 ID를 지원하는지 여부. (기본값: true)
    },
    // ...
  });

어댑터 생성

adapter 함수는 데이터베이스와 상호 작용하는 코드를 작성하는 곳입니다.

// ...
export const myAdapter = (config: CustomAdapterConfig = {}) =>
  createAdapter({
    config: {
      // ...
    },
    adapter: ({}) => {
      return {
        create: async ({ data, model, select }) => {
          // ...
        },
        update: async ({ data, model, select }) => {
          // ...
        },
        updateMany: async ({ data, model, select }) => {
          // ...
        },
        delete: async ({ data, model, select }) => {
          // ...
        },
        // ...
      };
    },
  });

adapter에 대한 자세한 내용은 여기를 참조하세요.

어댑터

adapter 함수는 데이터베이스와 상호 작용하는 코드를 작성하는 곳입니다.

아직 확인하지 않았다면 config 섹션options 객체를 확인하세요. 어댑터에 유용할 수 있습니다.

어댑터 함수에 들어가기 전에 사용할 수 있는 매개변수를 살펴보겠습니다.

  • options: Better Auth 옵션.
  • schema: 사용자의 Better Auth 인스턴스의 스키마.
  • debugLog: 디버그 로그 함수.
  • getField: 필드 가져오기 함수.
  • getDefaultModelName: 기본 모델 이름 가져오기 함수.
  • getDefaultFieldName: 기본 필드 이름 가져오기 함수.
  • getFieldAttributes: 필드 속성 가져오기 함수.
예제
adapter: ({
  options,
  schema,
  debugLog,
  getField,
  getDefaultModelName,
  getDefaultFieldName,
}) => {
  return {
    // ...
  };
};

어댑터 메서드

  • 모든 model 값은 이미 최종 사용자의 스키마 구성을 기반으로 데이터베이스에 대한 올바른 모델 이름으로 변환되었습니다.
    • 이는 주어진 모델의 schema 버전에 액세스해야 하는 경우 이 정확한 model 값을 사용할 수 없음을 의미합니다. 옵션에서 제공되는 getDefaultModelName 함수를 사용하여 modelschema 버전으로 변환해야 합니다.
  • 사용자의 schema 구성을 기반으로 반환하는 누락된 필드를 자동으로 채웁니다.
  • select 매개변수를 포함하는 모든 메서드는 데이터베이스에서 데이터를 더 효율적으로 가져오기 위한 것입니다. select 매개변수가 지정하는 것만 반환하는 것에 대해 걱정할 필요가 없습니다. 우리가 처리합니다.

create 메서드

create 메서드는 데이터베이스에 새 레코드를 만드는 데 사용됩니다.

참고: 사용자가 useNumberId 옵션을 활성화했거나 사용자의 Better Auth 구성에서 generateIdfalse인 경우, iddata 객체에 제공될 것으로 예상됩니다. 그렇지 않으면 id가 자동으로 생성됩니다.

또한 create 메서드에 forceAllowId를 매개변수로 전달할 수 있으며, 이를 통해 data 객체에 id를 제공할 수 있습니다. 우리는 forceAllowId를 내부적으로 처리하므로 걱정할 필요가 없습니다.

매개변수:

  • model: 새 데이터가 삽입될 모델/테이블 이름.
  • data: 데이터베이스에 삽입할 데이터.
  • select: 데이터베이스에서 반환할 필드 배열.

데이터베이스에 삽입된 데이터를 반환해야 합니다.

예제
create: async ({ model, data, select }) => {
  // 데이터베이스에 데이터를 삽입하는 예제.
  return await db.insert(model).values(data);
};

update 메서드

update 메서드는 데이터베이스의 레코드를 업데이트하는 데 사용됩니다.

매개변수:

  • model: 레코드가 업데이트될 모델/테이블 이름.
  • where: 레코드를 업데이트할 where 절.
  • update: 레코드를 업데이트할 데이터.

업데이트된 행의 데이터를 반환해야 합니다. 여기에는 업데이트되지 않은 필드도 포함됩니다.

예제
update: async ({ model, where, update }) => {
  // 데이터베이스의 데이터를 업데이트하는 예제.
  return await db.update(model).set(update).where(where);
};

updateMany 메서드

updateMany 메서드는 데이터베이스의 여러 레코드를 업데이트하는 데 사용됩니다.

매개변수:

  • model: 레코드가 업데이트될 모델/테이블 이름.
  • where: 레코드를 업데이트할 where 절.
  • update: 레코드를 업데이트할 데이터.
업데이트된 레코드 수를 반환해야 합니다.
예제
updateMany: async ({ model, where, update }) => {
  // 데이터베이스의 여러 레코드를 업데이트하는 예제.
  return await db.update(model).set(update).where(where);
};

delete 메서드

delete 메서드는 데이터베이스에서 레코드를 삭제하는 데 사용됩니다.

매개변수:

  • model: 레코드가 삭제될 모델/테이블 이름.
  • where: 레코드를 삭제할 where 절.
예제
delete: async ({ model, where }) => {
  // 데이터베이스에서 레코드를 삭제하는 예제.
  await db.delete(model).where(where);
}

deleteMany 메서드

deleteMany 메서드는 데이터베이스에서 여러 레코드를 삭제하는 데 사용됩니다.

매개변수:

  • model: 레코드가 삭제될 모델/테이블 이름.
  • where: 레코드를 삭제할 where 절.
삭제된 레코드 수를 반환해야 합니다.
예제
deleteMany: async ({ model, where }) => {
  // 데이터베이스에서 여러 레코드를 삭제하는 예제.
  return await db.delete(model).where(where);
};

findOne 메서드

findOne 메서드는 데이터베이스에서 단일 레코드를 찾는 데 사용됩니다.

매개변수:

  • model: 레코드를 찾을 모델/테이블 이름.
  • where: 레코드를 찾을 where 절.
  • select: 반환할 select 절.
데이터베이스에서 찾은 데이터를 반환해야 합니다.
예제
findOne: async ({ model, where, select }) => {
  // 데이터베이스에서 단일 레코드를 찾는 예제.
  return await db.select().from(model).where(where).limit(1);
};

findMany 메서드

findMany 메서드는 데이터베이스에서 여러 레코드를 찾는 데 사용됩니다.

매개변수:

  • model: 레코드를 찾을 모델/테이블 이름.
  • where: 레코드를 찾을 where 절.
  • limit: 반환할 레코드 수 제한.
  • sortBy: 레코드를 정렬할 sortBy 절.
  • offset: 반환할 레코드의 오프셋.

데이터베이스에서 찾은 데이터 배열을 반환해야 합니다.

예제
findMany: async ({ model, where, limit, sortBy, offset }) => {
  // 데이터베이스에서 여러 레코드를 찾는 예제.
  return await db
    .select()
    .from(model)
    .where(where)
    .limit(limit)
    .offset(offset)
    .orderBy(sortBy);
};

count 메서드

count 메서드는 데이터베이스의 레코드 수를 세는 데 사용됩니다.

매개변수:

  • model: 레코드를 셀 모델/테이블 이름.
  • where: 레코드를 셀 where 절.
계산된 레코드 수를 반환해야 합니다.
예제
count: async ({ model, where }) => {
  // 데이터베이스의 레코드 수를 세는 예제.
  return await db.select().from(model).where(where).count();
};

options (선택 사항)

options 객체는 사용자 정의 어댑터 옵션에서 얻은 잠재적인 구성을 위한 것입니다.

예제
const myAdapter = (config: CustomAdapterConfig) =>
  createAdapter({
    config: {
      // ...
    },
    adapter: ({ options }) => {
      return {
        options: config,
      };
    },
  });

createSchema (선택 사항)

createSchema 메서드를 사용하면 Better Auth CLI가 데이터베이스에 대한 스키마를 생성할 수 있습니다.

매개변수:

  • tables: 사용자의 Better-Auth 인스턴스 스키마의 테이블. 스키마 파일로 생성될 것으로 예상됩니다.
  • file: 사용자가 generate 명령에 전달한 파일. 예상 스키마 파일 출력 경로입니다.
예제
createSchema: async ({ file, tables }) => {
  // ... 데이터베이스용 스키마를 생성하는 사용자 정의 로직.
};

어댑터 테스트

어댑터를 테스트하는 데 사용할 수 있는 테스트 스위트를 제공했습니다. vitest를 사용해야 합니다.

my-adapter.test.ts
import { expect, test, describe } from "vitest";
import { runAdapterTest } from "better-auth/adapters/test";
import { myAdapter } from "./my-adapter";

describe("My Adapter Tests", async () => {
  afterAll(async () => {
    // 여기에서 DB 정리를 실행하세요...
  });
  const adapter = myAdapter({
    debugLogs: {
      // 어댑터 구성이 디버그 로그 전달을 허용하는 경우 여기에 전달하세요.
      isRunningAdapterTests: true, // 이것은 테스트가 실패할 때만 디버그 로그를 기록하도록 알려주는 우리의 최고 비밀 플래그입니다.
    },
  });

  runAdapterTest({
    getAdapter: async (betterAuthOptions = {}) => {
      return adapter(betterAuthOptions);
    },
  });
});

숫자 ID 테스트

데이터베이스가 숫자 ID를 지원하는 경우 이 테스트도 실행해야 합니다:

my-adapter.number-id.test.ts
import { expect, test, describe } from "vitest";
import { runNumberIdAdapterTest } from "better-auth/adapters/test";
import { myAdapter } from "./my-adapter";

describe("My Adapter Numeric ID Tests", async () => {
  afterAll(async () => {
    // 여기에서 DB 정리를 실행하세요...
  });
  const adapter = myAdapter({
    debugLogs: {
      // 어댑터 구성이 디버그 로그 전달을 허용하는 경우 여기에 전달하세요.
      isRunningAdapterTests: true, // 이것은 테스트가 실패할 때만 디버그 로그를 기록하도록 알려주는 우리의 최고 비밀 플래그입니다.
    },
  });

  runNumberIdAdapterTest({
    getAdapter: async (betterAuthOptions = {}) => {
      return adapter(betterAuthOptions);
    },
  });
});

Config

config 객체는 Better-Auth에 어댑터에 대한 정보를 제공하는 데 사용됩니다.

어댑터를 올바르게 구성하는 방법을 이해하는 데 도움이 되므로 아래 제공되는 각 옵션을 읽어보는 것을 강력히 권장합니다.

필수 구성

adapterId

어댑터의 고유 식별자.

adapterName

어댑터의 이름.

선택적 구성

supportsNumericIds

데이터베이스가 숫자 ID를 지원하는지 여부. 이것이 false로 설정되고 사용자의 구성에서 useNumberId가 활성화된 경우 오류가 발생합니다.

supportsJSON

데이터베이스가 JSON을 지원하는지 여부. 데이터베이스가 JSON을 지원하지 않으면 string을 사용하여 JSON 데이터를 저장합니다. 데이터를 검색할 때 string을 안전하게 JSON 객체로 다시 파싱합니다.

supportsDates

데이터베이스가 날짜를 지원하는지 여부. 데이터베이스가 날짜를 지원하지 않으면 string을 사용하여 날짜를 저장합니다. (ISO 문자열) 데이터를 검색할 때 string을 안전하게 Date 객체로 다시 파싱합니다.

supportsBooleans

데이터베이스가 불린을 지원하는지 여부. 데이터베이스가 불린을 지원하지 않으면 0 또는 1을 사용하여 불린 값을 저장합니다. 데이터를 검색할 때 0 또는 1을 안전하게 불린 값으로 다시 파싱합니다.

usePlural

스키마의 테이블 이름이 복수형인지 여부. 이것은 종종 사용자가 정의하고 사용자 정의 어댑터 옵션을 통해 전달됩니다. 사용자가 테이블 이름을 사용자 정의할 수 있도록 할 의도가 없다면 이 옵션을 무시하거나 false로 설정할 수 있습니다.

예제
const adapter = myAdapter({
  // 이 값은 createAdapter `config` 객체의 `usePlural`
  // 옵션으로 전달됩니다.
  usePlural: true,
});

transaction

어댑터가 트랜잭션을 지원하는지 여부. false이면 작업이 순차적으로 실행됩니다. 그렇지 않으면 TransactionAdapter로 콜백을 실행하는 함수를 제공하세요.

데이터베이스가 트랜잭션을 지원하지 않으면 오류 처리 및 롤백이 그다지 강력하지 않습니다. 더 나은 데이터 무결성을 위해 트랜잭션을 지원하는 데이터베이스를 사용하는 것이 좋습니다.

debugLogs

어댑터에 대한 디버그 로그를 활성화하는 데 사용됩니다. 불린을 전달하거나 다음 키가 있는 객체를 전달할 수 있습니다: create, update, updateMany, findOne, findMany, delete, deleteMany, count. 키 중 하나가 true이면 해당 메서드에 대한 디버그 로그가 활성화됩니다.

예제
// 모든 메서드에 대한 디버그 로그를 기록합니다.
const adapter = myAdapter({
  debugLogs: true,
});
예제
// `create` 및 `update` 메서드에 대한 디버그 로그만 기록합니다.
const adapter = myAdapter({
  debugLogs: {
    create: true,
    update: true,
  },
});

disableIdGeneration

ID 생성을 비활성화할지 여부. 이것이 true로 설정되면 사용자의 generateId 옵션이 무시됩니다.

customIdGenerator

데이터베이스가 특정 사용자 정의 ID 생성만 지원하는 경우 이 옵션을 사용하여 자체 ID를 생성할 수 있습니다.

mapKeysTransformInput

데이터베이스가 특정 상황에 대해 다른 키 이름을 사용하는 경우 이 옵션을 사용하여 키를 매핑할 수 있습니다. 이것은 특정 상황에 대해 다른 키 이름을 예상하는 데이터베이스에 유용합니다. 예를 들어 MongoDB는 _id를 사용하지만 Better-Auth에서는 id를 사용합니다.

반환된 객체의 각 키는 교체할 이전 키를 나타냅니다. 값은 새 키를 나타냅니다.

일부 키만 변환하는 부분 객체일 수 있습니다.

예제
mapKeysTransformInput: () => {
  return {
    id: "_id", // MongoDB에 저장하기 위해 `id`를 `_id`로 교체하려고 합니다
  };
},

mapKeysTransformOutput

데이터베이스가 특정 상황에 대해 다른 키 이름을 사용하는 경우 이 옵션을 사용하여 키를 매핑할 수 있습니다. 이것은 특정 상황에 대해 다른 키 이름을 사용하는 데이터베이스에 유용합니다. 예를 들어 MongoDB는 _id를 사용하지만 Better-Auth에서는 id를 사용합니다.

반환된 객체의 각 키는 교체할 이전 키를 나타냅니다. 값은 새 키를 나타냅니다.

일부 키만 변환하는 부분 객체일 수 있습니다.

예제
mapKeysTransformOutput: () => {
  return {
    _id: "id", // (MongoDB에서) `_id`를 (Better-Auth용) `id`로 교체하려고 합니다
  };
},

customTransformInput

데이터베이스에 저장되기 전에 입력 데이터를 변환해야 하는 경우 이 옵션을 사용하여 데이터를 변환할 수 있습니다.

supportsJSON, supportsDates 또는 supportsBooleans를 사용하는 경우 customTransformInput 함수가 호출되기 전에 변환이 적용됩니다.

customTransformInput 함수는 다음 인수를 받습니다:

  • data: 변환할 데이터.
  • field: 변환되는 필드.
  • fieldAttributes: 변환되는 필드의 필드 속성.
  • select: 쿼리가 반환할 것으로 예상하는 select 값.
  • model: 변환되는 모델.
  • schema: 변환되는 스키마.
  • options: Better Auth 옵션.

customTransformInput 함수는 주어진 작업의 데이터 객체의 모든 키에서 실행됩니다.

예제
customTransformInput: ({ field, data }) => {
  if (field === "id") {
    return "123"; // ID를 "123"으로 강제합니다
  }

  return data;
};

customTransformOutput

사용자에게 반환되기 전에 출력 데이터를 변환해야 하는 경우 이 옵션을 사용하여 데이터를 변환할 수 있습니다. customTransformOutput 함수는 출력 데이터를 변환하는 데 사용됩니다. customTransformInput 함수와 유사하게 주어진 작업의 데이터 객체의 모든 키에서 실행되지만 데이터베이스에서 데이터를 검색한 후에 실행됩니다.

예제
customTransformOutput: ({ field, data }) => {
  if (field === "name") {
    return "Bob"; // 이름을 "Bob"으로 강제합니다
  }

  return data;
};
const some_data = await adapter.create({
  model: "user",
  data: {
    name: "John",
  },
});

// 이름은 "Bob"이 됩니다
console.log(some_data.name);