├── .gitignore ├── LICENSE ├── README.md ├── lib └── SmallSQLite.ts └── mod.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | main.ts 3 | test.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 cybertim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Small SQLite ORM 2 | 3 | [![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/raw.githubusercontent.com/cybertim/SmallSQLiteORM/main/mod.ts) 4 | 5 | Very small Object-relational mapper (bare essential) to quickly setup embedded database in SQLite Deno/Typescript/Web. 6 | 7 | ## Learn By Examples 8 | 9 | ```typescript 10 | import { SSQL, SSQLTable } from "./lib/SmallSQLite.ts"; 11 | 12 | export class User extends SSQLTable { 13 | userName = ""; 14 | address = ""; 15 | active = false; 16 | age = 0; 17 | } 18 | 19 | export class Log extends SSQLTable { 20 | userId = -1; 21 | insertDate = new Date().getDate(); 22 | description = ""; 23 | status = 0; 24 | } 25 | 26 | const orm = new SSQL("test.db", [User, Log]); 27 | 28 | const user = new User(); 29 | 30 | user.address = "Denoland 12"; 31 | user.userName = "Joe Deno"; 32 | user.active = true; // Make Joe active 33 | orm.save(user); 34 | 35 | console.log(user.id); // Joe now has an id of 1 in our DB 36 | 37 | // Add 5 total some Logs 38 | for (let i = 0; i < 5; i++) { 39 | const log = new Log(); 40 | log.userId = user.id; 41 | log.description = "log " + i; 42 | log.status = 1; 43 | orm.save(log); 44 | } 45 | 46 | console.log("5 logs total: " + orm.count(Log)); 47 | 48 | // Update only 2 logs with status 2 in the db 49 | for (const log of orm.findMany(Log, { limit: 2 })) { 50 | log.status = 2; 51 | orm.save(log); 52 | } 53 | 54 | console.log( 55 | "Count only 2 logs with status > 1: " + 56 | orm.countBy(Log, { where: { clause: "status > ?", values: [1] } }) 57 | ); 58 | 59 | const orderedLogs = orm.findMany(Log, { 60 | where: { clause: "status < ?", values: [2] }, 61 | order: { by: "id", desc: true } 62 | }) 63 | 64 | for (const l of orderedLogs) console.log("ordered desc: " + l.id + " " + l.status); 65 | 66 | const logs = orm.findMany(Log, { offset: 4, limit: 1 }); // Returns only 1 result on offset 4 67 | 68 | const logUser = orm.findOne(User, logs[0].userId); // quickly retrieve the user of the log 69 | 70 | orm.delete(logs[0]); // Removed from the DB 71 | 72 | console.log("only 4 logs left: " + orm.count(Log)); 73 | ``` 74 | 75 | ## Breaking changes 0.1.5 -> 0.2.0 76 | * Classnames have been renamed eg. `SmallSQLiteORM` changed into `SSQL`. 77 | * All methodcalls use the `SSQLQuery` object instead with more options. 78 | 79 | ## Documentation 80 | View it online at [doc.deno.land](https://doc.deno.land/https/raw.githubusercontent.com/cybertim/SmallSQLiteORM/main/mod.ts) 81 | 82 | ## Extra Features 83 | - Automatically CREATE TABLE when database file is initialized 84 | - Automatically ALTER TABLE when model class is changed (only ADD COLUMN) 85 | -------------------------------------------------------------------------------- /lib/SmallSQLite.ts: -------------------------------------------------------------------------------- 1 | import { DB } from "https://deno.land/x/sqlite@v2.5.0/mod.ts"; 2 | 3 | /** 4 | * All your model classes should extend this class 5 | * It includes the incremental 'id' by default 6 | * ```ts 7 | * class User extends SSQLTable { 8 | * username = ""; 9 | * age = 18; 10 | * active = false; 11 | * } 12 | * ``` 13 | * @export 14 | * @class SSQLTable 15 | */ 16 | export class SSQLTable { 17 | id = -1; 18 | } 19 | 20 | /** 21 | * Interface used to override the DEFAULT column type values 22 | * @export 23 | * @interface SSQLDefaults 24 | */ 25 | export interface SSQLDefaults { 26 | bool: boolean; 27 | str: string; 28 | int: number; 29 | } 30 | 31 | /** 32 | * Interface used for queries 33 | */ 34 | export interface SSQLQuery { 35 | where?: { 36 | clause: string; 37 | values: (boolean | string | number)[] 38 | }, 39 | order?: { 40 | by: string; 41 | desc?: boolean; 42 | }, 43 | limit?: number, 44 | offset?: number 45 | } 46 | 47 | /** 48 | * ORM Wrapper to interact with deno.land/x/sqlite using your `SSQLTable` 49 | * @export 50 | * @class SSQL 51 | */ 52 | export class SSQL { 53 | public db: DB; 54 | private defaults: SSQLDefaults; 55 | 56 | /** 57 | * Create an instance of SSQL 58 | * ```ts 59 | * const orm = new SSQL("test.db", [User]); 60 | * ``` 61 | * @param dbName the name of the database file on disk used by sqlite 62 | * @param entities array of all models extending `SSQLTable` 63 | * @param defaults optional configuration to override DEFAULT vaules of columns by type 64 | */ 65 | constructor(dbName: string, entities: (new () => SSQLTable)[], defaults?: SSQLDefaults) { 66 | this.db = new DB(dbName); 67 | defaults ? this.defaults = defaults : this.defaults = { 68 | bool: false, 69 | int: -1, 70 | str: "" 71 | }; 72 | for (const entity of entities) { 73 | const obj = new entity(); 74 | this.createTable(obj); // create the table if it is not yet there 75 | const names = Object.getOwnPropertyNames(obj); 76 | // retrieve a list of all columns known in the sqlite db 77 | const data: string[] = []; 78 | for ( 79 | const [loc, col] of this.db.query("PRAGMA table_info(" + obj.constructor.name.toLowerCase() + ");") 80 | ) { 81 | data.push(col); 82 | } 83 | // check if there are new properties in the model compared to the table in sqlite 84 | const n = names.filter((item) => !data.includes(item)); 85 | if (n.length > 0) this.alterTable(obj, n); 86 | } 87 | } 88 | 89 | private columnInfo(table: T, column: string) { 90 | const v = Object.getOwnPropertyDescriptor(table, column); 91 | if (column === "id") { 92 | return "integer PRIMARY KEY AUTOINCREMENT NOT NULL"; 93 | } else if (typeof v?.value === "boolean") { 94 | return "boolean NOT NULL DEFAULT " + this.defaults.bool; 95 | } else if (typeof v?.value === "string") { 96 | return 'varchar DEFAULT "' + this.defaults.str + '"'; 97 | } else if (typeof v?.value === "number") { 98 | return "integer NOT NULL DEFAULT " + this.defaults.int; 99 | } 100 | return undefined; 101 | } 102 | 103 | private alterTable(table: T, columns: string[]) { 104 | for (const column of columns) { 105 | const statement = 'ALTER TABLE "' + table.constructor.name.toLowerCase() + 106 | '\" ADD COLUMN ' + column + " " + 107 | this.columnInfo(table, column); 108 | this.db.query(statement); 109 | } 110 | } 111 | 112 | private createTable(table: T) { 113 | const names = Object.getOwnPropertyNames(table); 114 | let statement = 'CREATE TABLE IF NOT EXISTS "' + table.constructor.name.toLowerCase() + '" ('; 115 | for (const p of names) { 116 | if (!statement.endsWith("(")) statement += ", "; 117 | statement += '"' + p + '" ' + this.columnInfo(table, p); 118 | } 119 | statement += ")"; 120 | this.db.query(statement); 121 | } 122 | 123 | private insertRecord(table: T) { 124 | const names = Object.getOwnPropertyNames(table); 125 | names.splice(0, 1); 126 | const statement = 'INSERT INTO "' + 127 | table.constructor.name.toLowerCase() + 128 | '" (' + names.join(", ") + ") VALUES (" + 129 | (new Array(names.length).fill("?")).join(", ") + ")"; 130 | const data = []; 131 | for (const p of names) { 132 | const v = Object.getOwnPropertyDescriptor(table, p); 133 | data.push(v?.value); 134 | } 135 | this.db.query(statement, data); 136 | table.id = this.db.lastInsertRowId; 137 | } 138 | 139 | private updateRecord(table: T) { 140 | const names = Object.getOwnPropertyNames(table); 141 | names.splice(0, 1); 142 | let statement = 'UPDATE "' + table.constructor.name.toLowerCase() + '" SET '; 143 | const data = []; 144 | for (const p of names) { 145 | const v = Object.getOwnPropertyDescriptor(table, p); 146 | if (!statement.endsWith("SET ")) statement += ", "; 147 | statement += p + " = ?"; 148 | data.push(v?.value); 149 | } 150 | statement += " WHERE id = ?"; 151 | data.push(table.id); 152 | this.db.query(statement, data); 153 | } 154 | 155 | private find( 156 | table: (new () => T), query: SSQLQuery, countOnly?: boolean): { count: number; objects: T[] } { 157 | let select = "*"; 158 | if (countOnly) select = "COUNT(*) AS total"; 159 | const obj = new table(); 160 | const rows = this.db.query( 161 | "SELECT " + select + ' FROM "' + obj.constructor.name + '"' + 162 | (query.where ? (" WHERE " + query.where.clause) : "") + 163 | (query.order ? (" ORDER BY " + query.order.by + (query.order.desc ? " DESC " : " ASC ")) : "") + 164 | (query.limit ? " LIMIT " + query.limit : "") + 165 | (query.offset ? " OFFSET " + query.offset : ""), 166 | (query.where ? query.where.values : []) 167 | ); 168 | if (!countOnly) { 169 | const list: T[] = []; 170 | let names: string[] = []; 171 | try { names = rows.columns().map((item) => item.name); } catch (e) { 172 | return { count: 0, objects: list }; 173 | } 174 | for (const row of rows) { 175 | const nobj = new table(); 176 | for (let i = 0; i < names.length; i++) { 177 | Object.defineProperty(nobj, names[i], { value: row[i] }); 178 | } 179 | list.push(nobj); 180 | } 181 | return { count: list.length, objects: list }; 182 | } else { 183 | return { count: rows.next().value[0], objects: [] }; 184 | } 185 | } 186 | 187 | /** 188 | * DELETE the obj from the SQLite database 189 | * @param obj model based on `SSQLTable` 190 | */ 191 | public delete(obj: T) { 192 | this.db.query('DELETE FROM "' + obj.constructor.name + '" WHERE id = ?', [obj.id]); 193 | } 194 | 195 | /** 196 | * INSERT or UPDATE the obj based on the id (INSERT when -1 else UPDATE) 197 | * @param obj model based on `SSQLTable` 198 | */ 199 | public save(obj: T) { 200 | if (obj.id === -1) this.insertRecord(obj); 201 | else this.updateRecord(obj); 202 | } 203 | 204 | /** 205 | * SELECT * FROM table and return model WHERE id equals given id 206 | * ```ts 207 | * const user = orm.findOne(User, 1); 208 | * ``` 209 | * @param table 210 | * @param id id to match with `SSQLTable` 211 | */ 212 | public findOne(table: (new () => T), id: number) { 213 | return this.find(table, { where: { clause: "id = ?", values: [id] } }).objects[0]; 214 | } 215 | 216 | /** 217 | * ```ts 218 | * const users = orm.findMany(User, { where: { clause: "username = ?", values: [username] }}); 219 | * ``` 220 | * @param table 221 | * @param query 222 | */ 223 | public findMany(table: (new () => T), query: SSQLQuery) { 224 | return this.find(table, query).objects; 225 | } 226 | 227 | /** 228 | * COUNT(*) on all records in the table given 229 | * @param table 230 | */ 231 | public count(table: (new () => T)) { 232 | return this.find(table, {}, true).count; 233 | } 234 | 235 | /** 236 | * COUNT(*) on all records in the table given matching the `SSQLQuery` query object 237 | * @param table 238 | * @param query 239 | */ 240 | public countBy(table: (new () => T), query: SSQLQuery) { 241 | return this.find(table, query, true).count; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/SmallSQLite.ts"; 2 | --------------------------------------------------------------------------------