├── .gitignore ├── .prettierrc ├── README.md ├── jest.config.js ├── package.json ├── release.sh ├── src ├── column.ts ├── condition.ts ├── db.ts ├── dialects │ ├── base │ │ ├── index.ts │ │ ├── read.ts │ │ ├── readAlias.ts │ │ └── where.ts │ ├── index.ts │ └── join │ │ ├── condition.ts │ │ ├── index.ts │ │ ├── mapper.ts │ │ ├── sql.ts │ │ └── types.ts ├── drivers │ ├── expo.ts │ ├── native.ts │ └── sqlite3.ts ├── index.ts ├── primary.ts ├── scope.ts ├── table.ts ├── types.ts └── utils.ts ├── test ├── data │ ├── crud.test.ts │ ├── driver │ │ └── index.ts │ ├── entity │ │ ├── Address.ts │ │ ├── Person.ts │ │ ├── Role.ts │ │ └── index.ts │ ├── insert.test.ts │ ├── join.test.ts │ └── single.test.ts ├── driver │ ├── driver │ │ └── index.ts │ ├── entity │ │ ├── Person.ts │ │ └── index.ts │ └── index.test.ts ├── readme │ └── index.test.ts ├── sql │ ├── any.test.ts │ ├── count.test.ts │ ├── create.test.ts │ ├── delete.test.ts │ ├── driver │ │ └── index.ts │ ├── drop.test.ts │ ├── entity │ │ ├── Address.ts │ │ ├── Person.ts │ │ ├── Role.ts │ │ └── index.ts │ ├── init.test.ts │ ├── insert.test.ts │ ├── join.test.ts │ ├── select.test.ts │ ├── single.test.ts │ ├── transaction.test.ts │ └── update.test.ts └── tsconfig.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | tmp/ 4 | typings/ 5 | npm-debug.* 6 | yarn-error.* 7 | *.db -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLITE-TS 2 | 3 | SQLite ORM for Typescript 4 | 5 | ## Installation 6 | 7 | Using npm: 8 | 9 | ``` 10 | npm i -S sqlite-ts 11 | ``` 12 | 13 | or yarn: 14 | 15 | ``` 16 | yarn add sqlite-ts 17 | ``` 18 | 19 | ## Usage 20 | 21 | It's easy! 22 | 23 | ### Define Entities 24 | 25 | ```ts 26 | import { Column, Primary } from 'sqlite-ts' 27 | 28 | class Person { 29 | @Primary() 30 | id: number = 0 31 | 32 | @Column('NVARCHAR') 33 | name: string = '' 34 | 35 | @Column('DATETIME') 36 | dob: Date = new Date() 37 | 38 | @Column('INTEGER') 39 | age: number = 0 40 | 41 | @Column('BOOLEAN') 42 | married: boolean = false 43 | 44 | @Column('MONEY') 45 | salary: number = 0 46 | } 47 | 48 | class Address { 49 | @Primary() 50 | id: number = 0 51 | 52 | @Column('INTEGER') 53 | person: number = 0 54 | 55 | @Column('NVARCHAR') 56 | address: string = '' 57 | } 58 | ``` 59 | 60 | ### Connect to Database 61 | 62 | ```ts 63 | // let's use sqlite3 from https://github.com/mapbox/node-sqlite3 64 | import Sqlite3 = require('sqlite3') 65 | 66 | // define entities object 67 | const entities = { 68 | Person, 69 | Address 70 | } 71 | 72 | // make a connection using SQLite3. 73 | // you can use other available drivers 74 | // or create your own 75 | const sqlite3Db = new sqlite.Database(':memory:') 76 | const db = await Db.init({ 77 | // set the driver 78 | driver: new SQLite3Driver(sqlite3Db), 79 | 80 | // set your entities here 81 | entities, 82 | 83 | // set `true` so all tables in entities will automatically created for you 84 | // if it does not exists yet in database 85 | createTables: false 86 | }) 87 | ``` 88 | 89 | ### Working with Entities 90 | 91 | From now to work with entities you can access your entities via `db.tables.[entity name].[action function]`. 92 | 93 | #### Create 94 | 95 | For example to create table you can simply do this: 96 | 97 | ```ts 98 | await db.tables.Person.create() 99 | await db.tables.Address.create() 100 | ``` 101 | 102 | or 103 | 104 | ```ts 105 | await db.createAllTables() 106 | ``` 107 | 108 | #### Insert 109 | 110 | ```ts 111 | // insert single data 112 | const result = await db.tables.Person.insert({ 113 | name: 'Joey', 114 | married: true, 115 | dob: new Date(2000, 1, 1, 0, 0, 0), 116 | age: 18, 117 | salary: 100 118 | }) 119 | ``` 120 | 121 | The `Person` entity is using default primary key which is `INTEGER` that is autogenerated. 122 | You can get inserted primary key value from the `result` of `insert` action above that returns: 123 | 124 | ```ts 125 | { 126 | insertId: 1, // generated primary key 127 | rowsAffected: 1 // number of created data 128 | } 129 | ``` 130 | 131 | You may want to insert multiple data at once like so: 132 | 133 | ```ts 134 | // insert multiple data at once 135 | const results = await db.tables.Person.insert([ 136 | { 137 | name: 'Hanna', 138 | married: false, 139 | dob: new Date(2001, 2, 2, 0, 0, 0), 140 | age: 17, 141 | salary: 100 142 | }, 143 | { 144 | name: 'Mary', 145 | married: false, 146 | dob: new Date(2002, 3, 3, 0, 0, 0), 147 | age: 26, 148 | salary: 50 149 | } 150 | ]) 151 | ``` 152 | 153 | But you can't get advantage of getting the generated primary keys for inserted data. 154 | Because the `results` only returns the last generated primary key: 155 | 156 | ```ts 157 | { 158 | insertId: 3, // latest generated primary key 159 | rowsAffected: 2 // number of created data 160 | } 161 | ``` 162 | 163 | If you have multiple action that you want to execute under `BEGIN` and `COMMIT` statement, 164 | you can use transaction to do this: 165 | 166 | ```ts 167 | await db.transaction(({ exec, tables }) => { 168 | exec( 169 | tables.Address.insert({ 170 | person: 1, 171 | address: `Joy's Home` 172 | }) 173 | ) 174 | exec( 175 | tables.Address.insert({ 176 | person: 2, 177 | address: `Hanna's Home` 178 | }) 179 | ) 180 | exec( 181 | tables.Address.insert({ 182 | person: 3, 183 | address: `Marry's Home` 184 | }) 185 | ) 186 | }) 187 | ``` 188 | 189 | Need to get inserted generated primary key under transaction? Simply do this instead: 190 | 191 | ```ts 192 | let address1: any 193 | let address2: any 194 | let address3: any 195 | await db.transaction(({ exec, tables }) => { 196 | exec( 197 | tables.Address.insert({ 198 | person: 1, 199 | address: `Joy's Home` 200 | }) 201 | ).then(r => { 202 | address1 = r 203 | }) 204 | 205 | exec( 206 | tables.Address.insert({ 207 | person: 2, 208 | address: `Hanna's Home` 209 | }) 210 | ).then(r => { 211 | address2 = r 212 | }) 213 | 214 | exec( 215 | tables.Address.insert({ 216 | person: 3, 217 | address: `Marry's Home` 218 | }) 219 | ).then(r => { 220 | address3 = r 221 | }) 222 | }) 223 | ``` 224 | 225 | The actions above should returns: 226 | 227 | ```ts 228 | // address1: 229 | { 230 | insertId: 1, 231 | rowsAffected: 1 232 | } 233 | 234 | // address2: 235 | { 236 | insertId: 2, 237 | rowsAffected: 1 238 | } 239 | 240 | // address3: 241 | { 242 | insertId: 1, 243 | rowsAffected: 1 244 | } 245 | ``` 246 | 247 | You can also do same things for `upsert`, `update`, `delete`, `create` and `drop` action. 248 | 249 | #### Select 250 | 251 | ##### Select All 252 | 253 | ```ts 254 | // select all 255 | const people = await db.tables.Person.select() 256 | ``` 257 | 258 | returns: 259 | 260 | ``` 261 | [ 262 | { id: 1, 263 | name: 'Joey', 264 | dob: 2000-01-31T17:00:00.000Z, 265 | age: 18, 266 | married: true, 267 | salary: 100 268 | }, 269 | { id: 2, 270 | name: 'Hanna', 271 | dob: 2001-03-01T17:00:00.000Z, 272 | age: 17, 273 | married: false, 274 | salary: 100 275 | }, 276 | { id: 3, 277 | name: 'Mary', 278 | dob: 2002-04-02T17:00:00.000Z, 279 | age: 26, 280 | married: false, 281 | salary: 50 282 | } 283 | ] 284 | ``` 285 | 286 | ##### Select Columns 287 | 288 | ```ts 289 | // select columns 290 | const people2 = await db.tables.Person.select(c => [c.id, c.name, c.salary]) 291 | ``` 292 | 293 | returns: 294 | 295 | ``` 296 | [ 297 | { id: 1, name: 'Joey', salary: 100 }, 298 | { id: 2, name: 'Hanna', salary: 100 }, 299 | { id: 3, name: 'Mary', salary: 50 } 300 | ] 301 | ``` 302 | 303 | ##### Select Limit 304 | 305 | ```ts 306 | // select with limit 307 | const people3 = await db.tables.Person.select(c => [ 308 | c.id, 309 | c.name, 310 | c.salary 311 | ]).limit(1) 312 | ``` 313 | 314 | returns: 315 | 316 | ``` 317 | [{ id: 1, name: 'Joey', salary: 100 }] 318 | ``` 319 | 320 | ##### Select Where 321 | 322 | ```ts 323 | // select with condition 324 | const people4 = await db.tables.Person.select(c => [c.id, c.name]).where(c => 325 | c.greaterThanOrEqual({ salary: 100 }) 326 | ) 327 | ``` 328 | 329 | returns: 330 | 331 | ``` 332 | [ { id: 1, name: 'Joey' }, { id: 2, name: 'Hanna' } ] 333 | ``` 334 | 335 | ##### Select Order 336 | 337 | ```ts 338 | // select with order 339 | const people5 = await db.tables.Person.select(c => [c.id, c.name]) 340 | .where(c => c.notEquals({ married: true })) 341 | .orderBy({ name: 'DESC' }) 342 | ``` 343 | 344 | returns: 345 | 346 | ``` 347 | [ { id: 3, name: 'Mary' }, { id: 2, name: 'Hanna' } ] 348 | ``` 349 | 350 | ##### Select Single Data 351 | 352 | ```ts 353 | // select single data 354 | const person = await db.tables.Person.single(c => [c.id, c.name]) 355 | ``` 356 | 357 | returns: 358 | 359 | ``` 360 | { id: 1, name: 'Joey' } 361 | ``` 362 | 363 | For the rest, you can play around with editor intellisense to get more options. 364 | 365 | #### Update 366 | 367 | ```ts 368 | // let's prove that she's not married yet 369 | let hanna = await db.tables.Person.single(c => [c.id, c.name, c.married]).where( 370 | c => c.equals({ id: 2 }) 371 | ) 372 | // returns: 373 | // hanna is not married yet = { id: 2, name: 'Hanna', married: false } 374 | 375 | // let's marry her 376 | await db.tables.Person.update({ married: true }).where(c => c.equals({ id: 2 })) 377 | 378 | hanna = await db.tables.Person.single(c => [c.id, c.name, c.married]).where(c => 379 | c.equals({ id: 2 }) 380 | ) 381 | // returns: 382 | // hanna is now married = { id: 2, name: 'Hanna', married: true } 383 | ``` 384 | 385 | #### Join 386 | 387 | ```ts 388 | const people6 = await db.tables.Person.join( 389 | t => ({ 390 | // FROM Person AS self JOIN Address AS address 391 | address: t.Address 392 | }), 393 | (p, { address }) => { 394 | // ON self.id = address.person 395 | p.equal({ id: address.person }) 396 | } 397 | ).map(f => ({ 398 | // SELECT self.id AS id, self.name AS name, address.address AS address 399 | id: f.self.id, 400 | name: f.self.name, 401 | address: f.address.address 402 | })) 403 | ``` 404 | 405 | results: 406 | 407 | ``` 408 | [ 409 | { id: 1, name: 'Joey', address: "Joy's Home" }, 410 | { id: 2, name: 'Hanna', address: "Hanna's Home" }, 411 | { id: 3, name: 'Mary', address: "Marry's Home" } 412 | ] 413 | ``` 414 | 415 | You can follow the `join` action with `where`, `limit` and `orderBy` as well: 416 | 417 | ```ts 418 | // join where order and limit 419 | const people7 = await db.tables.Person.join( 420 | t => ({ 421 | // FROM Person AS self JOIN Address AS address 422 | address: t.Address 423 | }), 424 | (p, { address }) => { 425 | // ON self.id = address.person 426 | p.equal({ id: address.person }) 427 | } 428 | ) 429 | .map(f => ({ 430 | // SELECT self.id AS id, self.name AS name, address.address AS address 431 | id: f.self.id, 432 | name: f.self.name, 433 | address: f.address.address 434 | })) 435 | // WHERE self.married = 1 436 | .where(p => p.self.equals({ married: true })) 437 | // ORDER BY address.address ASC 438 | .orderBy({ address: { address: 'ASC' } }) 439 | // LIMIT 1 440 | .limit(1) 441 | ``` 442 | 443 | result: 444 | 445 | ``` 446 | [{ id: 2, name: 'Hanna', address: "Hanna's Home" }] 447 | ``` 448 | 449 | #### Delete 450 | 451 | ```ts 452 | // delete 453 | const delResult = await db.tables.Person.delete().where(c => 454 | c.equals({ id: 3 }) 455 | ) 456 | ``` 457 | 458 | result: 459 | 460 | ``` 461 | { 462 | insertId: 3, 463 | rowsAffected: 1 464 | } 465 | ``` 466 | 467 | You can put `delete` action under `transaction`. 468 | 469 | #### Drop 470 | 471 | ```ts 472 | // drop 473 | await db.tables.Address.drop() 474 | 475 | // or drop inside transaction 476 | await db.transaction(({ exec, tables }) => { 477 | exec(tables.Address.drop()) 478 | exec(tables.Person.drop()) 479 | }) 480 | 481 | // or drop all tables 482 | await db.dropAllTables() 483 | ``` 484 | 485 | ## License 486 | 487 | MIT 488 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlite-ts", 3 | "version": "0.1.0", 4 | "description": "SQLite ORM for Typescript", 5 | "main": "dist/index.js", 6 | "typings": "typings/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "typings" 10 | ], 11 | "scripts": { 12 | "test": "jest", 13 | "lint": "tslint 'src/**/*.ts{,x}'", 14 | "compile": "tsc -d --declarationDir ./typings", 15 | "build": "rm -rf typings && rm -rf dist && npm run compile && prettier ./typings/**/*.d.ts --write" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/budiadiono/sqlite-ts.git" 20 | }, 21 | "keywords": [ 22 | "ORM", 23 | "SQLite", 24 | "ReactNative", 25 | "Expo", 26 | "Typescript" 27 | ], 28 | "author": "Budi Adiono ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/budiadiono/sqlite-ts/issues" 32 | }, 33 | "homepage": "https://github.com/budiadiono/sqlite-ts#readme", 34 | "peerDependencies": {}, 35 | "devDependencies": { 36 | "@types/expo": "^30.0.0", 37 | "@types/jest": "^23.3.5", 38 | "@types/node": "^10.9.4", 39 | "@types/react-native-sqlite-storage": "^3.3.1", 40 | "@types/sqlite3": "^3.1.3", 41 | "jest": "^23.6.0", 42 | "prettier": "^1.14.2", 43 | "sqlite3": "^4.0.3", 44 | "ts-jest": "^23.10.4", 45 | "tslib": "^1.9.3", 46 | "tslint": "^5.11.0", 47 | "tslint-config-prettier": "^1.15.0", 48 | "typescript": "^3.0.3" 49 | }, 50 | "dependencies": { 51 | "reflect-metadata": "^0.1.12" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version=$1 4 | echo 5 | 6 | echo "Releasing sqlite-ts version $version..." 7 | echo 8 | 9 | npm run build 10 | echo 11 | 12 | read -p "Are you sure want to release version $version? (y/n)" -n 1 -r 13 | if [[ $REPLY =~ ^[Yy]$ ]] 14 | then 15 | echo 16 | echo "Releasing $version now..." 17 | 18 | # stage all changes and commit 19 | git commit -am "v$version" 20 | 21 | # update npm version 22 | # commit also tagged here 23 | npm version $version --message "Release version $version" 24 | 25 | # push to repo 26 | git push origin master --tags 27 | echo 28 | echo "Publishing to npm..." 29 | npm publish 30 | echo 31 | echo "Done! -- published to https://www.npmjs.com/package/sqlite-ts" 32 | fi -------------------------------------------------------------------------------- /src/column.ts: -------------------------------------------------------------------------------- 1 | import { ColumnTypes } from './types' 2 | 3 | export const COLUMN_META_KEY = 'table:column' 4 | 5 | export function Column(type: ColumnTypes, size?: number) { 6 | return Reflect.metadata(COLUMN_META_KEY, { type, size }) 7 | } 8 | -------------------------------------------------------------------------------- /src/condition.ts: -------------------------------------------------------------------------------- 1 | import { ColumnInfo } from './types' 2 | import { Utils } from './utils' 3 | 4 | export class Condition { 5 | _sql: string[] = [] 6 | 7 | alias?: string 8 | descriptor: {} 9 | columns: { [key: string]: { primary: boolean } & ColumnInfo } = {} 10 | 11 | constructor( 12 | descriptor: {}, 13 | columns: { [key: string]: { primary: boolean } & ColumnInfo }, 14 | alias?: string 15 | ) { 16 | this.descriptor = descriptor 17 | this.columns = columns 18 | this.alias = alias 19 | } 20 | 21 | equals(p: { [key in keyof Partial]: any }) { 22 | Object.keys(p).map(k => { 23 | this._sql.push( 24 | `${this._thisField(k)} = ${Utils.asValue( 25 | this.columns[k].type, 26 | (p as any)[k] 27 | )}` 28 | ) 29 | }) 30 | return this 31 | } 32 | 33 | notEquals(p: { [key in keyof Partial]: any }) { 34 | Object.keys(p).map(k => { 35 | this._sql.push( 36 | `${this._thisField(k)} <> ${Utils.asValue( 37 | this.columns[k].type, 38 | (p as any)[k] 39 | )}` 40 | ) 41 | }) 42 | return this 43 | } 44 | 45 | greaterThan(p: { [key in keyof Partial]: any }) { 46 | Object.keys(p).map(k => { 47 | this._sql.push( 48 | `${this._thisField(k)} > ${Utils.asValue( 49 | this.columns[k].type, 50 | (p as any)[k] 51 | )}` 52 | ) 53 | }) 54 | return this 55 | } 56 | 57 | greaterThanOrEqual(p: { [key in keyof Partial]: any }) { 58 | Object.keys(p).map(k => { 59 | this._sql.push( 60 | `${this._thisField(k)} >= ${Utils.asValue( 61 | this.columns[k].type, 62 | (p as any)[k] 63 | )}` 64 | ) 65 | }) 66 | return this 67 | } 68 | 69 | lessThan(p: { [key in keyof Partial]: any }) { 70 | Object.keys(p).map(k => { 71 | this._sql.push( 72 | `${this._thisField(k)} < ${Utils.asValue( 73 | this.columns[k].type, 74 | (p as any)[k] 75 | )}` 76 | ) 77 | }) 78 | return this 79 | } 80 | 81 | lessThanOrEqual(p: { [key in keyof Partial]: any }) { 82 | Object.keys(p).map(k => { 83 | this._sql.push( 84 | `${this._thisField(k)} <= ${Utils.asValue( 85 | this.columns[k].type, 86 | (p as any)[k] 87 | )}` 88 | ) 89 | }) 90 | return this 91 | } 92 | 93 | in(p: { [key in keyof Partial]: any[] }) { 94 | Object.keys(p).map(k => { 95 | this._sql.push( 96 | `${this._thisField(k)} IN (${(p as any)[k] 97 | .map((v: any) => Utils.asValue(this.columns[k].type, v)) 98 | .join(', ')})` 99 | ) 100 | }) 101 | return this 102 | } 103 | 104 | between(p: { [key in keyof Partial]: any[] }) { 105 | Object.keys(p).map(k => { 106 | const val = (p as any)[k] 107 | const colType = this.columns[k].type 108 | this._sql.push( 109 | `${this._thisField(k)} BETWEEN ${Utils.asValue( 110 | colType, 111 | val[0] 112 | )} AND ${Utils.asValue(colType, val[1])}` 113 | ) 114 | }) 115 | return this 116 | } 117 | 118 | contains(p: { [key in keyof Partial]: string }) { 119 | Object.keys(p).map(k => { 120 | this._sql.push(`${this._thisField(k)} LIKE '${(p as any)[k]}'`) 121 | }) 122 | return this 123 | } 124 | 125 | startsWith(p: { [key in keyof Partial]: string }) { 126 | Object.keys(p).map(k => { 127 | this._sql.push(`${this._thisField(k)} LIKE '${(p as any)[k]}%'`) 128 | }) 129 | return this 130 | } 131 | 132 | endsWith(p: { [key in keyof Partial]: string }) { 133 | Object.keys(p).map(k => { 134 | this._sql.push(`${this._thisField(k)} LIKE '%${(p as any)[k]}'`) 135 | }) 136 | return this 137 | } 138 | 139 | get or() { 140 | this._sql.push('OR') 141 | return this 142 | } 143 | 144 | group(fn: (c: Condition) => any) { 145 | this._sql.push('(') 146 | fn(this) 147 | this._sql.push(')') 148 | return this 149 | } 150 | 151 | field(fn: (k: T) => void) { 152 | return 'field:' + fn.apply(this, [this.descriptor]) 153 | } 154 | 155 | sql() { 156 | const sql = [] 157 | let idx = 0 158 | for (const s of this._sql) { 159 | sql.push(s) 160 | if (idx < this._sql.length - 1) { 161 | const next = this._sql[idx + 1] 162 | if ( 163 | s !== 'OR' && 164 | s !== '(' && 165 | // s !== ')' && 166 | (next !== ')' && next !== 'OR') 167 | ) { 168 | sql.push('AND') 169 | } 170 | } 171 | 172 | idx++ 173 | } 174 | return sql.join(' ') 175 | } 176 | 177 | protected _thisField(field: string) { 178 | if (this.alias) { 179 | return `${Utils.quote(this.alias)}.${Utils.quote(field)}` 180 | } 181 | 182 | return Utils.quote(field) 183 | } 184 | } 185 | 186 | export type ConditionFunction = (condition: Condition) => Condition 187 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import { Scope, TransactionScope } from './scope' 2 | import { Table } from './table' 3 | import { ConstructorClass, DbDriver, KeyVal, ResultSet, ValueOf } from './types' 4 | 5 | export interface DbConfig { 6 | driver: DbDriver 7 | entities: TModels 8 | createTables?: boolean 9 | } 10 | 11 | export type Tables = { 12 | [P in keyof T]: Table, ConstructorClass>, T> 13 | } 14 | 15 | export class Db { 16 | /** 17 | * Create new database instance. 18 | * @param config Database Configuration 19 | */ 20 | static async init(config: DbConfig): Promise> { 21 | const _db = new Db() 22 | 23 | if (config.driver.init) { 24 | await config.driver.init() 25 | } 26 | 27 | _db.config = config 28 | _db.driver = config.driver 29 | 30 | try { 31 | _db.tables = _db.buildTables() 32 | if (config.createTables || config.createTables === undefined) { 33 | await _db.createAllTables() 34 | } 35 | return _db 36 | } catch (error) { 37 | throw new Error(error) 38 | } 39 | } 40 | 41 | config!: DbConfig 42 | driver!: DbDriver 43 | tables!: Tables 44 | 45 | constructor() { 46 | this.transaction = this.transaction.bind(this) 47 | } 48 | 49 | async close(): Promise { 50 | return this.config.driver.close() 51 | } 52 | 53 | buildTables(): Tables { 54 | const tables: KeyVal = {} 55 | const cls = this.config.entities as any 56 | 57 | for (const key of Object.keys(cls)) { 58 | const table = new Table(cls[key] as any, key, this) 59 | tables[key] = table 60 | } 61 | 62 | return tables as any 63 | } 64 | 65 | transaction( 66 | scope: (transacionScope: TransactionScope) => void 67 | ): Promise { 68 | return new Promise((resolve, reject) => { 69 | const resultSets: ResultSet[] = [] 70 | this.driver.transaction( 71 | tx => { 72 | const { tables, exec } = new Scope(this, tx) 73 | scope({ tables, exec }) 74 | }, 75 | reject, 76 | () => { 77 | resolve(resultSets) 78 | } 79 | ) 80 | }) 81 | } 82 | 83 | exec(sql: string, args?: any[]): Promise { 84 | return new Promise((resolve, reject) => { 85 | this.driver.transaction(tx => { 86 | tx.execSql( 87 | sql, 88 | args, 89 | res => { 90 | resolve(this.driver.getQueryResult(res)) 91 | }, 92 | reject 93 | ) 94 | }) 95 | }) 96 | } 97 | 98 | query(sql: string, args?: any[]): Promise { 99 | return new Promise((resolve, reject) => { 100 | this.driver.query(sql, args || [], reject, res => { 101 | resolve(this.driver.getQueryResult(res).rows.items()) 102 | }) 103 | }) 104 | } 105 | 106 | single(sql: string, args?: any[]): Promise { 107 | return new Promise((resolve, reject) => { 108 | this.driver.query(sql, args || [], reject, res => { 109 | resolve(this.driver.getQueryResult(res).rows.item(0)) 110 | }) 111 | }) 112 | } 113 | 114 | async dropAllTables() { 115 | return this.transaction(({ tables, exec }) => { 116 | for (const key of Object.keys(tables)) { 117 | exec(tables[key].drop()) 118 | } 119 | }) 120 | } 121 | 122 | async createAllTables() { 123 | return this.transaction(({ tables, exec }) => { 124 | for (const key of Object.keys(tables)) { 125 | exec(tables[key].create()) 126 | } 127 | }) 128 | } 129 | 130 | async generateBackupSql() { 131 | const sqlCreates: string[] = [] 132 | const sqlInserts: string[] = [] 133 | 134 | for (const key of Object.keys(this.tables)) { 135 | const tbl = this.tables[key] 136 | tbl.create() 137 | sqlCreates.push(tbl.sql) 138 | sqlInserts.push(await tbl.buildBackupSql()) 139 | } 140 | return ( 141 | sqlCreates.join('\r\n') + 142 | '\r\n' + 143 | sqlInserts.filter(s => s !== '').join('\r\n') 144 | ) 145 | } 146 | 147 | async restoreFromSql(sql: string, recreate?: boolean) { 148 | if (recreate) { 149 | await this.dropAllTables() 150 | } 151 | 152 | await this.transaction(({ exec }) => { 153 | const sqls = sql.split('\r\n') 154 | for (const s of sqls) { 155 | if (s.startsWith('CREATE')) { 156 | if (recreate) { 157 | exec(s) 158 | } 159 | } else if (s.startsWith('INSERT')) { 160 | exec(s) 161 | } 162 | } 163 | }) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/dialects/base/index.ts: -------------------------------------------------------------------------------- 1 | import { Condition, ConditionFunction } from '../../condition' 2 | import { KeyVal, ResultSet, TableInfo } from '../../types' 3 | import { Utils } from '../../utils' 4 | 5 | export enum DialectKind { 6 | READ = 0, 7 | SINGLE = 1, 8 | COUNT = 2, 9 | ANY = 3, 10 | WRITE = 4, 11 | JOIN = 5 12 | } 13 | 14 | export interface IDialectBase extends Promise { 15 | sql: string 16 | } 17 | 18 | export class DialectBase< 19 | M, 20 | R = M[] | M | ResultSet | undefined | number | boolean 21 | > implements IDialectBase { 22 | public sql: string; 23 | 24 | [Symbol.toStringTag]: 'Promise' 25 | 26 | protected info: TableInfo 27 | protected kind: DialectKind = DialectKind.READ 28 | protected aliases?: KeyVal> 29 | protected map?: any 30 | 31 | private res: ((value?: R | PromiseLike) => void) | undefined 32 | private rej: ((reason?: any) => void) | undefined 33 | private readonly promise: Promise 34 | 35 | constructor(info: TableInfo) { 36 | this.promise = new Promise((resolve, reject) => { 37 | this.res = resolve 38 | this.rej = reject 39 | }) 40 | 41 | this.sql = '' 42 | this.info = info 43 | } 44 | public then( 45 | resolve?: 46 | | ((value: R) => TResult1 | PromiseLike) 47 | | null 48 | | undefined, 49 | reject?: 50 | | ((reason: any) => TResult2 | PromiseLike) 51 | | null 52 | | undefined 53 | ): Promise { 54 | switch (this.kind) { 55 | case DialectKind.READ: 56 | return this._query(this.sql).then(results => { 57 | if (resolve) { 58 | resolve(results as any) 59 | } 60 | }, reject) as any 61 | 62 | case DialectKind.JOIN: 63 | if (!this.aliases || !this.map) { 64 | throw new Error('Join statement needs column alias and map object') 65 | } 66 | 67 | return this._query(this.sql, this.aliases, this.map).then(results => { 68 | if (resolve) { 69 | resolve(results as any) 70 | } 71 | }, reject) as any 72 | 73 | case DialectKind.SINGLE: 74 | if (!this.sql.endsWith('LIMIT 1')) { 75 | this.sql += ' LIMIT 1' 76 | } 77 | return this._single(this.sql).then(results => { 78 | if (resolve) { 79 | resolve(results as any) 80 | } 81 | }, reject) as any 82 | 83 | case DialectKind.COUNT: 84 | return this._count(this.sql).then(results => { 85 | if (resolve) { 86 | resolve(results as any) 87 | } 88 | }, reject) as any 89 | 90 | case DialectKind.ANY: 91 | return this._any(this.sql).then(results => { 92 | if (resolve) { 93 | resolve(results as any) 94 | } 95 | }, reject) as any 96 | 97 | case DialectKind.WRITE: 98 | return this._exec(this.sql).then(results => { 99 | if (resolve) { 100 | resolve(results as any) 101 | } 102 | }, reject) as any 103 | } 104 | 105 | throw new Error('Dialect not resolved') 106 | } 107 | 108 | public catch( 109 | onRejected?: (reason: any) => PromiseLike 110 | ): Promise { 111 | return this.promise.catch(onRejected) 112 | } 113 | 114 | public resolve(value?: R | PromiseLike): void { 115 | if (this.res) { 116 | return this.res(value) 117 | } 118 | } 119 | 120 | reject(reason?: any): void { 121 | if (this.rej) { 122 | return this.rej(reason) 123 | } 124 | } 125 | 126 | /** 127 | * Build SELECT sql from function. 128 | * @param fn Fields selection function. eg: p => p.foo or p => [ p.foo, p.bar ]. 129 | */ 130 | protected _select(fn: (k: M) => void) { 131 | const result = fn.call(this, this.info.descriptor) 132 | const fields: string[] = 133 | result instanceof Array ? result : result.split(',') 134 | return this._buildSelectFields(fields) 135 | } 136 | 137 | /** 138 | * Build selected fields for SELECT statement. 139 | * @param fields selected fields. 140 | */ 141 | protected _buildSelectFields(fields: string[]) { 142 | const selected: string[] = [] 143 | 144 | fields.forEach(k => { 145 | selected.push(Utils.selectAs(this.info.columns[k], k)) 146 | }) 147 | return selected.join(',') 148 | } 149 | 150 | /** 151 | * Build SQL statement from condition function. 152 | * @param fn Condition function. 153 | */ 154 | protected _condSql(fn: ConditionFunction) { 155 | return fn(new Condition(this.info.descriptor, this.info.columns)).sql() 156 | } 157 | 158 | /** 159 | * Map raw entity result to actual entity type. 160 | * @param raw Raw entity result (from query result). 161 | */ 162 | protected _mapResult(raw: T): T | undefined { 163 | if (!raw) { 164 | return undefined 165 | } 166 | 167 | const { columns } = this.info 168 | 169 | const obj = {} 170 | Object.keys(raw).forEach(k => { 171 | if (columns[k]) { 172 | obj[k] = Utils.asResult(columns[k].type, raw[k] as any) 173 | } else { 174 | obj[k] = raw[k] 175 | } 176 | }) 177 | return obj as T 178 | } 179 | 180 | protected _mapResultWithAlias( 181 | raw: T, 182 | aliases: KeyVal> 183 | ): T | undefined { 184 | if (!raw) { 185 | return undefined 186 | } 187 | 188 | const obj = {} 189 | Object.keys(raw).forEach(k => { 190 | const key = k.split('___') 191 | const { columns } = aliases[key[0]] 192 | 193 | if (columns[key[1]]) { 194 | obj[k] = Utils.asResult(columns[key[1]].type, raw[k] as any) 195 | } else { 196 | obj[k] = raw[k] 197 | } 198 | }) 199 | 200 | return obj as T 201 | } 202 | 203 | /** 204 | * Execute SQL statement. 205 | * @param sql SQL statement. 206 | */ 207 | protected _exec(sql: string): Promise { 208 | return this.info.db.exec(sql) 209 | } 210 | 211 | /** 212 | * Execute SQL statement as single query that returns single entity object. 213 | * @param sql SQL statement. 214 | */ 215 | protected async _single( 216 | sql: string 217 | ): Promise { 218 | return this._mapResult(await this.info.db.single(sql)) 219 | } 220 | 221 | /** 222 | * Execute SQL statement as normal query that returns list of entity object. 223 | * @param sql SQL statement. 224 | */ 225 | protected async _query( 226 | sql: string, 227 | aliases?: KeyVal>, 228 | map?: TResult 229 | ): Promise { 230 | const data = await this.info.db.query(sql) 231 | 232 | if (aliases) { 233 | if (!map) { 234 | throw new Error('Alias needs map') 235 | } 236 | 237 | // build results as match with map object 238 | const translate = (o: {} & TResult, dict: {}) => { 239 | const res = { ...(o as {}) } 240 | for (const k of Object.keys(o)) { 241 | const val = o[k] 242 | 243 | if (typeof val === 'string') { 244 | // val is alias? 245 | res[k] = dict[val] 246 | } else { 247 | // val is nested object 248 | res[k] = translate(val, dict) 249 | } 250 | } 251 | return res as TResult 252 | } 253 | 254 | return data.map(d => 255 | translate(map, this._mapResultWithAlias(d, aliases) as TResult) 256 | ) 257 | } 258 | 259 | // return flat entity 260 | return data.map(d => this._mapResult(d) as TResult) 261 | } 262 | 263 | /** 264 | * Execute SQL statement as count query that returns the number data. 265 | * @param sql SQL statement. 266 | */ 267 | protected async _count(sql: string): Promise { 268 | return (await this._single(sql)).count 269 | } 270 | 271 | /** 272 | * Execute SQL statement as count query and returns true if data number is found otherwise false. 273 | * @param sql SQL statement. 274 | */ 275 | protected async _any(sql: string): Promise { 276 | return (await this._count(sql)) > 0 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/dialects/base/read.ts: -------------------------------------------------------------------------------- 1 | import { ConditionFunction } from '../../condition' 2 | import { KeyVal, TableInfo } from '../../types' 3 | import { Utils } from '../../utils' 4 | import { DialectBase, DialectKind, IDialectBase } from './' 5 | 6 | export interface IDialectLimitOffset { 7 | limit(limit: number, offset?: number): IDialectBase 8 | } 9 | 10 | export interface IDialectOrderBy { 11 | orderBy( 12 | order?: { [P in keyof M]?: 'ASC' | 'DESC' } 13 | ): IDialectLimitOffset & IDialectBase 14 | } 15 | 16 | export interface IDialectWhere { 17 | where( 18 | condition?: ConditionFunction 19 | ): IDialectBase & IDialectOrderBy 20 | } 21 | 22 | export class ReadDialect extends DialectBase { 23 | constructor( 24 | info: TableInfo, 25 | sql: string, 26 | kind: DialectKind, 27 | aliases?: KeyVal>, 28 | map?: any 29 | ) { 30 | super(info) 31 | this.kind = kind 32 | this.sql = sql 33 | this.aliases = aliases 34 | this.map = map 35 | } 36 | 37 | public limit(limit: number, offset?: number): IDialectBase { 38 | this.sql += ` LIMIT ${limit}` 39 | if (offset !== undefined) { 40 | this.sql += ` OFFSET ${offset}` 41 | } 42 | 43 | return this 44 | } 45 | 46 | public orderBy( 47 | order?: { [P in keyof M]?: 'ASC' | 'DESC' } 48 | ): IDialectLimitOffset & IDialectBase { 49 | if (order) { 50 | this.sql += 51 | ' ORDER BY ' + 52 | Object.keys(order) 53 | .map(k => `${Utils.quote(k)} ${(order as any)[k]}`) 54 | .join(' ') 55 | } 56 | 57 | return this 58 | } 59 | 60 | public where( 61 | condition?: ConditionFunction 62 | ): IDialectOrderBy & IDialectLimitOffset & IDialectBase { 63 | if (condition) { 64 | this.sql += ` WHERE ${this._condSql(condition)}` 65 | } 66 | 67 | return this 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/dialects/base/readAlias.ts: -------------------------------------------------------------------------------- 1 | import { DialectBase, DialectKind, IDialectBase } from '.' 2 | import { Condition } from '../../condition' 3 | import { KeyVal, TableInfo, ValueOf } from '../../types' 4 | import { Utils } from '../../utils' 5 | 6 | export interface IDialectOrderByAlias extends IDialectBase { 7 | orderBy( 8 | order: { 9 | [key in keyof Partial]: { 10 | [P in keyof Partial>]: 'ASC' | 'DESC' 11 | } 12 | } & { 13 | self?: { [P in keyof M]?: 'ASC' | 'DESC' } 14 | } 15 | ): IDialectLimitAliasOffset 16 | } 17 | 18 | export interface IDialectWhereAlias extends IDialectBase { 19 | where( 20 | fields: ( 21 | left: { [key in keyof Q]: Condition> } & { 22 | self: Condition 23 | }, 24 | right: { [key in keyof Q]: ValueOf } & { 25 | self: M 26 | } 27 | ) => TResult 28 | ): IDialectOrderByAlias & IDialectLimitAliasOffset 29 | } 30 | 31 | export interface IDialectLimitAliasOffset extends IDialectBase { 32 | limit(limit: number, offset?: number): IDialectBase 33 | } 34 | 35 | export class ReadAliasDialect extends DialectBase 36 | implements 37 | IDialectOrderByAlias, 38 | IDialectWhereAlias, 39 | IDialectLimitAliasOffset { 40 | constructor( 41 | info: TableInfo, 42 | sql: string, 43 | kind: DialectKind, 44 | aliases: KeyVal>, 45 | map: any 46 | ) { 47 | super(info) 48 | this.kind = kind 49 | this.sql = sql 50 | this.aliases = aliases 51 | this.map = map 52 | } 53 | 54 | public limit(limit: number, offset?: number): IDialectBase { 55 | this.sql += ` LIMIT ${limit}` 56 | if (offset !== undefined) { 57 | this.sql += ` OFFSET ${offset}` 58 | } 59 | 60 | return this 61 | } 62 | 63 | public orderBy( 64 | order: { 65 | [key in keyof Partial]: { 66 | [P in keyof Partial>]: 'ASC' | 'DESC' 67 | } 68 | } & { 69 | self?: { [P in keyof M]?: 'ASC' | 'DESC' } 70 | } 71 | ): IDialectLimitAliasOffset { 72 | if (!this.aliases) { 73 | throw new Error('Alias needed to build WHERE statement.') 74 | } 75 | 76 | let sql = ' ORDER BY' 77 | for (const k of Object.keys(order)) { 78 | const fOrder = order[k] 79 | for (const o of Object.keys(fOrder)) { 80 | sql += ` ${Utils.quote(k)}.${Utils.quote(o)} ${fOrder[o]}` 81 | } 82 | } 83 | 84 | this.sql += sql 85 | 86 | return this 87 | } 88 | 89 | public where( 90 | fields: ( 91 | left: { [key in keyof Q]: Condition> } & { 92 | self: Condition 93 | }, 94 | right: { [key in keyof Q]: ValueOf } & { 95 | self: M 96 | } 97 | ) => TResult 98 | ): IDialectOrderByAlias & IDialectLimitAliasOffset { 99 | if (!this.aliases) { 100 | throw new Error('Alias needed to build WHERE statement.') 101 | } 102 | 103 | const rightDescriptor = {} 104 | const leftDescriptor: KeyVal> = {} 105 | 106 | for (const k of Object.keys(this.aliases)) { 107 | const a = this.aliases[k] 108 | leftDescriptor[k] = new Condition(a.descriptor, a.columns, k) 109 | rightDescriptor[k] = {} 110 | for (const c of Object.keys(a.columns)) { 111 | rightDescriptor[k][c] = `field:${k}.${c}` 112 | } 113 | } 114 | 115 | fields(leftDescriptor as any, rightDescriptor as any) 116 | const sqls: string[] = [] 117 | for (const k of Object.keys(leftDescriptor)) { 118 | sqls.push(leftDescriptor[k].sql()) 119 | } 120 | 121 | this.sql += 'WHERE ' + sqls.filter(s => s.trim() !== '').join(' AND ') 122 | 123 | return this 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/dialects/base/where.ts: -------------------------------------------------------------------------------- 1 | import { DialectBase, DialectKind, IDialectBase } from '.' 2 | import { ConditionFunction } from '../../condition' 3 | import { TableInfo } from '../../types' 4 | 5 | export class WhereDialect extends DialectBase { 6 | constructor(info: TableInfo, sql: string, kind: DialectKind) { 7 | super(info) 8 | this.kind = kind 9 | this.sql = sql 10 | } 11 | 12 | where(condition?: ConditionFunction): IDialectBase { 13 | if (condition) { 14 | this.sql += ` WHERE ${this._condSql(condition)}` 15 | } 16 | return this 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/dialects/index.ts: -------------------------------------------------------------------------------- 1 | import { KeyVal, ResultSet, TableInfo, ValueOf } from '../types' 2 | import { Utils } from '../utils' 3 | import { DialectBase, DialectKind, IDialectBase } from './base' 4 | import { IDialectWhere, ReadDialect } from './base/read' 5 | import { WhereDialect } from './base/where' 6 | import { JoinCondition, JoinMapper } from './join' 7 | 8 | export class Dialect extends DialectBase { 9 | constructor(info: TableInfo) { 10 | super(info) 11 | this.update = this.update.bind(this) 12 | } 13 | 14 | create(): IDialectBase { 15 | const { name, columns } = this.info 16 | 17 | const cols = Object.keys(columns).map(key => { 18 | const column = columns[key] 19 | const colType = Utils.getRealColumnType(key, column) 20 | const colPrimary = column.primary ? ' PRIMARY KEY' : '' 21 | return `${Utils.quote(key)} ${colType}${colPrimary}` 22 | }) 23 | 24 | this.sql = `CREATE TABLE IF NOT EXISTS ${Utils.quote(name)} (${cols.join( 25 | ', ' 26 | )})` 27 | 28 | this.kind = DialectKind.WRITE 29 | 30 | return this 31 | } 32 | 33 | drop(): IDialectBase { 34 | this.sql = `DROP TABLE IF EXISTS ${Utils.quote(this.info.name)}` 35 | this.kind = DialectKind.WRITE 36 | return this 37 | } 38 | 39 | select(fields?: ((k: T) => void) | '*'): ReadDialect { 40 | this.kind = DialectKind.READ 41 | return this.__select(fields) 42 | } 43 | 44 | single(fields?: ((k: T) => void) | '*'): ReadDialect { 45 | this.kind = DialectKind.SINGLE 46 | return this.__select(fields) 47 | } 48 | 49 | join>( 50 | selector: (tbl: TDb) => Q, 51 | clause: ( 52 | self: JoinCondition & T, 53 | other: { 54 | [key in keyof Q]: JoinCondition> & ValueOf 55 | } 56 | ) => void 57 | ): JoinMapper { 58 | const result = JoinCondition.buildSql(this.info, selector, clause) 59 | return new JoinMapper(this.info, result.tables, result.sql) 60 | } 61 | 62 | count(): IDialectWhere { 63 | this.sql = `SELECT COUNT(*) as count FROM ${Utils.quote(this.info.name)}` 64 | return new ReadDialect(this.info, this.sql, DialectKind.COUNT) 65 | } 66 | 67 | any(): IDialectWhere { 68 | this.sql = `SELECT COUNT(*) as count FROM ${Utils.quote(this.info.name)}` 69 | return new ReadDialect(this.info, this.sql, DialectKind.ANY) 70 | } 71 | 72 | insert(set: Partial, upsert?: boolean): IDialectBase 73 | insert(sets: Array>): IDialectBase 74 | insert( 75 | set: Partial | Array>, 76 | upsert?: boolean 77 | ): IDialectBase { 78 | this.kind = DialectKind.WRITE 79 | if (set instanceof Array) { 80 | return this.__insertMany(set) 81 | } 82 | 83 | return this.__insert(set, upsert) 84 | } 85 | 86 | upsert(set: Partial): IDialectBase { 87 | return this.insert(set, true) 88 | } 89 | 90 | update(set: Partial): WhereDialect { 91 | const sql = `UPDATE ${Utils.quote(this.info.name)} SET ${Object.keys(set) 92 | .map( 93 | k => 94 | `${Utils.quote(k)} = ${Utils.asValue( 95 | this.info.columns[k].type, 96 | (set as any)[k] 97 | )}` 98 | ) 99 | .join(', ')}` 100 | 101 | this.sql = sql 102 | return new WhereDialect(this.info, this.sql, DialectKind.WRITE) 103 | } 104 | 105 | delete(): WhereDialect { 106 | this.sql = `DELETE FROM ${Utils.quote(this.info.name)}` 107 | return new WhereDialect(this.info, this.sql, DialectKind.WRITE) 108 | } 109 | 110 | protected __select(fields?: ((k: T) => void) | '*'): ReadDialect { 111 | let sql: string 112 | if (!fields || fields === '*') { 113 | sql = this._buildSelectFields(Object.keys(this.info.columns)) 114 | } else { 115 | sql = this._select(fields) 116 | } 117 | 118 | this.sql = `SELECT ${sql} FROM ${Utils.quote(this.info.name)}` 119 | 120 | return new ReadDialect(this.info, this.sql, this.kind) 121 | } 122 | 123 | protected __insert( 124 | set: Partial | Array>, 125 | upsert?: boolean 126 | ): IDialectBase { 127 | const fields = [] 128 | const values = [] 129 | 130 | for (const key of Object.keys(set)) { 131 | fields.push(key) 132 | 133 | values.push( 134 | Utils.asValue(this.info.columns[key].type, (set as any)[key] as string) 135 | ) 136 | } 137 | 138 | const sqlUpsert = upsert ? ' OR REPLACE' : '' 139 | const sql = `INSERT${sqlUpsert} INTO ${Utils.quote( 140 | this.info.name 141 | )} (${fields.map(Utils.quote)}) VALUES (${values})` 142 | 143 | this.sql = sql 144 | return this 145 | } 146 | 147 | protected __insertMany(sets: Array>): IDialectBase { 148 | if (!sets.length) { 149 | throw new Error('No insert data defined.') 150 | } 151 | 152 | const fields: string[] = [] 153 | const set: any = sets.shift() 154 | const selectVal = [] 155 | for (const key of Object.keys(set)) { 156 | fields.push(key) 157 | selectVal.push( 158 | Utils.asValue(this.info.columns[key].type, (set as any)[ 159 | key 160 | ] as string) + 161 | ' AS ' + 162 | Utils.quote(key) 163 | ) 164 | } 165 | 166 | const unions = sets.map(s => { 167 | return `UNION ALL SELECT ${fields.map(k => 168 | Utils.asValue(this.info.columns[k].type, s[k]) 169 | )}` 170 | }) 171 | 172 | const sql = `INSERT INTO ${Utils.quote(this.info.name)} (${fields.map( 173 | Utils.quote 174 | )}) SELECT ${selectVal} ${unions.join(' ')}` 175 | 176 | this.sql = sql 177 | return this 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/dialects/join/condition.ts: -------------------------------------------------------------------------------- 1 | import { Table } from '../../table' 2 | import { KeyVal, TableInfo, ValueOf } from '../../types' 3 | import { buildJoinSql } from './sql' 4 | import { JoinStmt } from './types' 5 | 6 | export type JoinObject = { [key in string]: JoinCondition } & { 7 | sqls: JoinStmt[] 8 | } 9 | 10 | interface Result { 11 | sql: string 12 | tables: KeyVal 13 | } 14 | 15 | export class JoinCondition { 16 | static buildSql>( 17 | tableInfo: TableInfo, 18 | selector: (tbl: TDb) => Q, 19 | clause: ( 20 | self: JoinCondition & T, 21 | other: { 22 | [key in keyof Q]: JoinCondition> & ValueOf 23 | } 24 | ) => void 25 | ): Result { 26 | // get selected tables to be joined 27 | const tables = selector(tableInfo.db.tables as any) 28 | 29 | // prepare join conditions 30 | const condObj = this.buildConditionObject(tableInfo, tables) 31 | 32 | // invoke join clause against condition object 33 | clause(condObj.self as any, condObj as any) 34 | 35 | return { 36 | tables, 37 | sql: buildJoinSql(tables, condObj) 38 | } 39 | } 40 | 41 | static buildConditionObject( 42 | tableInfo: TableInfo, 43 | tables: KeyVal 44 | ): JoinObject { 45 | const obj = {} 46 | const stmts: JoinStmt[] = [] 47 | 48 | const pushSql = (stmt: JoinStmt) => { 49 | stmts.push(stmt) 50 | } 51 | 52 | tables.self = { 53 | info: tableInfo 54 | } 55 | 56 | // tslint:disable-next-line:no-string-literal 57 | obj['self'] = new JoinCondition(tables, 'self', pushSql) 58 | 59 | for (const k of Object.keys(tables)) { 60 | obj[k] = new JoinCondition(tables, k, pushSql) 61 | } 62 | 63 | return { ...obj, sqls: stmts as any } 64 | } 65 | 66 | private __alias: string 67 | private __push: (sql: JoinStmt) => void 68 | 69 | constructor( 70 | root: { [x: string]: Table }, 71 | name: string, 72 | push: (sql: JoinStmt) => void 73 | ) { 74 | // @ts-ignore 75 | const info = root[name].info 76 | 77 | for (const k of Object.keys(info.columns)) { 78 | this[k] = name + '.' + info.descriptor[k] 79 | } 80 | 81 | this.__alias = name 82 | this.__push = push 83 | } 84 | 85 | equal(p: { [key in keyof Partial]: any }) { 86 | for (const k of Object.keys(p)) { 87 | const val = (p[k] as string).split('.') 88 | 89 | this.__push({ 90 | left: { 91 | alias: this.__alias, 92 | column: k 93 | }, 94 | right: { 95 | alias: val[0], 96 | column: val[1] 97 | } 98 | }) 99 | } 100 | 101 | return this 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/dialects/join/index.ts: -------------------------------------------------------------------------------- 1 | export { JoinCondition, JoinObject } from './condition' 2 | export { JoinStmt } from './types' 3 | export { buildJoinSql } from './sql' 4 | export { JoinMapper } from './mapper' -------------------------------------------------------------------------------- /src/dialects/join/mapper.ts: -------------------------------------------------------------------------------- 1 | import { KeyVal, TableInfo, ValueOf } from '../../types' 2 | import { Utils } from '../../utils' 3 | import { DialectKind } from '../base' 4 | import { ReadAliasDialect } from '../base/readAlias' 5 | 6 | export class JoinMapper> { 7 | protected tableInfo: TableInfo 8 | protected sqlClause: string 9 | protected tables: KeyVal 10 | 11 | constructor(tableInfo: TableInfo, tables: KeyVal, sqlClause: string) { 12 | this.tableInfo = tableInfo 13 | this.tables = tables 14 | this.sqlClause = sqlClause 15 | } 16 | 17 | map( 18 | fields: ( 19 | column: { [key in keyof Q]: ValueOf } & { self: T } 20 | ) => TResult 21 | ) { 22 | const descriptor = {} 23 | const aliases: KeyVal> = {} 24 | 25 | // build descriptor and aliases 26 | for (const k of Object.keys(this.tables)) { 27 | const res = {} 28 | // @ts-ignore 29 | const info: TableInfo = this.tables[k].info 30 | for (const d of Object.keys(info.descriptor)) { 31 | res[d] = k + '___' + info.descriptor[d] 32 | } 33 | 34 | descriptor[k] = res 35 | aliases[k] = info 36 | } 37 | 38 | // build selected fields for SELECT statement 39 | const selectedFields: string[] = [] 40 | function getSelectedFields(m: {}) { 41 | for (const km of Object.keys(m)) { 42 | const r = m[km] 43 | if (typeof r === 'string') { 44 | const field = r.split('___') 45 | const toSelect = r.replace('___', '"."') 46 | 47 | const select = Utils.selectAs( 48 | aliases[field[0]].columns[field[1]], 49 | toSelect, 50 | r 51 | ) 52 | 53 | if (selectedFields.indexOf(select) < 0) { 54 | selectedFields.push(select) 55 | } 56 | } else { 57 | getSelectedFields(r) 58 | } 59 | } 60 | } 61 | 62 | // build map results 63 | const map = fields(descriptor as any) 64 | 65 | // get all columns to select 66 | getSelectedFields(map) 67 | 68 | // combine select join clause sql 69 | const sql = `SELECT ${selectedFields.join(',')} ${this.sqlClause}` 70 | 71 | return new ReadAliasDialect( 72 | this.tableInfo, 73 | sql, 74 | DialectKind.JOIN, 75 | aliases, 76 | map 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/dialects/join/sql.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from '../../utils' 2 | import { JoinObject } from './condition' 3 | import { JoinStmt } from './types' 4 | 5 | export function buildJoinSql(tables: T, joins: JoinObject) { 6 | let sql = '' 7 | const _regs: string[] = [] 8 | 9 | function tableName(name: string) { 10 | return tables[name].info.name 11 | } 12 | 13 | function registered(alias: string) { 14 | return _regs.indexOf(alias) > -1 15 | } 16 | 17 | function reg(alias: string) { 18 | if (registered(alias)) { 19 | return 20 | } 21 | 22 | _regs.push(alias) 23 | } 24 | 25 | let isLastJoin = false 26 | 27 | function sqlJoin(alias: string) { 28 | sql += ` JOIN ${Utils.quote(tableName(alias))} AS ${Utils.quote(alias)}` 29 | reg(alias) 30 | isLastJoin = true 31 | } 32 | 33 | function sqlOn(join: JoinStmt) { 34 | sql += ` ${isLastJoin ? 'ON' : 'AND'} ${Utils.quote( 35 | join.left.alias 36 | )}.${Utils.quote(join.left.column)} = ${Utils.quote( 37 | join.right.alias 38 | )}.${Utils.quote(join.right.column)} ` 39 | } 40 | 41 | for (const st of joins.sqls) { 42 | isLastJoin = false 43 | if (!_regs.length) { 44 | sql += ` FROM ${Utils.quote(tableName(st.left.alias))} AS ${Utils.quote( 45 | st.left.alias 46 | )}` 47 | reg(st.left.alias) 48 | 49 | sqlJoin(st.right.alias) 50 | } else { 51 | if (!registered(st.left.alias)) { 52 | sqlJoin(st.left.alias) 53 | } 54 | if (!registered(st.right.alias)) { 55 | sqlJoin(st.right.alias) 56 | } 57 | } 58 | sqlOn(st) 59 | } 60 | 61 | return sql 62 | } 63 | -------------------------------------------------------------------------------- /src/dialects/join/types.ts: -------------------------------------------------------------------------------- 1 | interface JoinField { 2 | alias: string 3 | column: string 4 | } 5 | 6 | export interface JoinStmt { 7 | left: JoinField 8 | right: JoinField 9 | } 10 | -------------------------------------------------------------------------------- /src/drivers/expo.ts: -------------------------------------------------------------------------------- 1 | import { SQLite } from 'expo' 2 | import { 3 | DbDriver, 4 | ErrorCallback, 5 | QueryCallback, 6 | ResultSet, 7 | Transaction 8 | } from '../types' 9 | 10 | export class ExpoSQLiteDriver implements DbDriver { 11 | db: SQLite.Database 12 | constructor(db: SQLite.Database) { 13 | this.db = db 14 | } 15 | 16 | transaction( 17 | scope: (trx: Transaction) => void, 18 | error?: (error: any) => void, 19 | success?: () => void 20 | ) { 21 | this.db.transaction( 22 | trx => 23 | scope({ 24 | execSql: (sql, args, resolve, reject) => { 25 | trx.executeSql( 26 | sql, 27 | args, 28 | (_, res) => { 29 | if (resolve) { 30 | resolve(res) 31 | } 32 | }, 33 | err => { 34 | if (reject) { 35 | reject(err) 36 | } 37 | } 38 | ) 39 | } 40 | }), 41 | error, 42 | success 43 | ) 44 | } 45 | 46 | query( 47 | sql: string, 48 | args: any[], 49 | error: ErrorCallback, 50 | success: QueryCallback 51 | ) { 52 | this.db.transaction(trx => { 53 | trx.executeSql( 54 | sql, 55 | args, 56 | (_, r) => { 57 | success(r) 58 | }, 59 | error 60 | ) 61 | }) 62 | } 63 | 64 | getQueryResult(result: SQLite.ResultSet): ResultSet { 65 | const { 66 | insertId, 67 | // @ts-ignore 68 | rowsAffected, 69 | rows: { _array, length } 70 | } = result 71 | return { 72 | insertId, 73 | rowsAffected, 74 | rows: { 75 | length, 76 | item: index => _array[index], 77 | items: () => _array 78 | } 79 | } 80 | } 81 | 82 | close(): Promise { 83 | return new Promise(resolve => resolve()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/drivers/native.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResultSet as SQLiteResultSet, 3 | SQLiteDatabase 4 | } from 'react-native-sqlite-storage' 5 | import { 6 | DbDriver, 7 | ErrorCallback, 8 | QueryCallback, 9 | ResultSet, 10 | Transaction 11 | } from '../types' 12 | 13 | export class ReactNativeSQLiteStorageDriver implements DbDriver { 14 | db: SQLiteDatabase 15 | constructor(db: SQLiteDatabase) { 16 | this.db = db 17 | } 18 | 19 | transaction( 20 | scope: (trx: Transaction) => void, 21 | error?: (error: any) => void, 22 | success?: () => void 23 | ) { 24 | this.db.transaction( 25 | trx => 26 | scope({ 27 | execSql: (sql, args, resolve, reject) => { 28 | trx.executeSql( 29 | sql, 30 | args, 31 | (_, res) => { 32 | if (resolve) { 33 | resolve(res) 34 | } 35 | }, 36 | err => { 37 | if (reject) { 38 | reject(err) 39 | } 40 | } 41 | ) 42 | } 43 | }), 44 | error, 45 | success 46 | ) 47 | } 48 | 49 | query( 50 | sql: string, 51 | args: any[], 52 | error: ErrorCallback, 53 | success: QueryCallback 54 | ) { 55 | this.db.executeSql( 56 | sql, 57 | args, 58 | r => { 59 | success(r) 60 | }, 61 | error 62 | ) 63 | } 64 | 65 | getQueryResult(result: SQLiteResultSet): ResultSet { 66 | const { 67 | insertId, 68 | rowsAffected, 69 | rows: { 70 | item, 71 | length, 72 | // @ts-ignore 73 | raw 74 | } 75 | } = result 76 | return { 77 | insertId, 78 | rowsAffected, 79 | rows: { 80 | item, 81 | length, 82 | items: raw 83 | } 84 | } 85 | } 86 | 87 | close(): Promise { 88 | return new Promise((resolve, reject) => { 89 | this.db 90 | .close() 91 | .then(resolve) 92 | .catch(reject) 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/drivers/sqlite3.ts: -------------------------------------------------------------------------------- 1 | import Sqlite3 from 'sqlite3' 2 | import { 3 | DbDriver, 4 | ErrorCallback, 5 | QueryCallback, 6 | ResultSet, 7 | Transaction 8 | } from '../types' 9 | 10 | interface SQLite3ResultSet { 11 | changes: number 12 | lastID: number 13 | results: any[] 14 | rowCount: number 15 | } 16 | 17 | export class SQLite3Driver implements DbDriver { 18 | db: Sqlite3.Database 19 | 20 | constructor(db: Sqlite3.Database) { 21 | this.db = db 22 | } 23 | 24 | transaction( 25 | scope: (tx: Transaction) => void, 26 | error?: ((error: any) => void), 27 | success?: (() => void) 28 | ): void { 29 | this.db.serialize(() => { 30 | this.db.run('BEGIN TRANSACTION') 31 | 32 | scope({ 33 | execSql: (sql, args, resolve, reject) => { 34 | this.db 35 | .prepare(sql) 36 | .run(args, function(err) { 37 | if (err) { 38 | if (reject) { 39 | reject(err) 40 | } 41 | 42 | return 43 | } 44 | 45 | if (resolve) { 46 | const result: SQLite3ResultSet = { 47 | changes: this.changes, 48 | lastID: this.lastID, 49 | results: [], 50 | rowCount: 0 51 | } 52 | 53 | resolve(result) 54 | } 55 | }) 56 | .finalize() 57 | } 58 | }) 59 | 60 | this.db.run('COMMIT TRANSACTION', [], err => { 61 | if (err) { 62 | if (error) { 63 | error(err) 64 | } 65 | return 66 | } 67 | 68 | if (success) { 69 | success() 70 | } 71 | }) 72 | }) 73 | } 74 | 75 | query( 76 | sql: string, 77 | args: any[], 78 | error: ErrorCallback, 79 | success: QueryCallback 80 | ): void { 81 | this.db.all(sql, args, (err, rows) => { 82 | if (err) { 83 | // @ts-ignore 84 | error(null, err) 85 | return 86 | } 87 | const result: SQLite3ResultSet = { 88 | changes: 0, 89 | lastID: 0, 90 | results: rows, 91 | rowCount: rows.length 92 | } 93 | 94 | success(result) 95 | }) 96 | } 97 | 98 | getQueryResult(result: SQLite3ResultSet): ResultSet { 99 | return { 100 | insertId: result.lastID, 101 | rows: { 102 | item: index => result.results[index], 103 | items: () => result.results, 104 | length: result.rowCount 105 | }, 106 | rowsAffected: result.changes 107 | } 108 | } 109 | 110 | close(): Promise { 111 | return new Promise((resolve, reject) => { 112 | this.db.close(error => { 113 | if (error) { 114 | reject(error) 115 | } else { 116 | resolve() 117 | } 118 | }) 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Db, Tables } from './db' 2 | export { Table } from './table' 3 | export { Column } from './column' 4 | export { Primary } from './primary' 5 | export { Utils } from './utils' 6 | export { ExpoSQLiteDriver } from './drivers/expo' 7 | export { ReactNativeSQLiteStorageDriver } from './drivers/native' 8 | export { SQLite3Driver } from './drivers/sqlite3' 9 | -------------------------------------------------------------------------------- /src/primary.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryKeyTypes } from './types' 2 | 3 | export const PRIMARY_META_KEY = 'table:primary' 4 | 5 | export function Primary(type: PrimaryKeyTypes = 'INTEGER', size?: number) { 6 | return Reflect.metadata(PRIMARY_META_KEY, { type, size }) 7 | } 8 | -------------------------------------------------------------------------------- /src/scope.ts: -------------------------------------------------------------------------------- 1 | import { Db, Tables } from './db' 2 | import { ResultSet, Transaction } from './types' 3 | 4 | export class Scope implements TransactionScope { 5 | db: Db 6 | transaction: Transaction 7 | tables: Tables 8 | 9 | constructor(db: Db, transaction: Transaction) { 10 | this.db = db 11 | this.transaction = transaction 12 | this.exec = this.exec.bind(this) 13 | this.tables = db.tables 14 | } 15 | 16 | exec(sql: any, args?: any[]): Promise 17 | exec(dialect: SqlDialect): Promise 18 | exec(param: string | SqlDialect, args?: any[]): Promise { 19 | const sql = typeof param === 'string' ? param : param.sql 20 | return new Promise((resolve, reject) => { 21 | this.transaction.execSql( 22 | sql, 23 | args, 24 | res => { 25 | resolve(this.db.driver.getQueryResult(res)) 26 | }, 27 | reject 28 | ) 29 | }) 30 | } 31 | } 32 | 33 | interface SqlDialect { 34 | sql: string 35 | } 36 | 37 | export interface TransactionScope { 38 | tables: Tables 39 | exec(sql: string, args?: any[]): Promise 40 | exec(dialect: SqlDialect): Promise 41 | } 42 | -------------------------------------------------------------------------------- /src/table.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import { COLUMN_META_KEY } from './column' 3 | import { Db } from './db' 4 | import { Dialect } from './dialects' 5 | import { PRIMARY_META_KEY } from './primary' 6 | import { ColumnInfo, ConstructorClass, TableInfo } from './types' 7 | import { Utils } from './utils' 8 | 9 | export class Table, TDb> extends Dialect< 10 | M, 11 | TDb 12 | > { 13 | static buildTableInfo>( 14 | db: Db, 15 | entity: T, 16 | name: string 17 | ): TableInfo { 18 | const table = new entity() 19 | 20 | const properties = Object.getOwnPropertyNames(table) 21 | const descriptor = {} 22 | const columns: { [key: string]: { primary: boolean } & ColumnInfo } = {} 23 | 24 | for (const key of properties) { 25 | ;(descriptor as any)[key] = key 26 | 27 | let primary = false 28 | let column = Reflect.getMetadata( 29 | COLUMN_META_KEY, 30 | table, 31 | key 32 | ) as ColumnInfo 33 | 34 | if (!column) { 35 | column = Reflect.getMetadata(PRIMARY_META_KEY, table, key) as ColumnInfo 36 | primary = true 37 | } 38 | 39 | if (column) { 40 | columns[key] = { 41 | primary, 42 | ...column 43 | } 44 | } 45 | } 46 | 47 | return { 48 | db, 49 | name, 50 | columns, 51 | descriptor 52 | } 53 | } 54 | 55 | constructor(entity: T, name: string, db: Db) { 56 | super(Table.buildTableInfo(db, entity, name)) 57 | this._mapResult = this._mapResult.bind(this) 58 | } 59 | 60 | async buildBackupSql() { 61 | const { db, name, columns } = this.info 62 | 63 | const cols = Object.keys(columns) 64 | .map(c => Utils.quote(c)) 65 | .join(', ') 66 | const tbl = Utils.quote(name) 67 | 68 | // get data values 69 | const values = await db.query(`SELECT ${cols} FROM ${tbl}`) 70 | if (!values || !values.length) { 71 | return '' 72 | } 73 | 74 | // build ordered column names 75 | const keys = Object.keys(values[0]) 76 | 77 | // build insert values sql 78 | const sql = `INSERT INTO ${tbl} (${keys 79 | .map(c => Utils.quote(c)) 80 | .join(', ')}) VALUES ${values 81 | .map(value => { 82 | return ( 83 | '(' + 84 | keys 85 | .map(col => 86 | Utils.asRawValue(this.info.columns[col].type, value[col]) 87 | ) 88 | .join(',') + 89 | ')' 90 | ) 91 | }) 92 | .join(',')}` 93 | return sql + ';' 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Db } from './db' 2 | 3 | export type KeyVal = { [key: string]: V } 4 | export type ValueOf = T[keyof T] 5 | 6 | export type PrimaryKeyTypes = 'INTEGER' | 'NVARCHAR' | 'CHAR' 7 | export type ColumnTypes = 8 | | PrimaryKeyTypes 9 | | 'BOOLEAN' 10 | | 'DECIMAL' 11 | | 'DATETIME' 12 | | 'MONEY' 13 | 14 | export interface ConstructorClass extends Function { 15 | new (): T 16 | } 17 | 18 | export interface TableInfo { 19 | db: Db 20 | name: string 21 | columns: { [key: string]: { primary: boolean } & ColumnInfo } 22 | descriptor: {} 23 | } 24 | 25 | export interface ColumnInfo { 26 | type: ColumnTypes 27 | size: number 28 | } 29 | 30 | export type QueryCallback = (result: any) => void 31 | export type TransactionCallback = ( 32 | transaction: Transaction, 33 | resultSet: any 34 | ) => void 35 | export type SuccessCallback = (resultSet?: any) => any 36 | export type ErrorCallback = (error: any) => any 37 | 38 | export interface Transaction { 39 | execSql( 40 | sql: string, 41 | args?: any[], 42 | success?: SuccessCallback, 43 | error?: ErrorCallback 44 | ): void 45 | } 46 | 47 | export interface ResultSet { 48 | insertId: number 49 | rowsAffected: number 50 | rows: { 51 | length: number 52 | item(index: number): any 53 | items(): any[] 54 | } 55 | } 56 | 57 | export interface DbDriver { 58 | init?: () => Promise 59 | close: () => Promise 60 | 61 | transaction( 62 | scope: (tx: Transaction) => void, 63 | error?: (error: any) => void, 64 | success?: () => void 65 | ): void 66 | 67 | query( 68 | sql: string, 69 | args: any[], 70 | error: ErrorCallback, 71 | success: QueryCallback 72 | ): void 73 | 74 | getQueryResult(resultSet: any): ResultSet 75 | } 76 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ColumnInfo, ColumnTypes } from './types' 2 | 3 | export class Utils { 4 | static quote(str: string): string { 5 | return `"${str}"` 6 | } 7 | 8 | static getRealColumnType(name: string, info: ColumnInfo) { 9 | const colSize = info.size ? `(${info.size})` : `` 10 | 11 | switch (info.type) { 12 | case 'BOOLEAN': 13 | return `BOOLEAN NOT NULL CHECK (${name} IN (0,1))` 14 | case 'MONEY': 15 | case 'DATETIME': 16 | return `INTEGER` 17 | default: 18 | return `${info.type}${colSize}` 19 | } 20 | } 21 | 22 | static asResult(colType: ColumnTypes, v: any) { 23 | switch (colType) { 24 | case 'DATETIME': 25 | return this.dateParse(v) 26 | case 'BOOLEAN': 27 | return v === 0 ? false : true 28 | case 'MONEY': 29 | return v / 100 30 | } 31 | 32 | return v 33 | } 34 | 35 | static asValue(colType: ColumnTypes, v: any) { 36 | 37 | // TODO: This is not good idea to use 'field:' prefix 38 | // use function might be good! 39 | if (typeof v === 'string' && v.startsWith('field:')) { 40 | return v.substr(6) 41 | } 42 | 43 | switch (colType) { 44 | case 'DATETIME': 45 | return this.strftime(v) 46 | case 'MONEY': 47 | return Math.round(v * 100) 48 | } 49 | 50 | switch (typeof v) { 51 | case 'string': 52 | return `'${v.replace(/\'/g, "''")}'` 53 | 54 | case 'undefined': 55 | return 'null' 56 | 57 | case 'boolean': 58 | return v === true ? '1' : '0' 59 | } 60 | 61 | if (v === null) { 62 | return 'null' 63 | } 64 | 65 | return v 66 | } 67 | 68 | static asRawValue(colType: ColumnTypes, v: any) { 69 | if (v === null) { 70 | return 'NULL' 71 | } 72 | 73 | switch (colType) { 74 | case 'DATETIME': 75 | case 'INTEGER': 76 | case 'BOOLEAN': 77 | case 'DECIMAL': 78 | return v 79 | } 80 | 81 | return `'${v}'` 82 | } 83 | 84 | static timeStamp(date: Date) { 85 | return date 86 | .toISOString() 87 | .slice(0, 19) 88 | .replace(/\-/g, '') 89 | .replace(/\:/g, '') 90 | .replace('T', '-') 91 | } 92 | 93 | static strftime(date: Date) { 94 | return `strftime('%s', '${this.formatSimpleISODate(date)}')` 95 | } 96 | 97 | static formatSimpleISODate(date: Date) { 98 | return `${date.getFullYear()}-${this.padStart( 99 | date.getMonth() + 1, 100 | 2 101 | )}-${this.padStart(date.getDate(), 2)} ${this.padStart( 102 | date.getHours(), 103 | 2 104 | )}:${this.padStart(date.getMinutes(), 2)}:${this.padStart( 105 | date.getSeconds(), 106 | 2 107 | )}` 108 | } 109 | 110 | static padStart( 111 | str: any, 112 | targetLength: number, 113 | padString: string = '0' 114 | ): string { 115 | str = str.toString() 116 | padString = String(typeof padString !== 'undefined' ? padString : ' ') 117 | if (str.length > targetLength) { 118 | return str 119 | } else { 120 | targetLength = targetLength - str.length 121 | if (targetLength > padString.length) { 122 | padString += padString.repeat(targetLength / padString.length) 123 | } 124 | return padString.slice(0, targetLength) + str 125 | } 126 | } 127 | 128 | static dateParse(str: string): Date { 129 | const parts = str.split(' ') 130 | const dates = parts[0].split('-').map(d => parseInt(d, 0)) 131 | const times = parts[1].split(':').map(d => parseInt(d, 0)) 132 | 133 | return new Date( 134 | dates[0], 135 | dates[1] - 1, 136 | dates[2], 137 | times[0], 138 | times[1], 139 | times[2] 140 | ) 141 | } 142 | 143 | static selectAs(info: ColumnInfo, fieldname: string, as?: string) { 144 | const field = Utils.quote(fieldname) 145 | if (info.type === 'DATETIME') { 146 | return Utils.selectAsDate(field, as) 147 | } 148 | 149 | if (as) { 150 | return `${field} AS ${Utils.quote(as)}` 151 | } 152 | 153 | return field 154 | } 155 | 156 | static selectAsDate(field: string, asField: string = field) { 157 | return `datetime(${field},'unixepoch') AS ${asField}` 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /test/data/crud.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { DataTestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | describe('crud', () => { 6 | let db: Db 7 | 8 | beforeAll(async done => { 9 | db = await Db.init({ 10 | driver: new DataTestDriver(':memory:'), 11 | entities, 12 | createTables: false 13 | }) 14 | 15 | done() 16 | }) 17 | 18 | afterAll(done => { 19 | db.close().then(done) 20 | }) 21 | 22 | test('data-crud', async done => { 23 | const { tables, transaction } = db 24 | 25 | const person = { 26 | name: 'Joey', 27 | married: false, 28 | dob: new Date(2000, 1, 1, 0, 0, 0), 29 | age: 1, 30 | salary: 100 31 | } 32 | 33 | await transaction(({ exec }) => { 34 | exec(tables.Person.create()) 35 | exec(tables.Person.insert(person)) 36 | }) 37 | 38 | const count = await tables.Person.count() 39 | const data = await tables.Person.single() 40 | const list = await tables.Person.select() 41 | const exists = await tables.Person.any() 42 | 43 | const personMatch = { id: 1, ...person } 44 | 45 | expect(count).toEqual(1) 46 | expect(data).toMatchObject(personMatch) 47 | expect(list.length).toEqual(1) 48 | expect(list[0]).toMatchObject(personMatch) 49 | expect(exists).toEqual(true) 50 | 51 | await tables.Person.update({ name: 'John' }).where(c => c.equals({ id: 1 })) 52 | 53 | const data3 = await tables.Person.single() 54 | expect(data3.name).toEqual('John') 55 | 56 | await tables.Person.delete() 57 | 58 | const count2 = await tables.Person.count() 59 | const data2 = await tables.Person.single() 60 | const list2 = await tables.Person.select() 61 | const exists2 = await tables.Person.any() 62 | 63 | expect(count2).toEqual(0) 64 | expect(data2).toBeUndefined() 65 | expect(list2.length).toEqual(0) 66 | expect(exists2).toEqual(false) 67 | 68 | done() 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /test/data/driver/index.ts: -------------------------------------------------------------------------------- 1 | import Sqlite3 = require('sqlite3') 2 | import { SQLite3Driver } from '../../../src/drivers/sqlite3' 3 | 4 | const sqlite = Sqlite3.verbose() 5 | 6 | export class DataTestDriver extends SQLite3Driver { 7 | filename: string 8 | 9 | constructor(filename: string) { 10 | // @ts-ignore 11 | super(null) 12 | this.filename = filename 13 | } 14 | 15 | async init(): Promise { 16 | return new Promise((resolve, reject) => { 17 | this.db = new sqlite.Database(this.filename, error => { 18 | if (error) { 19 | reject(error) 20 | } else { 21 | // @ts-ignore 22 | this.db.wait(resolve) 23 | } 24 | }) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/data/entity/Address.ts: -------------------------------------------------------------------------------- 1 | import { Column, Primary } from '../../../src' 2 | 3 | export class Address { 4 | @Primary() 5 | id: number = 0 6 | 7 | @Column('INTEGER') 8 | person: number = 0 9 | 10 | @Column('NVARCHAR') 11 | address: string = '' 12 | } 13 | -------------------------------------------------------------------------------- /test/data/entity/Person.ts: -------------------------------------------------------------------------------- 1 | import { Column, Primary } from '../../../src' 2 | 3 | export class Person { 4 | @Primary() 5 | id: number = 0 6 | 7 | @Column('NVARCHAR') 8 | name: string = '' 9 | 10 | @Column('DATETIME') 11 | dob: Date = new Date() 12 | 13 | @Column('INTEGER') 14 | age: number = 0 15 | 16 | @Column('BOOLEAN') 17 | married: boolean = false 18 | 19 | @Column('MONEY') 20 | salary: number = 0 21 | } 22 | -------------------------------------------------------------------------------- /test/data/entity/Role.ts: -------------------------------------------------------------------------------- 1 | import { Column, Primary } from '../../../src' 2 | 3 | export class Role { 4 | @Primary() 5 | id: number = 0 6 | 7 | @Column('INTEGER') 8 | user: number = 0 9 | 10 | @Column('NVARCHAR') 11 | role: string = '' 12 | } 13 | -------------------------------------------------------------------------------- /test/data/entity/index.ts: -------------------------------------------------------------------------------- 1 | export { Person } from './Person' 2 | export { Address } from './Address' 3 | export { Role } from './Role' 4 | -------------------------------------------------------------------------------- /test/data/insert.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { DataTestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | describe('data-insert', () => { 6 | let db: Db 7 | 8 | beforeAll(async done => { 9 | db = await Db.init({ 10 | driver: new DataTestDriver(':memory:'), 11 | entities 12 | }) 13 | 14 | done() 15 | }) 16 | 17 | afterAll(done => { 18 | db.close().then(() => { 19 | done() 20 | }) 21 | }) 22 | 23 | beforeEach(done => { 24 | db.tables.Person.delete().then(() => { 25 | done() 26 | }) 27 | }) 28 | 29 | test('insert-direct', async done => { 30 | const person = { 31 | name: 'Joey', 32 | married: false, 33 | dob: new Date(2000, 1, 1, 0, 0, 0), 34 | age: 1, 35 | salary: 100 36 | } 37 | 38 | const insertResult = await db.tables.Person.insert(person) 39 | const selectResults = await db.tables.Person.select() 40 | 41 | expect(insertResult.rowsAffected).toEqual(1) 42 | expect(insertResult.insertId).toEqual(1) 43 | expect(insertResult.rows.length).toEqual(0) 44 | 45 | const personMatch = { id: 1, ...person } 46 | expect(selectResults.length).toEqual(1) 47 | expect(selectResults[0]).toMatchObject(personMatch) 48 | 49 | done() 50 | }) 51 | 52 | test('insert-with-transaction', async done => { 53 | const person = { 54 | name: 'Joey', 55 | married: false, 56 | dob: new Date(2000, 1, 1, 0, 0, 0), 57 | age: 1, 58 | salary: 100 59 | } 60 | 61 | await db.transaction(({ tables, exec }) => { 62 | exec(tables.Person.insert(person)) 63 | }) 64 | 65 | const results = await db.tables.Person.select() 66 | 67 | const personMatch = { id: 1, ...person } 68 | 69 | expect(results.length).toEqual(1) 70 | expect(results[0]).toMatchObject(personMatch) 71 | 72 | done() 73 | }) 74 | 75 | test('insert-string-with-quote', async done => { 76 | const person = { 77 | name: `Joey's Nephew`, 78 | married: false, 79 | dob: new Date(2000, 1, 1, 0, 0, 0), 80 | age: 1, 81 | salary: 100 82 | } 83 | 84 | await db.transaction(({ tables, exec }) => { 85 | exec(tables.Person.insert(person)) 86 | }) 87 | 88 | const results = await db.tables.Person.select() 89 | const personMatch = { id: 1, ...person } 90 | 91 | expect(results.length).toEqual(1) 92 | expect(results[0]).toMatchObject(personMatch) 93 | 94 | done() 95 | }) 96 | 97 | test('insert-string-with-double-quote', async done => { 98 | const person = { 99 | name: `Joey"s Nephew`, 100 | married: false, 101 | dob: new Date(2000, 1, 1, 0, 0, 0), 102 | age: 1, 103 | salary: 100 104 | } 105 | 106 | await db.transaction(({ tables, exec }) => { 107 | exec(tables.Person.insert(person)) 108 | }) 109 | 110 | const results = await db.tables.Person.select() 111 | const personMatch = { id: 1, ...person } 112 | 113 | expect(results.length).toEqual(1) 114 | expect(results[0]).toMatchObject(personMatch) 115 | 116 | done() 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /test/data/join.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { DataTestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | describe('data-join', () => { 6 | let db: Db 7 | 8 | beforeAll(async done => { 9 | db = await Db.init({ 10 | driver: new DataTestDriver(':memory:'), 11 | entities 12 | }) 13 | 14 | done() 15 | }) 16 | 17 | afterAll(done => { 18 | db.close().then(() => { 19 | done() 20 | }) 21 | }) 22 | 23 | beforeEach(done => { 24 | db.transaction(({ exec, tables }) => { 25 | exec(tables.Person.delete()) 26 | exec(tables.Address.delete()) 27 | exec(tables.Role.delete()) 28 | }).then(() => { 29 | done() 30 | }) 31 | }) 32 | 33 | test('data-join', async done => { 34 | const { tables } = db 35 | 36 | const person = await tables.Person.insert({ 37 | name: 'Joey', 38 | married: false, 39 | dob: new Date(2000, 1, 1, 0, 0, 0), 40 | age: 1, 41 | salary: 100 42 | }) 43 | 44 | await tables.Address.insert({ 45 | person: person.insertId, 46 | address: 'Nowhere' 47 | }) 48 | 49 | await tables.Role.insert({ 50 | role: 'Admin', 51 | user: person.insertId 52 | }) 53 | 54 | const result = await tables.Person.join( 55 | o => ({ 56 | addr: o.Address, 57 | role: o.Role 58 | }), 59 | (from, { addr, role }) => { 60 | from 61 | .equal({ 62 | id: role.id 63 | }) 64 | .equal({ 65 | id: addr.id 66 | }) 67 | } 68 | ).map(f => ({ 69 | id: f.self.id, 70 | profile: { 71 | name: f.self.name, 72 | birth: f.self.dob 73 | }, 74 | attr: { 75 | address: f.addr.address, 76 | role: f.role.role 77 | } 78 | })) 79 | 80 | const expected = [ 81 | { 82 | id: 1, 83 | profile: { name: 'Joey', birth: new Date(2000, 1, 1, 0, 0, 0) }, 84 | attr: { address: 'Nowhere', role: 'Admin' } 85 | } 86 | ] 87 | 88 | expect(result).toEqual(expected) 89 | 90 | done() 91 | }) 92 | 93 | test('data-join-where', async done => { 94 | const { tables } = db 95 | 96 | await tables.Person.insert([ 97 | { 98 | name: 'Joey', 99 | married: false, 100 | dob: new Date(2000, 1, 1, 0, 0, 0), 101 | age: 1, 102 | salary: 100 103 | }, 104 | { 105 | name: 'Mary', 106 | married: false, 107 | dob: new Date(2000, 2, 2, 0, 0, 0), 108 | age: 2, 109 | salary: 50 110 | } 111 | ]) 112 | 113 | await tables.Address.insert([ 114 | { 115 | person: 1, 116 | address: 'Nowhere' 117 | }, 118 | { 119 | person: 2, 120 | address: 'Here' 121 | } 122 | ]) 123 | 124 | await tables.Role.insert([ 125 | { 126 | role: 'Admin', 127 | user: 1 128 | }, 129 | { 130 | role: 'Mary', 131 | user: 2 132 | } 133 | ]) 134 | 135 | const result = await tables.Person.join( 136 | o => ({ 137 | addr: o.Address, 138 | role: o.Role 139 | }), 140 | (from, { addr, role }) => { 141 | from 142 | .equal({ 143 | id: role.id 144 | }) 145 | .equal({ 146 | id: addr.id 147 | }) 148 | } 149 | ) 150 | .map(f => ({ 151 | id: f.self.id, 152 | profile: { 153 | name: f.self.name, 154 | birth: f.self.dob 155 | }, 156 | attr: { 157 | address: f.addr.address, 158 | role: f.role.role 159 | } 160 | })) 161 | .where((f, r) => [ 162 | f.self.equals({ id: 2 }).endsWith({ name: 'ry' }), 163 | f.role.equals({ role: r.self.name }) 164 | ]) 165 | 166 | expect(result).toEqual([ 167 | { 168 | id: 2, 169 | profile: { name: 'Mary', birth: new Date(2000, 2, 2, 0, 0, 0) }, 170 | attr: { address: 'Here', role: 'Mary' } 171 | } 172 | ]) 173 | 174 | done() 175 | }) 176 | 177 | test('data-join-where-order', async done => { 178 | const { tables } = db 179 | 180 | await tables.Person.insert([ 181 | { 182 | name: 'Joey', 183 | married: false, 184 | dob: new Date(2000, 1, 1, 0, 0, 0), 185 | age: 1, 186 | salary: 100 187 | }, 188 | { 189 | name: 'Mary', 190 | married: false, 191 | dob: new Date(2000, 2, 2, 0, 0, 0), 192 | age: 2, 193 | salary: 50 194 | } 195 | ]) 196 | 197 | await tables.Address.insert([ 198 | { 199 | person: 1, 200 | address: 'Nowhere' 201 | }, 202 | { 203 | person: 2, 204 | address: 'Here' 205 | } 206 | ]) 207 | 208 | await tables.Role.insert([ 209 | { 210 | role: 'Admin', 211 | user: 1 212 | }, 213 | { 214 | role: 'Mary', 215 | user: 2 216 | } 217 | ]) 218 | 219 | const result = await tables.Person.join( 220 | o => ({ 221 | addr: o.Address, 222 | role: o.Role 223 | }), 224 | (from, { addr, role }) => { 225 | from 226 | .equal({ 227 | id: role.id 228 | }) 229 | .equal({ 230 | id: addr.id 231 | }) 232 | } 233 | ) 234 | .map(f => ({ 235 | id: f.self.id, 236 | name: f.self.name, 237 | address: f.addr.address, 238 | role: f.role.role, 239 | dateOfBirth: f.self.dob 240 | })) 241 | .where(f => [f.self.notEquals({ married: true })]) 242 | .orderBy({ self: { id: 'DESC' } }) 243 | 244 | expect(result).toEqual([ 245 | { 246 | id: 2, 247 | name: 'Mary', 248 | address: 'Here', 249 | role: 'Mary', 250 | dateOfBirth: new Date(2000, 2, 2, 0, 0, 0) 251 | }, 252 | { 253 | id: 1, 254 | name: 'Joey', 255 | address: 'Nowhere', 256 | role: 'Admin', 257 | dateOfBirth: new Date(2000, 1, 1, 0, 0, 0) 258 | } 259 | ]) 260 | 261 | done() 262 | }) 263 | 264 | test('data-join-where-order-limit', async done => { 265 | const { tables } = db 266 | 267 | await tables.Person.insert([ 268 | { 269 | name: 'Joey', 270 | married: false, 271 | dob: new Date(2000, 1, 1, 0, 0, 0), 272 | age: 1, 273 | salary: 100 274 | }, 275 | { 276 | name: 'Mary', 277 | married: false, 278 | dob: new Date(2000, 2, 2, 0, 0, 0), 279 | age: 2, 280 | salary: 50 281 | } 282 | ]) 283 | 284 | await tables.Address.insert([ 285 | { 286 | person: 1, 287 | address: 'Nowhere' 288 | }, 289 | { 290 | person: 2, 291 | address: 'Here' 292 | } 293 | ]) 294 | 295 | await tables.Role.insert([ 296 | { 297 | role: 'Admin', 298 | user: 1 299 | }, 300 | { 301 | role: 'Mary', 302 | user: 2 303 | } 304 | ]) 305 | 306 | const result = await tables.Person.join( 307 | o => ({ 308 | addr: o.Address, 309 | role: o.Role 310 | }), 311 | (from, { addr, role }) => { 312 | from 313 | .equal({ 314 | id: role.id 315 | }) 316 | .equal({ 317 | id: addr.id 318 | }) 319 | } 320 | ) 321 | .map(f => ({ 322 | id: f.self.id, 323 | name: f.self.name, 324 | address: f.addr.address, 325 | role: f.role.role, 326 | dateOfBirth: f.self.dob 327 | })) 328 | .where(f => [f.self.notEquals({ married: true })]) 329 | .orderBy({ self: { id: 'DESC' } }) 330 | .limit(1) 331 | 332 | expect(result).toEqual([ 333 | { 334 | id: 2, 335 | name: 'Mary', 336 | address: 'Here', 337 | role: 'Mary', 338 | dateOfBirth: new Date(2000, 2, 2, 0, 0, 0) 339 | } 340 | ]) 341 | 342 | done() 343 | }) 344 | }) 345 | -------------------------------------------------------------------------------- /test/data/single.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { DataTestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | describe('data-single', () => { 6 | let db: Db 7 | 8 | beforeAll(async done => { 9 | db = await Db.init({ 10 | driver: new DataTestDriver(':memory:'), 11 | entities 12 | }) 13 | 14 | done() 15 | }) 16 | 17 | afterAll(done => { 18 | db.close().then(() => { 19 | done() 20 | }) 21 | }) 22 | 23 | beforeEach(done => { 24 | db.transaction(({ exec, tables }) => { 25 | exec(tables.Person.delete()) 26 | exec(tables.Address.delete()) 27 | exec(tables.Role.delete()) 28 | }).then(() => { 29 | done() 30 | }) 31 | }) 32 | 33 | test('single-test-no-data', async done => { 34 | const data = await db.tables.Person.single() 35 | expect(data).toBeUndefined() 36 | done() 37 | }) 38 | 39 | test('single-test-no-data-with-condition', async done => { 40 | const data = await db.tables.Person.single().where(c => c.equals({ id: 1 })) 41 | expect(data).toBeUndefined() 42 | done() 43 | }) 44 | 45 | test('single-test-no-data-with-order', async done => { 46 | const data = await db.tables.Person.single().orderBy({ id: 'ASC' }) 47 | expect(data).toBeUndefined() 48 | done() 49 | }) 50 | 51 | test('single-test-with-data', async done => { 52 | const now = new Date(1992, 2, 26) 53 | await db.tables.Person.insert({ 54 | id: 1, 55 | name: 'Budi', 56 | married: false, 57 | age: 26, 58 | dob: now, 59 | salary: 20000 60 | }) 61 | const data = await db.tables.Person.single() 62 | expect(data).toEqual({ 63 | id: 1, 64 | name: 'Budi', 65 | married: false, 66 | age: 26, 67 | dob: now, 68 | salary: 20000 69 | }) 70 | done() 71 | }) 72 | 73 | test('single-test-with-data-condition', async done => { 74 | const now = new Date(1992, 2, 26) 75 | await db.tables.Person.insert([ 76 | { 77 | id: 1, 78 | name: 'Budi', 79 | married: false, 80 | age: 26, 81 | dob: now, 82 | salary: 20000 83 | }, 84 | { 85 | id: 2, 86 | name: 'Mark', 87 | married: true, 88 | age: 26, 89 | dob: now, 90 | salary: 10000 91 | } 92 | ]) 93 | 94 | const data = await db.tables.Person.single().where(c => c.equals({ id: 2 })) 95 | 96 | expect(data).toEqual({ 97 | id: 2, 98 | name: 'Mark', 99 | married: true, 100 | age: 26, 101 | dob: now, 102 | salary: 10000 103 | }) 104 | done() 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /test/driver/driver/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DbDriver, 3 | ErrorCallback, 4 | QueryCallback, 5 | ResultSet, 6 | Transaction 7 | } from '../../../src/types' 8 | 9 | export class TestDriver implements DbDriver { 10 | sql: string[] = [] 11 | isQuery: boolean = false 12 | 13 | async init(): Promise { 14 | await true 15 | } 16 | 17 | transaction( 18 | scope: (tx: Transaction) => void, 19 | _error: ((error: any) => void), 20 | success: (() => void) 21 | ): void { 22 | this.sql.push('BEGIN') 23 | scope({ 24 | execSql: (sql, _args, resolve) => { 25 | this.sql.push(sql) 26 | this.isQuery = false 27 | if (resolve) { 28 | resolve({}) 29 | } 30 | } 31 | }) 32 | this.sql.push('COMMIT') 33 | if (success) { 34 | success() 35 | } 36 | } 37 | 38 | close(): Promise { 39 | return new Promise(resolve => { 40 | resolve() 41 | }) 42 | } 43 | 44 | query( 45 | sql: string, 46 | _args: any[], 47 | _error: ErrorCallback, 48 | success: QueryCallback 49 | ): void { 50 | this.sql.push(sql) 51 | this.isQuery = true 52 | 53 | if (sql.indexOf(' as count FROM ') > -1) { 54 | success([{ count: 1 }]) 55 | } else { 56 | success([{}]) 57 | } 58 | } 59 | 60 | getQueryResult(results: any[]): ResultSet { 61 | return { 62 | insertId: 1, 63 | rows: { 64 | item: index => results[index], 65 | items: () => results, 66 | length: results.length 67 | }, 68 | rowsAffected: results.length 69 | } 70 | } 71 | 72 | reset() { 73 | this.sql = [] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/driver/entity/Person.ts: -------------------------------------------------------------------------------- 1 | import { Column, Primary } from '../../../src' 2 | 3 | export class Person { 4 | @Primary() 5 | id: number = 0 6 | 7 | @Column('NVARCHAR') 8 | name: string = '' 9 | 10 | @Column('DATETIME') 11 | dob: Date = new Date() 12 | 13 | @Column('INTEGER') 14 | age: number = 0 15 | 16 | @Column('BOOLEAN') 17 | married: boolean = false 18 | 19 | @Column('MONEY') 20 | salary: number = 0 21 | } 22 | -------------------------------------------------------------------------------- /test/driver/entity/index.ts: -------------------------------------------------------------------------------- 1 | export { Person } from './Person' 2 | -------------------------------------------------------------------------------- /test/driver/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('driver-insert', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ tables }) => { 11 | driver.reset() 12 | 13 | await tables.Person.insert({ id: 1, name: 'Foo' }) 14 | expect(driver.sql).toEqual([ 15 | 'BEGIN', 16 | `INSERT INTO "Person" ("id","name") VALUES (1,'Foo')`, 17 | 'COMMIT' 18 | ]) 19 | done() 20 | }) 21 | }) 22 | 23 | test('driver-transaction', done => { 24 | const driver = new TestDriver() 25 | Db.init({ 26 | driver, 27 | entities 28 | }).then(async ({ transaction }) => { 29 | driver.reset() 30 | 31 | await transaction(({ tables, exec }) => { 32 | exec(tables.Person.insert({ id: 1, name: 'Foo' })) 33 | exec(tables.Person.insert({ id: 1, name: 'Bar' })) 34 | exec(tables.Person.insert({ id: 1, name: 'Meh' })) 35 | }) 36 | 37 | expect(driver.sql).toEqual([ 38 | 'BEGIN', 39 | `INSERT INTO "Person" ("id","name") VALUES (1,'Foo')`, 40 | `INSERT INTO "Person" ("id","name") VALUES (1,'Bar')`, 41 | `INSERT INTO "Person" ("id","name") VALUES (1,'Meh')`, 42 | 'COMMIT' 43 | ]) 44 | done() 45 | }) 46 | }) 47 | 48 | test('driver-read', done => { 49 | const driver = new TestDriver() 50 | Db.init({ 51 | driver, 52 | entities 53 | }).then(async ({ tables }) => { 54 | driver.reset() 55 | 56 | await tables.Person.select(c => [c.name, c.salary]) 57 | .where(c => c.equals({ id: 1 })) 58 | .orderBy({ dob: 'ASC', id: 'ASC' }) 59 | .limit(1) 60 | 61 | expect(driver.sql).toEqual([ 62 | 'SELECT "name","salary" FROM "Person" WHERE "id" = 1 ORDER BY "dob" ASC "id" ASC LIMIT 1' 63 | ]) 64 | 65 | done() 66 | }) 67 | }) 68 | 69 | -------------------------------------------------------------------------------- /test/readme/index.test.ts: -------------------------------------------------------------------------------- 1 | import Sqlite3 = require('sqlite3') 2 | import { Column, Db, Primary, SQLite3Driver } from '../../src' 3 | 4 | const sqlite = Sqlite3.verbose() 5 | 6 | test('readme', async done => { 7 | // 8 | // Define Entities 9 | // 10 | 11 | class Person { 12 | @Primary() 13 | id: number = 0 14 | 15 | @Column('NVARCHAR') 16 | name: string = '' 17 | 18 | @Column('DATETIME') 19 | dob: Date = new Date() 20 | 21 | @Column('INTEGER') 22 | age: number = 0 23 | 24 | @Column('BOOLEAN') 25 | married: boolean = false 26 | 27 | @Column('MONEY') 28 | salary: number = 0 29 | } 30 | 31 | // tslint:disable-next-line:max-classes-per-file 32 | class Address { 33 | @Primary() 34 | id: number = 0 35 | 36 | @Column('INTEGER') 37 | person: number = 0 38 | 39 | @Column('NVARCHAR') 40 | address: string = '' 41 | } 42 | 43 | // 44 | // Connect to Database 45 | // 46 | 47 | // define entities object 48 | const entities = { 49 | Person, 50 | Address 51 | } 52 | 53 | // make a connection using SQLite3. 54 | // you can use other available drivers 55 | // or create your own 56 | const sqlite3Db = new sqlite.Database(':memory:') 57 | const db = await Db.init({ 58 | // set the driver 59 | driver: new SQLite3Driver(sqlite3Db), 60 | 61 | // set your entities here 62 | entities, 63 | 64 | // set `true` so all tables in entities will automatically created for you 65 | // if it does not exists yet in database 66 | createTables: false 67 | }) 68 | 69 | // create all tables 70 | await db.createAllTables() 71 | 72 | // or create table one by one 73 | await db.tables.Person.create() 74 | await db.tables.Address.create() 75 | 76 | // insert single data 77 | const result = await db.tables.Person.insert({ 78 | name: 'Joey', 79 | married: true, 80 | dob: new Date(2000, 1, 1, 0, 0, 0), 81 | age: 18, 82 | salary: 100 83 | }) 84 | 85 | expect(result.insertId).toEqual(1) 86 | expect(result.rowsAffected).toEqual(1) 87 | 88 | // insert multiple data at once 89 | const results = await db.tables.Person.insert([ 90 | { 91 | name: 'Hanna', 92 | married: false, 93 | dob: new Date(2001, 2, 2, 0, 0, 0), 94 | age: 17, 95 | salary: 100 96 | }, 97 | { 98 | name: 'Mary', 99 | married: false, 100 | dob: new Date(2002, 3, 3, 0, 0, 0), 101 | age: 26, 102 | salary: 50 103 | } 104 | ]) 105 | 106 | expect(results.insertId).toEqual(3) 107 | expect(results.rowsAffected).toEqual(2) 108 | 109 | let address1: any 110 | let address2: any 111 | let address3: any 112 | await db.transaction(({ exec, tables }) => { 113 | exec( 114 | tables.Address.insert({ 115 | person: 1, 116 | address: `Joy's Home` 117 | }) 118 | ).then(r => { 119 | address1 = r 120 | }) 121 | 122 | exec( 123 | tables.Address.insert({ 124 | person: 2, 125 | address: `Hanna's Home` 126 | }) 127 | ).then(r => { 128 | address2 = r 129 | }) 130 | 131 | exec( 132 | tables.Address.insert({ 133 | person: 3, 134 | address: `Marry's Home` 135 | }) 136 | ).then(r => { 137 | address3 = r 138 | }) 139 | }) 140 | 141 | expect(address1.insertId).toEqual(1) 142 | expect(address1.rowsAffected).toEqual(1) 143 | expect(address2.insertId).toEqual(2) 144 | expect(address2.rowsAffected).toEqual(1) 145 | expect(address3.insertId).toEqual(3) 146 | expect(address3.rowsAffected).toEqual(1) 147 | 148 | // select all 149 | const people = await db.tables.Person.select() 150 | expect(people).toEqual([ 151 | { 152 | id: 1, 153 | name: 'Joey', 154 | dob: new Date(2000, 1, 1, 0, 0, 0), 155 | age: 18, 156 | married: true, 157 | salary: 100 158 | }, 159 | { 160 | id: 2, 161 | name: 'Hanna', 162 | dob: new Date(2001, 2, 2, 0, 0, 0), 163 | age: 17, 164 | married: false, 165 | salary: 100 166 | }, 167 | { 168 | id: 3, 169 | name: 'Mary', 170 | dob: new Date(2002, 3, 3, 0, 0, 0), 171 | age: 26, 172 | married: false, 173 | salary: 50 174 | } 175 | ]) 176 | 177 | // select columns 178 | const people2 = await db.tables.Person.select(c => [c.id, c.name, c.salary]) 179 | expect(people2).toEqual([ 180 | { id: 1, name: 'Joey', salary: 100 }, 181 | { id: 2, name: 'Hanna', salary: 100 }, 182 | { id: 3, name: 'Mary', salary: 50 } 183 | ]) 184 | 185 | // select with limit 186 | const people3 = await db.tables.Person.select(c => [ 187 | c.id, 188 | c.name, 189 | c.salary 190 | ]).limit(1) 191 | expect(people3).toEqual([{ id: 1, name: 'Joey', salary: 100 }]) 192 | 193 | // select with condition 194 | const people4 = await db.tables.Person.select(c => [c.id, c.name]).where(c => 195 | c.greaterThanOrEqual({ salary: 100 }) 196 | ) 197 | expect(people4).toEqual([{ id: 1, name: 'Joey' }, { id: 2, name: 'Hanna' }]) 198 | 199 | // select with order 200 | const people5 = await db.tables.Person.select(c => [c.id, c.name]) 201 | .where(c => c.notEquals({ married: true })) 202 | .orderBy({ name: 'DESC' }) 203 | 204 | expect(people5).toEqual([{ id: 3, name: 'Mary' }, { id: 2, name: 'Hanna' }]) 205 | 206 | // select single data 207 | const person = await db.tables.Person.single(c => [c.id, c.name]) 208 | expect(person).toEqual({ id: 1, name: 'Joey' }) 209 | 210 | // update 211 | 212 | // let's prove that she's not married yet 213 | 214 | let hanna = await db.tables.Person.single(c => [ 215 | c.id, 216 | c.name, 217 | c.married 218 | ]).where(c => c.equals({ id: 2 })) 219 | 220 | // hanna is not married yet = { id: 2, name: 'Hanna', married: false } 221 | expect(hanna).toEqual({ id: 2, name: 'Hanna', married: false }) 222 | 223 | // let's marry her 224 | await db.tables.Person.update({ married: true }).where(c => 225 | c.equals({ id: 2 }) 226 | ) 227 | 228 | hanna = await db.tables.Person.single(c => [c.id, c.name, c.married]).where( 229 | c => c.equals({ id: 2 }) 230 | ) 231 | 232 | // hanna is now married = { id: 2, name: 'Hanna', married: true } 233 | expect(hanna).toEqual({ id: 2, name: 'Hanna', married: true }) 234 | 235 | const people6 = await db.tables.Person.join( 236 | t => ({ 237 | // FROM Person AS self JOIN Address AS address 238 | address: t.Address 239 | }), 240 | (p, { address }) => { 241 | // ON self.id = address.person 242 | p.equal({ id: address.person }) 243 | } 244 | ).map(f => ({ 245 | // SELECT self.id AS id, self.name AS name, address.address AS address 246 | id: f.self.id, 247 | name: f.self.name, 248 | address: f.address.address 249 | })) 250 | 251 | expect(people6).toEqual([ 252 | { id: 1, name: 'Joey', address: "Joy's Home" }, 253 | { id: 2, name: 'Hanna', address: "Hanna's Home" }, 254 | { id: 3, name: 'Mary', address: "Marry's Home" } 255 | ]) 256 | 257 | // join where order and limit 258 | const people7 = await db.tables.Person.join( 259 | t => ({ 260 | // FROM Person AS self JOIN Address AS address 261 | address: t.Address 262 | }), 263 | (p, { address }) => { 264 | // ON self.id = address.person 265 | p.equal({ id: address.person }) 266 | } 267 | ) 268 | .map(f => ({ 269 | // SELECT self.id AS id, self.name AS name, address.address AS address 270 | id: f.self.id, 271 | name: f.self.name, 272 | address: f.address.address 273 | })) 274 | // WHERE self.married = 1 275 | .where(p => p.self.equals({ married: true })) 276 | // ORDER BY address.address ASC 277 | .orderBy({ address: { address: 'ASC' } }) 278 | // LIMIT 1 279 | .limit(1) 280 | 281 | expect(people7).toEqual([{ id: 2, name: 'Hanna', address: "Hanna's Home" }]) 282 | 283 | // delete 284 | const delResult = await db.tables.Person.delete().where(c => 285 | c.equals({ id: 3 }) 286 | ) 287 | 288 | expect(delResult.insertId).toEqual(3) 289 | expect(delResult.rowsAffected).toEqual(1) 290 | 291 | // drop 292 | await db.tables.Address.drop() 293 | 294 | // or drop inside transaction 295 | await db.transaction(({ exec, tables }) => { 296 | exec(tables.Address.drop()) 297 | exec(tables.Person.drop()) 298 | }) 299 | 300 | // or drop all tables 301 | await db.dropAllTables() 302 | 303 | done() 304 | }) 305 | -------------------------------------------------------------------------------- /test/sql/any.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-any', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ tables }) => { 11 | driver.reset() 12 | 13 | await tables.Person.any().where(c => c.equals({ id: 1 })) 14 | 15 | expect(driver.sql).toEqual([ 16 | 'SELECT COUNT(*) as count FROM "Person" WHERE "id" = 1' 17 | ]) 18 | 19 | done() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/sql/count.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-count', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ tables }) => { 11 | driver.reset() 12 | 13 | await tables.Person.count().where(c => c.equals({ id: 1 })) 14 | 15 | expect(driver.sql).toEqual([ 16 | 'SELECT COUNT(*) as count FROM "Person" WHERE "id" = 1' 17 | ]) 18 | 19 | done() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/sql/create.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-create', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ tables }) => { 11 | driver.reset() 12 | 13 | await tables.Person.create() 14 | 15 | expect(driver.sql).toEqual([ 16 | 'CREATE TABLE IF NOT EXISTS "Person" ("id" INTEGER PRIMARY KEY, "name" NVARCHAR, "dob" INTEGER, "age" INTEGER, "married" BOOLEAN NOT NULL CHECK (married IN (0,1)), "salary" INTEGER)' 17 | ]) 18 | 19 | done() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/sql/delete.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-delete', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ tables }) => { 11 | driver.reset() 12 | 13 | await tables.Person.delete() 14 | await tables.Person.delete().where(c => c.equals({ age: 28 })) 15 | 16 | expect(driver.sql).toEqual([ 17 | 'DELETE FROM "Person"', 18 | 'DELETE FROM "Person" WHERE "age" = 28' 19 | ]) 20 | 21 | done() 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/sql/driver/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DbDriver, 3 | ErrorCallback, 4 | QueryCallback, 5 | ResultSet, 6 | Transaction 7 | } from '../../../src/types' 8 | 9 | export class TestDriver implements DbDriver { 10 | sql: string[] = [] 11 | isQuery: boolean = false 12 | 13 | async init(): Promise { 14 | await true 15 | } 16 | 17 | transaction( 18 | scope: (tx: Transaction) => void, 19 | _error: ((error: any) => void), 20 | success: (() => void) 21 | ): void { 22 | scope({ 23 | execSql: (sql, _args, resolve) => { 24 | this.sql.push(sql) 25 | this.isQuery = false 26 | if (resolve) { 27 | resolve({}) 28 | } 29 | 30 | if (success) { 31 | success() 32 | } 33 | } 34 | }) 35 | } 36 | 37 | close(): Promise { 38 | return new Promise(resolve => { 39 | resolve() 40 | }) 41 | } 42 | 43 | query( 44 | sql: string, 45 | _args: any[], 46 | _error: ErrorCallback, 47 | success: QueryCallback 48 | ): void { 49 | this.sql.push(sql) 50 | this.isQuery = true 51 | 52 | if (sql.indexOf(' as count FROM ') > -1) { 53 | success([{ count: 1 }]) 54 | } else { 55 | success([{}]) 56 | } 57 | } 58 | 59 | getQueryResult(results: any[]): ResultSet { 60 | return { 61 | insertId: 1, 62 | rows: { 63 | item: index => results[index], 64 | items: () => results, 65 | length: results.length 66 | }, 67 | rowsAffected: results.length 68 | } 69 | } 70 | 71 | reset() { 72 | this.sql = [] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/sql/drop.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-drop', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ tables }) => { 11 | driver.reset() 12 | 13 | await tables.Person.drop() 14 | 15 | expect(driver.sql).toEqual(['DROP TABLE IF EXISTS "Person"']) 16 | 17 | done() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /test/sql/entity/Address.ts: -------------------------------------------------------------------------------- 1 | import { Column, Primary } from '../../../src' 2 | 3 | export class Address { 4 | @Primary() 5 | id: number = 0 6 | 7 | @Column('INTEGER') 8 | person: number = 0 9 | 10 | @Column('NVARCHAR') 11 | address: string = '' 12 | } 13 | -------------------------------------------------------------------------------- /test/sql/entity/Person.ts: -------------------------------------------------------------------------------- 1 | import { Column, Primary } from '../../../src' 2 | 3 | export class Person { 4 | @Primary() 5 | id: number = 0 6 | 7 | @Column('NVARCHAR') 8 | name: string = '' 9 | 10 | @Column('DATETIME') 11 | dob: Date = new Date() 12 | 13 | @Column('INTEGER') 14 | age: number = 0 15 | 16 | @Column('BOOLEAN') 17 | married: boolean = false 18 | 19 | @Column('MONEY') 20 | salary: number = 0 21 | } 22 | -------------------------------------------------------------------------------- /test/sql/entity/Role.ts: -------------------------------------------------------------------------------- 1 | import { Column, Primary } from '../../../src' 2 | 3 | export class Role { 4 | @Primary() 5 | id: number = 0 6 | 7 | @Column('INTEGER') 8 | user: number = 0 9 | 10 | @Column('NVARCHAR') 11 | role: string = '' 12 | } 13 | -------------------------------------------------------------------------------- /test/sql/entity/index.ts: -------------------------------------------------------------------------------- 1 | export { Person } from './Person' 2 | export { Address } from './Address' 3 | export { Role } from './Role' 4 | -------------------------------------------------------------------------------- /test/sql/init.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-init', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(db => { 11 | expect(db.tables.Person).toBeTruthy() 12 | expect(db.tables.Person).toHaveProperty('info') 13 | // tslint:disable-next-line:no-string-literal 14 | expect(db.tables.Person['info']).toMatchObject({ 15 | name: 'Person', 16 | columns: { 17 | id: { primary: true, type: 'INTEGER', size: undefined }, 18 | name: { primary: false, type: 'NVARCHAR', size: undefined }, 19 | dob: { primary: false, type: 'DATETIME', size: undefined }, 20 | age: { primary: false, type: 'INTEGER', size: undefined }, 21 | married: { primary: false, type: 'BOOLEAN', size: undefined }, 22 | salary: { primary: false, type: 'MONEY', size: undefined } 23 | } 24 | }) 25 | 26 | expect(driver.sql).toEqual([ 27 | 'CREATE TABLE IF NOT EXISTS "Person" ("id" INTEGER PRIMARY KEY, "name" NVARCHAR, "dob" INTEGER, "age" INTEGER, "married" BOOLEAN NOT NULL CHECK (married IN (0,1)), "salary" INTEGER)', 28 | 'CREATE TABLE IF NOT EXISTS "Address" ("id" INTEGER PRIMARY KEY, "person" INTEGER, "address" NVARCHAR)', 29 | 'CREATE TABLE IF NOT EXISTS "Role" ("id" INTEGER PRIMARY KEY, "user" INTEGER, "role" NVARCHAR)' 30 | ]) 31 | 32 | done() 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/sql/insert.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-insert', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ tables }) => { 11 | driver.reset() 12 | 13 | await tables.Person.insert({ id: 1, name: 'Foo' }) 14 | expect(driver.sql).toEqual([ 15 | `INSERT INTO "Person" ("id","name") VALUES (1,'Foo')` 16 | ]) 17 | 18 | driver.reset() 19 | await tables.Person.upsert({ id: 1, name: 'Foo' }) 20 | expect(driver.sql).toEqual([ 21 | `INSERT OR REPLACE INTO "Person" ("id","name") VALUES (1,'Foo')` 22 | ]) 23 | 24 | driver.reset() 25 | await tables.Person.insert([ 26 | { id: 1, name: 'Foo' }, 27 | { id: 2, name: 'Bar' }, 28 | { id: 3, name: 'Meh' } 29 | ]) 30 | 31 | expect(driver.sql).toEqual([ 32 | 'INSERT INTO "Person" ("id","name") SELECT 1 AS "id",\'Foo\' AS "name" UNION ALL SELECT 2,\'Bar\' UNION ALL SELECT 3,\'Meh\'' 33 | ]) 34 | 35 | done() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/sql/join.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | interface PersonAddress { 6 | id: number 7 | name: string 8 | address: string 9 | } 10 | 11 | describe('sql-join', () => { 12 | const driver = new TestDriver() 13 | let db: Db 14 | 15 | beforeAll(async done => { 16 | db = await Db.init({ 17 | driver, 18 | entities 19 | }) 20 | done() 21 | }) 22 | 23 | afterAll(done => { 24 | db.close().then(() => { 25 | done() 26 | }) 27 | }) 28 | 29 | beforeEach(done => { 30 | driver.reset() 31 | done() 32 | }) 33 | 34 | test('sql-joins', async done => { 35 | const { tables } = db 36 | 37 | await tables.Person.join( 38 | o => ({ 39 | addr: o.Address, 40 | role: o.Role 41 | }), 42 | (from, { addr, role }) => { 43 | from 44 | .equal({ 45 | id: role.id 46 | }) 47 | .equal({ 48 | id: addr.id 49 | }) 50 | } 51 | ).map(f => ({ 52 | id: f.self.id, 53 | name: f.self.name, 54 | foo: { 55 | address: f.addr.address, 56 | role: f.role.role 57 | } 58 | })) 59 | 60 | await tables.Person.join( 61 | o => ({ 62 | addr: o.Address, 63 | role: o.Role 64 | }), 65 | (from, { addr, role }) => { 66 | from.equal({ 67 | id: role.id 68 | }) 69 | role.equal({ 70 | id: addr.id 71 | }) 72 | } 73 | ).map(f => ({ 74 | id: f.self.id, 75 | name: f.self.name, 76 | address: f.addr.address 77 | })) 78 | 79 | await tables.Person.join( 80 | o => ({ 81 | addr: o.Address, 82 | role: o.Role 83 | }), 84 | (from, { addr, role }) => { 85 | from.equal({ 86 | id: role.id, 87 | name: role.role 88 | }) 89 | role.equal({ 90 | id: addr.id 91 | }) 92 | } 93 | ).map(f => ({ 94 | id: f.self.id 95 | })) 96 | 97 | await tables.Person.join( 98 | o => ({ 99 | addr: o.Address, 100 | role: o.Role 101 | }), 102 | (from, { addr, role }) => { 103 | from 104 | .equal({ 105 | id: role.id 106 | }) 107 | .equal({ 108 | name: role.role 109 | }) 110 | role.equal({ 111 | id: addr.id 112 | }) 113 | } 114 | ).map(f => ({ 115 | id: f.self.id 116 | })) 117 | 118 | expect(driver.sql).toEqual([ 119 | 'SELECT "self"."id" AS "self___id","self"."name" AS "self___name","addr"."address" AS "addr___address","role"."role" AS "role___role" FROM "Person" AS "self" JOIN "Role" AS "role" ON "self"."id" = "role"."id" JOIN "Address" AS "addr" ON "self"."id" = "addr"."id" ', 120 | 'SELECT "self"."id" AS "self___id","self"."name" AS "self___name","addr"."address" AS "addr___address" FROM "Person" AS "self" JOIN "Role" AS "role" ON "self"."id" = "role"."id" JOIN "Address" AS "addr" ON "role"."id" = "addr"."id" ', 121 | 'SELECT "self"."id" AS "self___id" FROM "Person" AS "self" JOIN "Role" AS "role" ON "self"."id" = "role"."id" AND "self"."name" = "role"."role" JOIN "Address" AS "addr" ON "role"."id" = "addr"."id" ', 122 | 'SELECT "self"."id" AS "self___id" FROM "Person" AS "self" JOIN "Role" AS "role" ON "self"."id" = "role"."id" AND "self"."name" = "role"."role" JOIN "Address" AS "addr" ON "role"."id" = "addr"."id" ' 123 | ]) 124 | 125 | done() 126 | }) 127 | 128 | test('sql-join-where', async done => { 129 | const { tables } = db 130 | 131 | await tables.Address.join( 132 | p => ({ p: p.Person }), 133 | (a, other) => { 134 | a.equal({ id: other.p.id }) 135 | } 136 | ) 137 | .map(f => ({ 138 | id: f.p.id, 139 | name: f.p.name, 140 | address: f.self.address 141 | })) 142 | .where(l => l.self.startsWith({ address: 'foo' })) 143 | 144 | expect(driver.sql).toEqual([ 145 | 'SELECT "p"."id" AS "p___id","p"."name" AS "p___name","self"."address" AS "self___address" FROM "Address" AS "self" JOIN "Person" AS "p" ON "self"."id" = "p"."id" WHERE "self"."address" LIKE \'foo%\'' 146 | ]) 147 | 148 | done() 149 | }) 150 | 151 | test('sql-join-order', async done => { 152 | const { tables } = db 153 | 154 | await tables.Address.join( 155 | p => ({ p: p.Person }), 156 | (a, other) => { 157 | a.equal({ id: other.p.id }) 158 | } 159 | ) 160 | .map(f => ({ 161 | id: f.p.id, 162 | name: f.p.name, 163 | address: f.self.address 164 | })) 165 | .orderBy({ p: { name: 'ASC', dob: 'DESC' }, self: { address: 'ASC' } }) 166 | 167 | expect(driver.sql).toEqual([ 168 | 'SELECT "p"."id" AS "p___id","p"."name" AS "p___name","self"."address" AS "self___address" FROM "Address" AS "self" JOIN "Person" AS "p" ON "self"."id" = "p"."id" ORDER BY "p"."name" ASC "p"."dob" DESC "self"."address" ASC' 169 | ]) 170 | 171 | done() 172 | }) 173 | 174 | test('sql-join-limit', async done => { 175 | const { tables } = db 176 | 177 | await tables.Address.join( 178 | p => ({ p: p.Person }), 179 | (a, other) => { 180 | a.equal({ id: other.p.id }) 181 | } 182 | ) 183 | .map(f => ({ 184 | id: f.p.id, 185 | name: f.p.name, 186 | address: f.self.address 187 | })) 188 | .limit(10, 5) 189 | 190 | expect(driver.sql).toEqual([ 191 | 'SELECT "p"."id" AS "p___id","p"."name" AS "p___name","self"."address" AS "self___address" FROM "Address" AS "self" JOIN "Person" AS "p" ON "self"."id" = "p"."id" LIMIT 10 OFFSET 5' 192 | ]) 193 | 194 | done() 195 | }) 196 | 197 | test('sql-join-order-limit', async done => { 198 | const { tables } = db 199 | 200 | await tables.Address.join( 201 | p => ({ p: p.Person }), 202 | (a, other) => { 203 | a.equal({ id: other.p.id }) 204 | } 205 | ) 206 | .map(f => ({ 207 | id: f.p.id, 208 | name: f.p.name, 209 | address: f.self.address 210 | })) 211 | .orderBy({ p: { name: 'ASC', dob: 'DESC' }, self: { address: 'ASC' } }) 212 | .limit(10, 5) 213 | 214 | expect(driver.sql).toEqual([ 215 | 'SELECT "p"."id" AS "p___id","p"."name" AS "p___name","self"."address" AS "self___address" FROM "Address" AS "self" JOIN "Person" AS "p" ON "self"."id" = "p"."id" ORDER BY "p"."name" ASC "p"."dob" DESC "self"."address" ASC LIMIT 10 OFFSET 5' 216 | ]) 217 | 218 | done() 219 | }) 220 | 221 | test('sql-join-where-limit', async done => { 222 | const { tables } = db 223 | 224 | await tables.Address.join( 225 | p => ({ p: p.Person }), 226 | (a, other) => { 227 | a.equal({ id: other.p.id }) 228 | } 229 | ) 230 | .map(f => ({ 231 | id: f.p.id, 232 | name: f.p.name, 233 | address: f.self.address 234 | })) 235 | .where(l => l.self.startsWith({ address: 'foo' })) 236 | .limit(10, 5) 237 | 238 | expect(driver.sql).toEqual([ 239 | 'SELECT "p"."id" AS "p___id","p"."name" AS "p___name","self"."address" AS "self___address" FROM "Address" AS "self" JOIN "Person" AS "p" ON "self"."id" = "p"."id" WHERE "self"."address" LIKE \'foo%\' LIMIT 10 OFFSET 5' 240 | ]) 241 | 242 | done() 243 | }) 244 | }) 245 | -------------------------------------------------------------------------------- /test/sql/select.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-select', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ tables }) => { 11 | driver.reset() 12 | 13 | await tables.Person.select(c => [c.name, c.salary]) 14 | .where(c => c.equals({ id: 1 })) 15 | .orderBy({ dob: 'ASC', id: 'ASC' }) 16 | .limit(1) 17 | 18 | expect(driver.sql).toEqual([ 19 | 'SELECT "name","salary" FROM "Person" WHERE "id" = 1 ORDER BY "dob" ASC "id" ASC LIMIT 1' 20 | ]) 21 | 22 | done() 23 | }) 24 | }) 25 | 26 | test('sql-select-order-limit', done => { 27 | const driver = new TestDriver() 28 | Db.init({ 29 | driver, 30 | entities 31 | }).then(async ({ tables }) => { 32 | driver.reset() 33 | 34 | await tables.Person.select(c => [c.name, c.salary]) 35 | .orderBy({ dob: 'ASC', id: 'ASC' }) 36 | .limit(1) 37 | 38 | expect(driver.sql).toEqual([ 39 | 'SELECT "name","salary" FROM "Person" ORDER BY "dob" ASC "id" ASC LIMIT 1' 40 | ]) 41 | 42 | done() 43 | }) 44 | }) 45 | 46 | test('sql-select-where-limit', done => { 47 | const driver = new TestDriver() 48 | Db.init({ 49 | driver, 50 | entities 51 | }).then(async ({ tables }) => { 52 | driver.reset() 53 | 54 | await tables.Person.select(c => [c.name, c.salary]) 55 | .where(c => c.equals({ id: 1 })) 56 | .limit(1) 57 | 58 | expect(driver.sql).toEqual([ 59 | 'SELECT "name","salary" FROM "Person" WHERE "id" = 1 LIMIT 1' 60 | ]) 61 | 62 | done() 63 | }) 64 | }) 65 | 66 | test('sql-select-limit', done => { 67 | const driver = new TestDriver() 68 | Db.init({ 69 | driver, 70 | entities 71 | }).then(async ({ tables }) => { 72 | driver.reset() 73 | 74 | await tables.Person.select(c => [c.name, c.salary]).limit(1) 75 | 76 | expect(driver.sql).toEqual(['SELECT "name","salary" FROM "Person" LIMIT 1']) 77 | 78 | done() 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /test/sql/single.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-single', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ tables }) => { 11 | driver.reset() 12 | 13 | await tables.Person.single(c => [c.name, c.salary]) 14 | .where(c => c.equals({ id: 1 })) 15 | .orderBy({ dob: 'ASC', id: 'ASC' }) 16 | 17 | expect(driver.sql).toEqual([ 18 | 'SELECT "name","salary" FROM "Person" WHERE "id" = 1 ORDER BY "dob" ASC "id" ASC LIMIT 1' 19 | ]) 20 | 21 | done() 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/sql/transaction.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-transaction', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ transaction }) => { 11 | driver.reset() 12 | 13 | await transaction(({ tables, exec }) => { 14 | exec('DELETE Foo') 15 | exec(tables.Person.update({ id: 1, name: 'Foo' })) 16 | exec( 17 | tables.Person.update({ id: 1, name: 'Foo' }).where(c => 18 | c.equals({ age: 28 }) 19 | ) 20 | ) 21 | exec(tables.Person.insert({ id: 1, name: 'Foo' })) 22 | exec(tables.Person.upsert({ id: 1, name: 'Foo' })) 23 | exec( 24 | tables.Person.insert([ 25 | { id: 1, name: 'Foo' }, 26 | { id: 2, name: 'Bar' }, 27 | { id: 3, name: 'Meh' } 28 | ]) 29 | ) 30 | exec(tables.Person.delete()) 31 | exec(tables.Person.delete().where(c => c.equals({ age: 28 }))) 32 | }) 33 | 34 | expect(driver.sql).toEqual([ 35 | 'DELETE Foo', 36 | `UPDATE "Person" SET "id" = 1, "name" = 'Foo'`, 37 | 'UPDATE "Person" SET "id" = 1, "name" = \'Foo\' WHERE "age" = 28', 38 | `INSERT INTO "Person" ("id","name") VALUES (1,'Foo')`, 39 | `INSERT OR REPLACE INTO "Person" ("id","name") VALUES (1,'Foo')`, 40 | 'INSERT INTO "Person" ("id","name") SELECT 1 AS "id",\'Foo\' AS "name" UNION ALL SELECT 2,\'Bar\' UNION ALL SELECT 3,\'Meh\'', 41 | 'DELETE FROM "Person"', 42 | 'DELETE FROM "Person" WHERE "age" = 28' 43 | ]) 44 | 45 | done() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/sql/update.test.ts: -------------------------------------------------------------------------------- 1 | import { Db } from '../../src' 2 | import { TestDriver } from './driver' 3 | import * as entities from './entity' 4 | 5 | test('sql-insert', done => { 6 | const driver = new TestDriver() 7 | Db.init({ 8 | driver, 9 | entities 10 | }).then(async ({ tables }) => { 11 | driver.reset() 12 | await tables.Person.update({ id: 1, name: 'Foo' }) 13 | expect(driver.sql).toEqual([`UPDATE "Person" SET "id" = 1, "name" = 'Foo'`]) 14 | 15 | driver.reset() 16 | await tables.Person.update({ id: 1, name: 'Foo' }).where(c => 17 | c.equals({ age: 28 }) 18 | ) 19 | expect(driver.sql).toEqual([ 20 | 'UPDATE "Person" SET "id" = 1, "name" = \'Foo\' WHERE "age" = 28' 21 | ]) 22 | 23 | done() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["es6", "dom"], 6 | "sourceMap": true, 7 | "moduleResolution": "node", 8 | "forceConsistentCasingInFileNames": true, 9 | "noImplicitReturns": true, 10 | "noImplicitThis": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "noUnusedLocals": true, 15 | "skipLibCheck": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "jsx": "react-native", 19 | "allowSyntheticDefaultImports": true, 20 | "noEmitHelpers": true, 21 | "importHelpers": true, 22 | "strict": true, 23 | "noUnusedParameters": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": true, 8 | "moduleResolution": "node", 9 | "forceConsistentCasingInFileNames": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUnusedLocals": true, 16 | "skipLibCheck": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "jsx": "react-native", 20 | "allowSyntheticDefaultImports": true, 21 | "noEmitHelpers": true, 22 | "importHelpers": true, 23 | "strict": true, 24 | "noUnusedParameters": true 25 | }, 26 | "exclude": ["node_modules", "typings", "test"] 27 | } 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name": false, 5 | "no-empty-interface": false, 6 | "member-access": false, 7 | "jsx-no-lambda": false, 8 | "object-literal-sort-keys": false, 9 | "jsx-boolean-value": false, 10 | "interface-over-type-literal": false, 11 | "variable-name": false, 12 | "no-angle-bracket-type-assertion": false, 13 | "no-console": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------