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;