-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
750be13
commit c69fc07
Showing
4 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export {}; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { BunORM } from "../index"; | ||
const db = new BunORM("db.sqlite", { | ||
tables: { | ||
roles: { | ||
columns: { | ||
name: { | ||
type: "TEXT", | ||
}, | ||
}, | ||
}, | ||
users: { | ||
columns: { | ||
username: { | ||
type: "TEXT", | ||
}, | ||
password: { | ||
type: "TEXT", | ||
}, | ||
lastOnline: { | ||
type: "JSON", | ||
// Value doesn't matter since we only need | ||
// it to infer the type | ||
customDataType: {}, | ||
}, | ||
roles: { | ||
type: "REL", | ||
table: "roles", | ||
}, | ||
}, | ||
mw: { | ||
get: [ | ||
(item) => Object.assign(item, { lastOnline: new Date(item.lastOnline) }), | ||
], | ||
}, | ||
fx: { | ||
checkPassword: (item, pw) => "123" + pw === item.password, | ||
hashPassword: (item) => { | ||
// Please use bcrypt or something similar instead of this | ||
// This code is made just for this example and should not be used | ||
// since it's super unsafe | ||
item.password = "123" + item.password; | ||
return item; | ||
}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
// Try to type something by yourself | ||
// Your IDE should autocomplete based on your db schema | ||
// (On VSCode you might have to press STRG + Space after typing "db." to toggle autocomplete) | ||
db.tables.roles.save({ | ||
name: "admin", | ||
}); | ||
db.tables.users.save(db.tables.users | ||
.create({ | ||
username: "admin", | ||
password: "nimda", | ||
lastOnline: new Date(), | ||
roles: 1, | ||
}) | ||
.hashPassword()); | ||
const admin = db.tables.users.find({ | ||
where: { | ||
username: "admin", | ||
}, | ||
})[0]; | ||
admin.roles; // -> will be of type number since the relation is not resolved | ||
const resolvedAdmin = db.tables.users.find({ | ||
where: { | ||
username: "admin", | ||
}, | ||
resolve: ["roles"], | ||
})[0]; | ||
resolvedAdmin.roles[0].name; | ||
const onlyUsernameAdmins = db.tables.users.find({ | ||
select: ["username"], | ||
}); | ||
// everything except username and the default attributes (id, createdAt, updatedAt and functions) will be undefined | ||
onlyUsernameAdmins[0].username; | ||
db.tables.users.delete({ | ||
username: "admin", | ||
}); |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/// <reference types="bun-types" /> | ||
import { Database } from "bun:sqlite"; | ||
type _Narrow<T, U> = [U] extends [T] ? U : Extract<T, U>; | ||
type Narrow<T = unknown> = _Narrow<T, (...args: any[]) => any> | _Narrow<T, 0 | (number & {})> | _Narrow<T, 0n | (bigint & {})> | _Narrow<T, "" | (string & {})> | _Narrow<T, boolean> | _Narrow<T, symbol> | _Narrow<T, []> | _Narrow<T, { | ||
[_: PropertyKey]: Narrow; | ||
}> | (T extends object ? { | ||
[K in keyof T]: Narrow<T[K]>; | ||
} : never) | Extract<{} | null | undefined, T>; | ||
type SQLiteDataType = "NULL" | "INTEGER" | "REAL" | "TEXT" | "BLOB"; | ||
type CustomDataType = "JSON"; | ||
export type DataType = SQLiteDataType | CustomDataType; | ||
export type NormalizeDataType<T extends DataType, C extends JSON | Column | Relation> = T extends "NULL" ? null : T extends "INTEGER" ? number : T extends "REAL" ? number : T extends "TEXT" ? string : T extends "BLOB" ? Uint8Array : T extends "JSON" ? C extends JSON ? undefined extends C["customDataType"] ? any : C["customDataType"] : any : unknown; | ||
type RemoveFirstParam<T> = T extends (first: any, ...rest: infer P) => infer R ? (...args: P) => R : never; | ||
export interface Relation { | ||
type: "REL"; | ||
table: keyof Tables; | ||
nullable?: boolean; | ||
} | ||
export interface Column { | ||
type: DataType; | ||
default?: string; | ||
nullable?: boolean; | ||
unique?: boolean; | ||
} | ||
export interface JSON extends Column { | ||
type: "JSON"; | ||
customDataType?: any; | ||
} | ||
export interface Columns { | ||
[name: string]: Column | JSON | Relation; | ||
} | ||
type AddColumnDefaults<T> = { | ||
id: number; | ||
} & T & { | ||
updatedAt: string; | ||
createdAt: string; | ||
}; | ||
type AddTableFx<T extends Tables[string], X> = X & (T extends undefined ? {} : { | ||
[name in keyof T["fx"]]: RemoveFirstParam<T["fx"][name]>; | ||
}); | ||
type ExcludeProps<T, U> = Pick<T, Exclude<keyof T, keyof U>>; | ||
type _ColumnSortOut<T extends Tables[string]["columns"], K extends keyof T> = T[K] extends Column ? T[K]["nullable"] extends true ? K : undefined extends T[K]["default"] ? never : K : T[K]["nullable"] extends true ? K : never; | ||
type And<A, TA, B, TB> = A extends TA ? (B extends TB ? true : false) : false; | ||
type _Columns<TT extends Tables, T extends Tables[string], WORelations extends boolean = false> = { | ||
[colName in keyof T["columns"] as _ColumnSortOut<T["columns"], colName>]?: And<WORelations, true, T["columns"][colName]["type"], "REL"> extends true ? number : T["columns"][colName] extends Column ? NormalizeDataType<T["columns"][colName]["type"], T["columns"][colName]> : T["columns"][colName] extends Relation ? _Columns<TT, TT[T["columns"][colName]["table"]]>[] : never; | ||
} & ExcludeProps<{ | ||
[colName in keyof T["columns"]]: And<WORelations, true, T["columns"][colName]["type"], "REL"> extends true ? number : T["columns"][colName] extends Column ? NormalizeDataType<T["columns"][colName]["type"], T["columns"][colName]> : T["columns"][colName] extends Relation ? _Columns<TT, TT[T["columns"][colName]["table"]]>[] : never; | ||
}, { | ||
[colName in keyof T["columns"] as _ColumnSortOut<T["columns"], colName>]: And<WORelations, true, T["columns"][colName]["type"], "REL"> extends true ? number : T["columns"][colName] extends Column ? NormalizeDataType<T["columns"][colName]["type"], T["columns"][colName]> : T["columns"][colName] extends Relation ? _Columns<TT, TT[T["columns"][colName]["table"]]>[] : never; | ||
}>; | ||
export interface Table { | ||
columns: Columns; | ||
fx?: { | ||
[name: string]: Narrow<(item: any, ...args: any[]) => any>; | ||
}; | ||
mw?: { | ||
get?: Narrow<(item: any) => any>[]; | ||
set?: Narrow<(item: any) => any>[]; | ||
}; | ||
} | ||
export interface Tables { | ||
[name: string]: Table; | ||
} | ||
type SortOut<I, E> = Pick<I, { | ||
[K in keyof I]: I[K] extends E ? K : never; | ||
}[keyof I]>; | ||
type Or<A, TA, B, TB> = A extends TA ? true : B extends TB ? true : false; | ||
interface _TableFunctions<TT extends Tables, T extends Tables[string]> { | ||
create: (cols: _Columns<TT, T, true>) => AddTableFx<T, _Columns<TT, T, true>>; | ||
save: (cols: { | ||
id?: number; | ||
} & _Columns<TT, T, true>) => void; | ||
delete: (opts: Partial<_Columns<TT, T, true>>) => void; | ||
find: <S extends (keyof T["columns"])[] | undefined, R extends Narrow<(keyof SortOut<T["columns"], { | ||
type: "REL"; | ||
}>)[] | undefined>>(opts?: { | ||
where?: Partial<_Columns<TT, T, true>>; | ||
select?: S; | ||
resolve?: R; | ||
}) => AddTableFx<T, AddColumnDefaults<Or<S, (keyof T["columns"])[], R, (keyof SortOut<T["columns"], { | ||
type: "REL"; | ||
}>)[]> extends true ? { | ||
[colName in S extends (keyof T["columns"])[] ? keyof Pick<T["columns"], S[number]> : keyof T["columns"]]: T["columns"][colName] extends Column ? NormalizeDataType<T["columns"][colName]["type"], T["columns"][colName]> : T["columns"][colName] extends Relation ? R extends (keyof SortOut<T["columns"], { | ||
type: "REL"; | ||
}>)[] ? R[number] extends never ? number : colName extends R[number] ? _Columns<TT, TT[T["columns"][colName]["table"]]>[] : number : number : never; | ||
} : _Columns<TT, T, true>>>[]; | ||
findBy: (opts: Partial<_Columns<TT, T>>) => AddTableFx<T, AddColumnDefaults<_Columns<TT, T, true>>>[]; | ||
} | ||
type _Tables<T extends Tables> = { | ||
[tableName in keyof T]: _TableFunctions<T, T[tableName]>; | ||
}; | ||
export interface Config<T extends Tables> { | ||
tables: T; | ||
} | ||
export declare class BunORM<T extends Narrow<Tables>> { | ||
readonly config: Config<T>; | ||
tables: _Tables<T>; | ||
db: Database; | ||
private createTable; | ||
private Col; | ||
constructor(file: string, config: Config<T>); | ||
} | ||
export default BunORM; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { Database } from "bun:sqlite"; | ||
const arr = (length, map) => new Array(length).fill(0).map((_, i) => map(i + 1)); | ||
export class BunORM { | ||
config; | ||
tables; | ||
db; | ||
createTable = (table, opts) => (typeof this.db.run(`CREATE TABLE IF NOT EXISTS ${table} ('id' INTEGER PRIMARY KEY AUTOINCREMENT,${Object.entries(opts.columns) | ||
.filter(([_, opts]) => opts.type !== "REL") | ||
.map(([col, opts]) => [ | ||
`'${col}'`, | ||
opts.type === "JSON" ? "TEXT" : opts.type, | ||
opts.default && `DEFAULT ${opts.default}`, | ||
opts.unique && "UNIQUE", | ||
!opts.nullable && "NOT NULL", | ||
] | ||
.filter((x) => !!x) | ||
.join(" ")) | ||
.join()}${Object.entries(opts.columns).filter(([_, opts]) => opts.type === "REL").length | ||
? "," + | ||
Object.entries(opts.columns) | ||
.filter(([_, opts]) => opts.type === "REL") | ||
.map(([col, opts]) => [`'${col}' INTEGER`, !opts.nullable && "NOT NULL"] | ||
.filter((x) => !!x) | ||
.join(" ")) | ||
.join() | ||
: ""},'updatedAt' DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,'createdAt' DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL${Object.entries(opts.columns).filter(([_, opts]) => opts.type === "REL").length | ||
? "," + | ||
Object.entries(opts.columns) | ||
.filter(([_, opts]) => opts.type === "REL") | ||
.map(([col, opts]) => `FOREIGN KEY ('${col}') REFERENCES ${opts.table} (id)`) | ||
.join() | ||
: ""});`) !== "number"); | ||
Col = (table) => { | ||
const Table = this.config.tables[table], injectFx = (rows, table = Table) => rows.map((x) => Object.assign(x, Object.fromEntries(Object.entries(table.fx || {}).map(([k, v]) => [ | ||
k, | ||
(...args) => v(x, ...args), | ||
])))), parseJSON = (rows, table = Table) => Object.values(table.columns).every((x) => x.type !== "JSON") | ||
? rows | ||
: rows.map((row) => Object.fromEntries(Object.entries(row).map(([k, v]) => [ | ||
k, | ||
table.columns[k] && table.columns[k].type === "JSON" | ||
? JSON.parse(v) | ||
: v, | ||
]))), executeGetMiddleware = (rows, table = Table) => !Array.isArray(table.mw?.get) || | ||
table.mw.get.some((mw) => typeof mw !== "function") | ||
? rows | ||
: rows.map((row) => { | ||
table.mw.get.forEach((mw) => (row = mw(row))); | ||
return row; | ||
}), resolveRelations = (rows, keys) => !keys.length || | ||
Object.values(Table.columns).every((x) => x.type !== "REL") | ||
? rows | ||
: rows.map((row) => Object.fromEntries(Object.entries(row).map(([k, v]) => [ | ||
k, | ||
Table.columns[k] && Table.columns[k].type === "REL" | ||
? executeGetMiddleware(injectFx(parseJSON(this.db | ||
.query(`SELECT * FROM ${Table.columns[k].table} WHERE id = $id`) | ||
.all({ $id: v }), this.config.tables[Table.columns[k] | ||
.table]), this.config.tables[Table.columns[k] | ||
.table]), this.config.tables[Table.columns[k] | ||
.table]) | ||
: v, | ||
]))); | ||
return { | ||
create: (cols) => injectFx([cols])[0], | ||
save: (_cols) => { | ||
const cols = Object.fromEntries(Object.entries(_cols) | ||
.filter(([_, v]) => typeof v !== "function") | ||
.map(([col, val]) => [ | ||
col, | ||
Table.columns[col].type === "JSON" ? JSON.stringify(val) : val, | ||
])); | ||
cols.id && | ||
this.db | ||
.query(`SELECT COUNT(*) AS count FROM ${table} WHERE id = $id;`) | ||
.get({ $id: cols.id }).count !== 0 | ||
? this.db | ||
.query(`UPDATE ${table} SET ${Object.keys(cols) | ||
.filter((x) => x !== "id") | ||
.map((x, i) => `${x} = $S_${x}`) | ||
.join()} WHERE id = $id;`) | ||
.run(Object.fromEntries([ | ||
...Object.entries(cols) | ||
.filter(([k]) => k !== "id") | ||
.map(([k, v]) => [`$S_${k}`, v]), | ||
["$id", cols.id], | ||
])) | ||
: this.db | ||
.query(`INSERT INTO ${table} ` + | ||
(Object.keys(cols).length === 0 | ||
? "DEFAULT VALUES;" | ||
: `('${Object.keys(cols).join("','")}') VALUES (${arr(Object.keys(cols).length, (i) => `?${i}`).join()});`)) | ||
.all(...Object.values(cols)); | ||
}, | ||
delete: (opts) => this.db | ||
.query(`DELETE FROM ${table} WHERE ${Object.keys(opts) | ||
.map((x, i) => `${x} = ?${i + 1}`) | ||
.join(" AND ")};`) | ||
.run(...Object.values(opts)), | ||
find: (opts) => resolveRelations(executeGetMiddleware(injectFx(parseJSON(this.db | ||
.query(!opts | ||
? `SELECT * FROM ${table};` | ||
: `SELECT ${opts.select | ||
? opts.select.length === 0 | ||
? "" | ||
: opts.select.join() | ||
: "*"} FROM ${table}${opts.where | ||
? ` WHERE ${Object.keys(opts.where) | ||
.map((x, i) => `${x} = ?${i + 1}`) | ||
.join(" AND ")}` | ||
: ""};`) | ||
.all(...(opts?.where ? Object.values(opts.where) : []))))), opts?.resolve || []), | ||
findBy: (opts) => injectFx(parseJSON(this.db | ||
.query(!opts | ||
? `SELECT * FROM ${table};` | ||
: `SELECT * FROM ${table}${opts | ||
? ` WHERE ${Object.keys(opts) | ||
.map((x, i) => `${x} = ?${i + 1}`) | ||
.join(" AND ")}` | ||
: ""};`) | ||
.all(...(opts ? Object.values(opts) : [])))), | ||
}; | ||
}; | ||
constructor(file, config) { | ||
this.config = config; | ||
this.db = new Database(file); | ||
Object.values(config.tables).some((x) => Object.values(x.columns).some((y) => y.type === "REL")) && | ||
!this.db.query("PRAGMA foreign_keys;").get().foreign_keys && | ||
this.db.query("PRAGMA foreign_keys = ON;").run(); | ||
this.tables = Object.fromEntries(Object.entries(config.tables).map(([table, opts]) => this.createTable(table, opts) && [table, this.Col(table)])); | ||
} | ||
} | ||
export default BunORM; |