-
Notifications
You must be signed in to change notification settings - Fork 111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature: add sqlite storage #21
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@openauthjs/openauth": patch | ||
--- | ||
|
||
add sqlite storage adapter |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
/node_modules | ||
node_modules | ||
.sst | ||
.env | ||
dist | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
storage.db |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { authorizer } from "@openauthjs/openauth"; | ||
import { SQLiteStorage } from "@openauthjs/openauth/storage/sqlite"; | ||
import { PasswordAdapter } from "@openauthjs/openauth/adapter/password"; | ||
import { PasswordUI } from "@openauthjs/openauth/ui/password"; | ||
import { subjects } from "../../subjects.js"; | ||
|
||
const storage = SQLiteStorage({ persist: "storage.db" }); | ||
|
||
export default authorizer({ | ||
subjects, | ||
storage, | ||
providers: { | ||
password: PasswordAdapter( | ||
PasswordUI({ | ||
sendCode: async (email, code) => { | ||
console.log(email, code); | ||
}, | ||
}) | ||
), | ||
}, | ||
success: async (ctx, value) => { | ||
if (value.provider === "password") { | ||
return ctx.subject("user", { | ||
email: value.email, | ||
}); | ||
} | ||
throw new Error("Invalid provider"); | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,59 @@ | ||||||||
import { joinKey, splitKey, type StorageAdapter } from "./storage.js"; | ||||||||
import Database from "libsql"; | ||||||||
|
||||||||
export interface SqliteStorageOptions { | ||||||||
persist?: string; | ||||||||
tableName?: string; | ||||||||
} | ||||||||
|
||||||||
export function SQLiteStorage(input?: SqliteStorageOptions): StorageAdapter { | ||||||||
// initialize sqlite database and create the necessary table structure | ||||||||
const db = new Database(input?.persist ?? ":memory:"); | ||||||||
const TABLE_NAME = input?.tableName ?? "__openauth__kv_storage"; | ||||||||
|
||||||||
db.exec( | ||||||||
`CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (key TEXT PRIMARY KEY, value TEXT, expiry INTEGER)` | ||||||||
); | ||||||||
|
||||||||
return { | ||||||||
async get(key: string[]) { | ||||||||
const joined = joinKey(key); | ||||||||
|
||||||||
const row = db | ||||||||
.prepare(`SELECT value, expiry FROM ${TABLE_NAME} WHERE key = ?`) | ||||||||
.get(joined) as { value: string; expiry: number } | undefined; | ||||||||
|
||||||||
if (row && row.expiry && row.expiry < Date.now()) { | ||||||||
db.prepare(`DELETE FROM ${TABLE_NAME} WHERE key = ?`).run(joined); | ||||||||
return undefined; | ||||||||
} | ||||||||
return row ? JSON.parse(row.value) : undefined; | ||||||||
}, | ||||||||
async set(key: string[], value: any, ttl?: number) { | ||||||||
const expiry = ttl ? Date.now() + ttl * 1000 : undefined; | ||||||||
Comment on lines
+32
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is already set in https://github.com/openauthjs/openauth/blob/master/packages/openauth/src/storage/storage.ts#L33
Suggested change
|
||||||||
const joined = joinKey(key); | ||||||||
db.prepare( | ||||||||
`INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, expiry) VALUES (?, ?, ?)` | ||||||||
).run(joined, JSON.stringify(value), expiry); | ||||||||
}, | ||||||||
async remove(key: string[]) { | ||||||||
const joined = joinKey(key); | ||||||||
db.prepare(`DELETE FROM ${TABLE_NAME} WHERE key = ?`).run(joined); | ||||||||
}, | ||||||||
async *scan(prefix: string[]) { | ||||||||
const joined = joinKey(prefix); | ||||||||
const rows = db | ||||||||
.prepare( | ||||||||
`SELECT key, value, expiry FROM ${TABLE_NAME} WHERE key LIKE ?` | ||||||||
) | ||||||||
.all(joined + "%") as { key: string; value: string; expiry: number }[]; | ||||||||
|
||||||||
for (const row of rows) { | ||||||||
if (row.expiry && row.expiry < Date.now()) { | ||||||||
continue; | ||||||||
} | ||||||||
yield [splitKey(row.key), JSON.parse(row.value)]; | ||||||||
} | ||||||||
}, | ||||||||
}; | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { beforeEach, describe, expect, test } from "bun:test"; | ||
import { SQLiteStorage } from "../src/storage/sqlite"; | ||
|
||
let storage = SQLiteStorage(); | ||
|
||
beforeEach(async () => { | ||
storage = SQLiteStorage(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is pretty much the same as memory storage test |
||
}); | ||
|
||
describe("set", () => { | ||
test("basic", async () => { | ||
await storage.set(["users", "123"], { name: "Test User" }); | ||
const result = await storage.get(["users", "123"]); | ||
expect(result).toEqual({ name: "Test User" }); | ||
}); | ||
|
||
test("ttl", async () => { | ||
await storage.set(["temp", "key"], { value: "value" }, 0.1); // 100ms TTL | ||
let result = await storage.get(["temp", "key"]); | ||
expect(result?.value).toBe("value"); | ||
|
||
await new Promise((resolve) => setTimeout(resolve, 150)); | ||
result = await storage.get(["temp", "key"]); | ||
expect(result).toBeUndefined(); | ||
}); | ||
|
||
test("nested", async () => { | ||
const complexObj = { | ||
id: 1, | ||
nested: { a: 1, b: { c: 2 } }, | ||
array: [1, 2, 3], | ||
}; | ||
await storage.set(["complex"], complexObj); | ||
const result = await storage.get(["complex"]); | ||
expect(result).toEqual(complexObj); | ||
}); | ||
}); | ||
|
||
describe("get", () => { | ||
test("missing", async () => { | ||
const result = await storage.get(["nonexistent"]); | ||
expect(result).toBeUndefined(); | ||
}); | ||
|
||
test("key", async () => { | ||
await storage.set(["a", "b", "c"], { value: "nested" }); | ||
const result = await storage.get(["a", "b", "c"]); | ||
expect(result?.value).toBe("nested"); | ||
}); | ||
}); | ||
|
||
describe("remove", () => { | ||
test("existing", async () => { | ||
await storage.set(["test"], "value"); | ||
await storage.remove(["test"]); | ||
const result = await storage.get(["test"]); | ||
expect(result).toBeUndefined(); | ||
}); | ||
|
||
test("missing", async () => { | ||
expect(storage.remove(["nonexistent"])).resolves.toBeUndefined(); | ||
}); | ||
}); | ||
|
||
describe("scan", () => { | ||
test("all", async () => { | ||
await storage.set(["users", "1"], { id: 1 }); | ||
await storage.set(["users", "2"], { id: 2 }); | ||
await storage.set(["other"], { id: 3 }); | ||
const results = await Array.fromAsync(storage.scan(["users"])); | ||
expect(results).toHaveLength(2); | ||
expect(results).toContainEqual([["users", "1"], { id: 1 }]); | ||
expect(results).toContainEqual([["users", "2"], { id: 2 }]); | ||
}); | ||
|
||
test("ttl", async () => { | ||
await storage.set(["temp", "1"], "a", 0.1); | ||
await storage.set(["temp", "2"], "b", 0.1); | ||
await storage.set(["temp", "3"], "c"); | ||
expect(await Array.fromAsync(storage.scan(["temp"]))).toHaveLength(3); | ||
await new Promise((resolve) => setTimeout(resolve, 150)); | ||
expect(await Array.fromAsync(storage.scan(["temp"]))).toHaveLength(1); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expiry is a separate column instead of being part of the value, as it's relatively easier to query.