Last active
May 30, 2025 05:32
-
-
Save simonbengtsson/8308f494805e3c9e5dd7ab65ec6b022b to your computer and use it in GitHub Desktop.
D1 wrapper with kv+ api
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type Document = z.infer<typeof Document> | |
export const Document = z.object({ | |
id: z.string(), | |
}) | |
export type User = z.infer<typeof User> | |
export const User = Document.extend({ | |
name: z.string(), | |
email: z.email(), | |
}) | |
export const getUsersStore = function (db: D1Database) { | |
return getCollection<User>(db, { | |
schema: User, | |
tableName: "users", | |
}) | |
} | |
export async function getCollection<T extends Document>( | |
db: D1Database, | |
options: { | |
schema: z.ZodSchema<T> | |
tableName: string | |
}, | |
) { | |
const sql = /*sql*/ `CREATE TABLE IF NOT EXISTS ${options.tableName} (key TEXT PRIMARY KEY, value JSON NOT NULL);` | |
await db.exec(sql) | |
async function get(key: string): Promise<T | null> { | |
const result = await db | |
.prepare(/*sql*/ `SELECT value FROM ${options.tableName} WHERE key = ?`) | |
.bind(key) | |
.first<{ value: string }>() | |
const raw = result?.value | |
if (!raw) return null | |
return parseDocument(raw) | |
} | |
async function set(document: T) { | |
await db | |
.prepare( | |
/*sql*/ `INSERT INTO ${options.tableName} (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`, | |
) | |
.bind(document.id, JSON.stringify(document)) | |
.run() | |
} | |
async function del(key: string) { | |
await db | |
.prepare(/*sql*/ `DELETE FROM ${options.tableName} WHERE key = ?`) | |
.bind(key) | |
.run() | |
} | |
async function list(): Promise<T[]> { | |
const result = await db | |
.prepare(/*sql*/ `SELECT value FROM ${options.tableName}`) | |
.all() | |
return result.results.map((row) => parseDocument(row.value)) | |
} | |
async function listWhere( | |
field: string, | |
operator: string, | |
value: any, | |
): Promise<T[]> { | |
field = field.replace(/[^a-zA-Z0-9_]/g, "") | |
const supportedSqliteOperators = ["=", ">", "<", ">=", "<=", "!="] | |
if (!supportedSqliteOperators.includes(operator)) { | |
throw new Error(`Unsupported operator: ${operator}`) | |
} | |
const sql = /* sql */ ` | |
SELECT | |
value, | |
json_extract(value, '$.${field}') AS operation | |
FROM ${options.tableName} | |
WHERE operation ${operator} ? | |
` | |
const result = await db.prepare(sql).bind(value).all() | |
return result.results.map((row) => parseDocument(row.value)) | |
} | |
function parseDocument(raw: any): T { | |
return options.schema.parse(JSON.parse(raw)) | |
} | |
return { get, set, del, list, listWhere } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment