From c69fc078661a213a81c7f408fe300ec4348b084b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Mar 2023 19:45:42 +0100 Subject: [PATCH] Build --- lib/example/example.d.ts | 1 + lib/example/example.js | 82 ++++++++++++++++++++++++ lib/index.d.ts | 103 ++++++++++++++++++++++++++++++ lib/index.js | 133 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 lib/example/example.d.ts create mode 100644 lib/example/example.js create mode 100644 lib/index.d.ts create mode 100644 lib/index.js diff --git a/lib/example/example.d.ts b/lib/example/example.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/lib/example/example.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/lib/example/example.js b/lib/example/example.js new file mode 100644 index 0000000..a064406 --- /dev/null +++ b/lib/example/example.js @@ -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", +}); diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..ae795c4 --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,103 @@ +/// +import { Database } from "bun:sqlite"; +type _Narrow = [U] extends [T] ? U : Extract; +type Narrow = _Narrow any> | _Narrow | _Narrow | _Narrow | _Narrow | _Narrow | _Narrow | _Narrow | (T extends object ? { + [K in keyof T]: Narrow; +} : 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 "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 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 = { + id: number; +} & T & { + updatedAt: string; + createdAt: string; +}; +type AddTableFx = X & (T extends undefined ? {} : { + [name in keyof T["fx"]]: RemoveFirstParam; +}); +type ExcludeProps = Pick>; +type _ColumnSortOut = 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 extends TA ? (B extends TB ? true : false) : false; +type _Columns = { + [colName in keyof T["columns"] as _ColumnSortOut]?: And extends true ? number : T["columns"][colName] extends Column ? NormalizeDataType : T["columns"][colName] extends Relation ? _Columns[] : never; +} & ExcludeProps<{ + [colName in keyof T["columns"]]: And extends true ? number : T["columns"][colName] extends Column ? NormalizeDataType : T["columns"][colName] extends Relation ? _Columns[] : never; +}, { + [colName in keyof T["columns"] as _ColumnSortOut]: And extends true ? number : T["columns"][colName] extends Column ? NormalizeDataType : T["columns"][colName] extends Relation ? _Columns[] : 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 = Pick; +type Or = A extends TA ? true : B extends TB ? true : false; +interface _TableFunctions { + create: (cols: _Columns) => AddTableFx>; + save: (cols: { + id?: number; + } & _Columns) => void; + delete: (opts: Partial<_Columns>) => void; + find: )[] | undefined>>(opts?: { + where?: Partial<_Columns>; + select?: S; + resolve?: R; + }) => AddTableFx)[]> extends true ? { + [colName in S extends (keyof T["columns"])[] ? keyof Pick : keyof T["columns"]]: T["columns"][colName] extends Column ? NormalizeDataType : T["columns"][colName] extends Relation ? R extends (keyof SortOut)[] ? R[number] extends never ? number : colName extends R[number] ? _Columns[] : number : number : never; + } : _Columns>>[]; + findBy: (opts: Partial<_Columns>) => AddTableFx>>[]; +} +type _Tables = { + [tableName in keyof T]: _TableFunctions; +}; +export interface Config { + tables: T; +} +export declare class BunORM> { + readonly config: Config; + tables: _Tables; + db: Database; + private createTable; + private Col; + constructor(file: string, config: Config); +} +export default BunORM; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..d10d6bb --- /dev/null +++ b/lib/index.js @@ -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;