diff --git a/.auri/$t9fgucw6.md b/.auri/$t9fgucw6.md new file mode 100644 index 000000000..12e96aa34 --- /dev/null +++ b/.auri/$t9fgucw6.md @@ -0,0 +1,6 @@ +--- +package: "@lucia-auth/adapter-sqlite" # package name +type: "minor" # "major", "minor", "patch" +--- + +Add libSQL adapter \ No newline at end of file diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/astro.md b/documentation-v2/content/main/0.start-here/0.getting-started/astro.md index fb1d8a004..7f1966c91 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/astro.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/astro.md @@ -53,6 +53,7 @@ const auth = lucia({ ### Adapters for database drivers and ORMs - [`better-sqlite3`](/database-adapters/better-sqlite3): SQLite +- [libSQL](/database-adapters/libSQL): libSQL (Turso) - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/express.md b/documentation-v2/content/main/0.start-here/0.getting-started/express.md index e86660f54..256cba32d 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/express.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/express.md @@ -64,6 +64,7 @@ const auth = lucia({ ### Adapters for database drivers and ORMs - [`better-sqlite3`](/database-adapters/better-sqlite3): SQLite +- [libSQL](/database-adapters/libSQL): libSQL (Turso) - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/index.md b/documentation-v2/content/main/0.start-here/0.getting-started/index.md index 667c4fd38..e8a793ced 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/index.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/index.md @@ -83,6 +83,7 @@ const auth = lucia({ ### Adapters for database drivers and ORMs - [`better-sqlite3`](/database-adapters/better-sqlite3): SQLite +- [libSQL](/database-adapters/libSQL): libSQL (Turso) - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-app.md b/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-app.md index cf5d73147..8adc419d2 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-app.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-app.md @@ -59,6 +59,7 @@ const auth = lucia({ ### Adapters for database drivers and ORMs - [`better-sqlite3`](/database-adapters/better-sqlite3): SQLite +- [libSQL](/database-adapters/libSQL): libSQL (Turso) - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-pages.md b/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-pages.md index 4b2c95179..5042bee70 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-pages.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-pages.md @@ -59,6 +59,7 @@ const auth = lucia({ ### Adapters for database drivers and ORMs - [`better-sqlite3`](/database-adapters/better-sqlite3): SQLite +- [libSQL](/database-adapters/libSQL): libSQL (Turso) - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/nuxt.md b/documentation-v2/content/main/0.start-here/0.getting-started/nuxt.md index 26afa433a..fea60d2ea 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/nuxt.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/nuxt.md @@ -54,6 +54,7 @@ const auth = lucia({ ### Adapters for database drivers and ORMs - [`better-sqlite3`](/database-adapters/better-sqlite3): SQLite +- [libSQL](/database-adapters/libSQL): libSQL (Turso) - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/remix.md b/documentation-v2/content/main/0.start-here/0.getting-started/remix.md index 30134c572..dd2f11801 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/remix.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/remix.md @@ -75,6 +75,7 @@ const auth = lucia({ ### Adapters for database drivers and ORMs - [`better-sqlite3`](/database-adapters/better-sqlite3): SQLite +- [libSQL](/database-adapters/libSQL): libSQL (Turso) - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/sveltekit.md b/documentation-v2/content/main/0.start-here/0.getting-started/sveltekit.md index 211095af7..ccba55e53 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/sveltekit.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/sveltekit.md @@ -56,6 +56,7 @@ const auth = lucia({ ### Adapters for database drivers and ORMs - [`better-sqlite3`](/database-adapters/better-sqlite3): SQLite +- [libSQL](/database-adapters/libSQL): libSQL (Turso) - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL diff --git a/documentation-v2/content/main/1.basics/0.database.md b/documentation-v2/content/main/1.basics/0.database.md index f453805b3..dd1f6dcb8 100644 --- a/documentation-v2/content/main/1.basics/0.database.md +++ b/documentation-v2/content/main/1.basics/0.database.md @@ -13,6 +13,7 @@ There are 2 types of adapters provided by Lucia: Regular adapters, and session a We currently provide the following adapters: - [`better-sqlite3`](/database-adapters/better-sqlite3) +- [libSQL](/database-adapters/libSQL): libSQL (Turso) - [Cloudflare D1](/database-adapters/cloudflare-d1) - [Mongoose](/database-adapters/mongoose) - [`mysql2`](/database-adapters/mysql2) diff --git a/documentation-v2/content/main/2.database-adapters/libsql.md b/documentation-v2/content/main/2.database-adapters/libsql.md new file mode 100644 index 000000000..c7ac2107c --- /dev/null +++ b/documentation-v2/content/main/2.database-adapters/libsql.md @@ -0,0 +1,103 @@ +--- +menuTitle: "libSQL" +title: "libSQL adapter" +description: "Learn how to use libSQL with Lucia" +--- + +Adapter for [libSQL](https://github.com/libsql/libsql) provided by the SQLite adapter package. + +```ts +import { libSQL } from "@lucia-auth/adapter-sqlite"; +``` + +```ts +const libSQL: ( + client: Client, + tableNames: { + user: string; + key: string; + session: string; + } +) => InitializeAdapter; +``` + +##### Parameters + +Table names are automatically escaped. + +| name | type | description | +| -------------------- | -------- | ------------------ | +| `client` | `Client` | Database client | +| `tableNames.user` | `string` | User table name | +| `tableNames.key` | `string` | Key table name | +| `tableNames.session` | `string` | Session table name | + +## Installation + +``` +npm i @lucia-auth/adapter-sqlite +pnpm add @lucia-auth/adapter-sqlite +yarn add @lucia-auth/adapter-sqlite +``` + +## Usage + +```ts +import { lucia } from "lucia"; +import { libsql } from "@lucia-auth/adapter-sqlite"; +import { createClient } from "@libsql/client"; + +const db = createClient({ + url: "file:test/main.db" +}); + +const auth = lucia({ + adapter: libsql(db, { + user: "user", + key: "user_key", + session: "user_session" + }) + // ... +}); +``` + +## libSQL schema + +You can choose any table names, just make sure to define them in the adapter argument. + +### User table + +You can add additional columns to store user attributes. + +```sql +CREATE TABLE user ( + id VARCHAR(31) NOT NULL PRIMARY KEY +); +``` + +### Key table + +Make sure to update the foreign key statement if you change the user table name. + +```sql +CREATE TABLE user_key ( + id VARCHAR(255) NOT NULL PRIMARY KEY, + user_id VARCHAR(15) NOT NULL, + hashed_password VARCHAR(255), + FOREIGN KEY (user_id) REFERENCES user(id) +); +``` + +### Session table + +You can add additional columns to store session attributes. Make sure to update the foreign key statement if you change the user table name. + +```sql +CREATE TABLE user_session ( + id VARCHAR(127) NOT NULL PRIMARY KEY, + user_id VARCHAR(15) NOT NULL, + active_expires BIGINT NOT NULL, + idle_expires BIGINT NOT NULL, + FOREIGN KEY (user_id) REFERENCES user(id) +); +``` diff --git a/packages/adapter-sqlite/README.md b/packages/adapter-sqlite/README.md index 9d2586cc6..813fa2d51 100644 --- a/packages/adapter-sqlite/README.md +++ b/packages/adapter-sqlite/README.md @@ -11,6 +11,7 @@ SQLite adapter for Lucia ## Supported drivers - [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3) +- [libSQL](https://github.com/libsql/libsql) (Turso) ## Installation @@ -57,3 +58,10 @@ Finally, run: ``` pnpm test.d1 ``` + + +### libSQL + +``` +pnpm test.libsql +``` \ No newline at end of file diff --git a/packages/adapter-sqlite/package.json b/packages/adapter-sqlite/package.json index 49906e540..b119b71e6 100644 --- a/packages/adapter-sqlite/package.json +++ b/packages/adapter-sqlite/package.json @@ -14,10 +14,10 @@ "build": "shx rm -rf ./dist/* && tsc", "auri.build": "pnpm build", "test.better-sqlite3": "tsx test/better-sqlite3/index.ts", - "test.d1": "tsx test/d1/index.ts" + "test.d1": "tsx test/d1/index.ts", + "test.libsql": "tsx test/libsql/index.ts" }, "keywords": [ - "lucia", "lucia", "auth", "better-sqlite3", @@ -40,15 +40,20 @@ }, "peerDependencies": { "better-sqlite3": "^8.0.0", + "@libsql/client": "^0.2.1", "lucia": "2.0.0-beta.4" }, "peerDependenciesMeta": { "better-sqlite3": { "optional": true + }, + "@libsql/client": { + "optional": true } }, "devDependencies": { "@cloudflare/workers-types": "^4.20230518.0", + "@libsql/client": "^0.2.1", "@lucia-auth/adapter-test": "latest", "@miniflare/d1": "^2.14.0", "@types/better-sqlite3": "^7.6.3", diff --git a/packages/adapter-sqlite/src/drivers/libsql.ts b/packages/adapter-sqlite/src/drivers/libsql.ts new file mode 100644 index 000000000..44f399d40 --- /dev/null +++ b/packages/adapter-sqlite/src/drivers/libsql.ts @@ -0,0 +1,194 @@ +import { helper, getSetArgs, escapeName } from "../utils.js"; + +import type { + SessionSchema, + Adapter, + InitializeAdapter, + UserSchema, + KeySchema +} from "lucia"; +import { Client, LibsqlError } from "@libsql/client"; + +export const libsqlAdapter = ( + db: Client, + tables: { + user: string; + session: string; + key: string; + } +): InitializeAdapter => { + const ESCAPED_USER_TABLE_NAME = escapeName(tables.user); + const ESCAPED_SESSION_TABLE_NAME = escapeName(tables.session); + const ESCAPED_KEY_TABLE_NAME = escapeName(tables.key); + + return (LuciaError) => { + return { + getUser: async (userId) => { + const result = await db.execute({ + sql: `SELECT * FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = ?`, + args: [userId] + }); + const rows = result.rows as unknown[] as UserSchema[]; + return rows.at(0) ?? null; + }, + setUser: async (user, key) => { + const [userFields, userValues, userArgs] = helper(user); + const insertUserQuery = { + sql: `INSERT INTO ${ESCAPED_USER_TABLE_NAME} ( ${userFields} ) VALUES ( ${userValues} )`, + args: userArgs + }; + if (!key) { + await db.execute(insertUserQuery); + return; + } + try { + const [keyFields, keyValues, keyArgs] = helper(key); + const insertKeyQuery = { + sql: `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${keyFields} ) VALUES ( ${keyValues} )`, + args: keyArgs + }; + await db.batch("write", [insertUserQuery, insertKeyQuery]); + } catch (e) { + if ( + e instanceof LibsqlError && + e.code === "SQLITE_CONSTRAINT_PRIMARYKEY" && + e.message?.includes(".id") + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteUser: async (userId) => { + await db.execute({ + sql: `DELETE FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = ?`, + args: [userId] + }); + }, + updateUser: async (userId, partialUser) => { + const [fields, values, args] = helper(partialUser); + args.push(userId); + await db.execute({ + sql: `UPDATE ${ESCAPED_USER_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?`, + args + }); + }, + + getSession: async (sessionId) => { + const result = await db.execute({ + sql: `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`, + args: [sessionId] + }); + const rows = result.rows as unknown[] as SessionSchema[]; + return rows.at(0) ?? null; + }, + getSessionsByUserId: async (userId) => { + const result = await db.execute({ + sql: `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = ?`, + args: [userId] + }); + return result.rows as unknown[] as SessionSchema[]; + }, + setSession: async (session) => { + try { + const [fields, values, args] = helper(session); + await db.execute({ + sql: `INSERT INTO ${ESCAPED_SESSION_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )`, + args + }); + } catch (e) { + if ( + e instanceof LibsqlError && + e.code === "SQLITE_CONSTRAINT_FOREIGNKEY" + ) { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + throw e; + } + }, + deleteSession: async (sessionId) => { + await db.execute({ + sql: `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`, + args: [sessionId] + }); + }, + deleteSessionsByUserId: async (userId) => { + await db.execute({ + sql: `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = ?`, + args: [userId] + }); + }, + updateSession: async (sessionId, partialSession) => { + const [fields, values, args] = helper(partialSession); + const setArgs = getSetArgs(fields, values); + args.push(sessionId); + await db.execute({ + sql: `UPDATE ${ESCAPED_SESSION_TABLE_NAME} SET ${setArgs} WHERE id = ?`, + args + }); + }, + + getKey: async (keyId) => { + const result = await db.execute({ + sql: `SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = ?`, + args: [keyId] + }); + const rows = result.rows as unknown[] as KeySchema[]; + return rows.at(0) ?? null; + }, + getKeysByUserId: async (userId) => { + const result = await db.execute({ + sql: `SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = ?`, + args: [userId] + }); + return result.rows as unknown[] as KeySchema[]; + }, + setKey: async (key) => { + try { + const [fields, values, args] = helper(key); + await db.execute({ + sql: `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )`, + args + }); + } catch (e) { + if (e instanceof LibsqlError) { + if (e.code === "SQLITE_CONSTRAINT_FOREIGNKEY") { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + if ( + e.code === "SQLITE_CONSTRAINT_PRIMARYKEY" && + e.message?.includes(".id") + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + } + throw e; + } + }, + deleteKey: async (keyId) => { + await db.execute({ + sql: `DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = ?`, + args: [keyId] + }); + }, + deleteKeysByUserId: async (userId) => { + await db.execute({ + sql: `DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = ?`, + args: [userId] + }); + }, + updateKey: async (keyId, partialKey) => { + const [fields, values, args] = helper(partialKey); + const setArgs = getSetArgs(fields, values); + args.push(keyId); + await db.execute({ + sql: `UPDATE ${ESCAPED_KEY_TABLE_NAME} SET ${setArgs} WHERE id = ?`, + args + }); + } + }; + }; +}; diff --git a/packages/adapter-sqlite/src/index.ts b/packages/adapter-sqlite/src/index.ts index 21a65d4de..61072ecf4 100644 --- a/packages/adapter-sqlite/src/index.ts +++ b/packages/adapter-sqlite/src/index.ts @@ -1,2 +1,3 @@ export { betterSqlite3Adapter as betterSqlite3 } from "./drivers/better-sqlite3.js"; export { d1Adapter as d1 } from "./drivers/d1.js"; +export { libsqlAdapter as libsql } from "./drivers/libsql.js"; diff --git a/packages/adapter-sqlite/test/libsql/index.ts b/packages/adapter-sqlite/test/libsql/index.ts new file mode 100644 index 000000000..b47123846 --- /dev/null +++ b/packages/adapter-sqlite/test/libsql/index.ts @@ -0,0 +1,43 @@ +import { testAdapter, Database } from "@lucia-auth/adapter-test"; +import { LuciaError } from "lucia"; +import { createClient } from "@libsql/client"; + +import { TABLE_NAMES } from "../db.js"; +import { libsqlAdapter } from "../../src/drivers/libsql.js"; +import { escapeName, helper } from "../../src/utils.js"; + +import type { QueryHandler, TableQueryHandler } from "@lucia-auth/adapter-test"; + +const db = createClient({ + url: "file:test/main.db" +}); + +const createTableQueryHandler = (tableName: string): TableQueryHandler => { + const ESCAPED_TABLE_NAME = escapeName(tableName); + return { + get: async () => { + const { rows } = await db.execute(`SELECT * FROM ${ESCAPED_TABLE_NAME}`); + return rows; + }, + insert: async (value: any) => { + const [fields, placeholders, args] = helper(value); + await db.execute({ + sql: `INSERT INTO ${ESCAPED_TABLE_NAME} ( ${fields} ) VALUES ( ${placeholders} )`, + args + }); + }, + clear: async () => { + await db.execute(`DELETE FROM ${ESCAPED_TABLE_NAME}`); + } + }; +}; + +const queryHandler: QueryHandler = { + user: createTableQueryHandler(TABLE_NAMES.user), + session: createTableQueryHandler(TABLE_NAMES.session), + key: createTableQueryHandler(TABLE_NAMES.key) +}; + +const adapter = libsqlAdapter(db, TABLE_NAMES)(LuciaError); + +testAdapter(adapter, new Database(queryHandler)); diff --git a/packages/adapter-sqlite/test/main.db b/packages/adapter-sqlite/test/main.db index 4e504bb6a..1fe474821 100644 Binary files a/packages/adapter-sqlite/test/main.db and b/packages/adapter-sqlite/test/main.db differ