├── .eslintignore ├── .gitattributes ├── src └── sqlite3orm │ ├── utils │ ├── index.ts │ ├── sequentialize.ts │ ├── wait.ts │ └── identifiers.ts │ ├── dbcatalog │ ├── index.ts │ ├── DbTableInfo.ts │ └── DbCatalogDAO.ts │ ├── core │ ├── core.ts │ ├── index.ts │ ├── SqlDatabaseSettings.ts │ ├── SqlBackup.ts │ ├── SqlConnectionPoolDatabase.ts │ ├── SqlStatement.ts │ └── SqlConnectionPool.ts │ ├── spec │ ├── fixtures │ │ └── cipher.db │ ├── core │ │ ├── SqlBackup.spec.ts │ │ └── SqlStatement.spec.ts │ ├── metadata │ │ ├── MultiDecoratorsPerModel.spec.ts │ │ ├── datatypes │ │ │ ├── DataTypeJson.spec.ts │ │ │ ├── DataTypeOther.spec.ts │ │ │ ├── DataTypeDate.spec.ts │ │ │ └── DataTypeBoolean.spec.ts │ │ ├── decorators.spec.ts │ │ └── ForeignKey.spec.ts │ ├── query │ │ └── issue74.spec.ts │ ├── ReadmeSample.spec.ts │ └── dbcatalog │ │ └── DbCatalogDAO.spec.ts │ ├── metadata │ ├── PropertyType.ts │ ├── ValueTransformer.ts │ ├── index.ts │ ├── IDXDefinition.ts │ ├── FKDefinition.ts │ ├── DefaultValueTransformers.ts │ ├── MetaProperty.ts │ ├── Field.ts │ ├── Schema.ts │ ├── decorators.ts │ ├── MetaModel.ts │ └── Table.ts │ ├── query │ ├── index.ts │ ├── QueryOperation.ts │ ├── Filter.ts │ ├── QueryModelPredicates.ts │ ├── QueryCondition.ts │ ├── Where.ts │ ├── QueryPropertyPredicate.ts │ ├── QueryModel.ts │ └── QueryModelBase.ts │ └── index.ts ├── spec ├── support │ └── jasmine.json └── jasmine-runner.js ├── .npm-upgrade.json ├── .npmignore ├── .clang-format ├── Makefile ├── .compodocrc.json ├── typedoc.json ├── docs ├── external-sqlite3.md └── sqlcipher.md ├── .prettierrc.js ├── .snyk ├── .editorconfig ├── .gitignore ├── .eslintrc.js ├── tsconfig.json ├── .github └── workflows │ └── build.yml ├── LICENSE ├── package.json └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .nyc_output 5 | tmp 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-vendored 2 | ~ 3 | -------------------------------------------------------------------------------- /src/sqlite3orm/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './identifiers'; 2 | export * from './sequentialize'; 3 | -------------------------------------------------------------------------------- /src/sqlite3orm/dbcatalog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DbTableInfo'; 2 | export * from './DbCatalogDAO'; 3 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "/src/sqlite3orm/spec", 3 | "spec_files": ["**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/sqlite3orm/core/core.ts: -------------------------------------------------------------------------------- 1 | import * as _dbg from 'debug'; 2 | export const debugORM = _dbg('sqlite3orm:orm'); 3 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/fixtures/cipher.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gms1/node-sqlite3-orm/HEAD/src/sqlite3orm/spec/fixtures/cipher.db -------------------------------------------------------------------------------- /.npm-upgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": { 3 | "prettier": { 4 | "versions": ">= 2.0", 5 | "reason": "" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | # -- generated output: 3 | 4 | *.spec.d.ts 5 | *.spec.js 6 | *.tgz 7 | 8 | # -- installed dependencies: 9 | node_modules 10 | 11 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/PropertyType.ts: -------------------------------------------------------------------------------- 1 | export enum PropertyType { 2 | UNKNOWN = 0, 3 | BOOLEAN = 1, 4 | STRING, 5 | NUMBER, 6 | DATE, 7 | } 8 | -------------------------------------------------------------------------------- /src/sqlite3orm/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Filter'; 2 | export * from './QueryModel'; 3 | export * from './QueryModelBase'; 4 | export * from './Where'; 5 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 120 4 | JavaScriptQuotes: Single 5 | JavaScriptWrapImports: true 6 | AlignEscapedNewlinesLeft: true 7 | SortIncludes: true 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default test 2 | 3 | NODE_VERSION := $(shell node -v | awk -F. '{sub(/v/,""); print $$1}') 4 | 5 | default: test 6 | 7 | test: 8 | npm run release:build 9 | npm run coverage:report 10 | npm run coverage:html 11 | -------------------------------------------------------------------------------- /src/sqlite3orm/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './SqlConnectionPool'; 3 | export * from './SqlDatabase'; 4 | export * from './SqlDatabaseSettings'; 5 | export * from './SqlStatement'; 6 | export * from './SqlBackup'; 7 | -------------------------------------------------------------------------------- /.compodocrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlite3orm", 3 | "tsconfig": "tsconfig.json", 4 | "output": "docs/sqlite3orm", 5 | "theme": "vagrant", 6 | "disablePrivate": true, 7 | "disableProtected": true, 8 | "disableInternal": true 9 | } 10 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "modules", 3 | "excludeExternals": true, 4 | "excludeNotExported": true, 5 | "excludePrivate": true, 6 | "ignoreCompilerErrors": true, 7 | "tsconfig": "tsconfig.json", 8 | "readme": "./README.md" 9 | } 10 | -------------------------------------------------------------------------------- /src/sqlite3orm/query/QueryOperation.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import { MetaModel } from '../metadata'; 3 | 4 | export interface QueryOperation { 5 | toSql(metaModel: MetaModel, params: Object, tablePrefix: string): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/ValueTransformer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export interface ValueTransformer { 3 | /* 4 | * serialize property value to DB 5 | */ 6 | toDB(input: any): any; 7 | 8 | /* 9 | * deserialize property value from DB 10 | */ 11 | fromDB(input: any): any; 12 | } 13 | -------------------------------------------------------------------------------- /docs/external-sqlite3.md: -------------------------------------------------------------------------------- 1 | # build node-sqlite3 using external sqlite3 2 | 3 | ```shell 4 | SQLITE3_HOME="/path/to/sqlite3" 5 | export LD_RUN_PATH="${SQLITE3_HOME}/lib" 6 | 7 | npm install sqlite3 --build-from-source --sqlite="${SQLITE3_HOME} 2>&1 | tee sqlite3.log 8 | ldd node_modules/sqlite3/lib/binding/node-v64-linux-x64/node_sqlite3.node | grep sqlite 9 | ``` 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | jsxSingleQuote: false, 8 | trailingComma: 'all', 9 | bracketSpacing: true, 10 | jsxBracketSameLine: false, 11 | arrowParens: 'always', 12 | proseWrap: 'preserve', 13 | htmlWhitespaceSensitivity: 'ignore', 14 | }; 15 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.13.3 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | SNYK-JS-CHOWNR-73502: 6 | - sqlite3 > node-pre-gyp > tar > chownr: 7 | reason: only dev-dependency and no fix available 8 | expires: '2020-01-11T08:09:32.501Z' 9 | patch: {} 10 | -------------------------------------------------------------------------------- /docs/sqlcipher.md: -------------------------------------------------------------------------------- 1 | # using SQLCipher 2 | 3 | - install sqlcipher or build your own 4 | - rebuild node-sqlite3 using sqlcipher see [building for sqlcipher](https://github.com/TryGhost/node-sqlite3#building-for-sqlcipher) 5 | 6 | e.g for Debian/Ubuntu run: 7 | 8 | ```bash 9 | sudo apt install sqlcipher libsqlcipher-dev 10 | npm install sqlite3 --build-from-source --sqlite_libname=sqlcipher --sqlite=/usr 11 | ``` 12 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decorators'; 2 | export * from './DefaultValueTransformers'; 3 | export * from './Field'; 4 | export * from './FKDefinition'; 5 | export * from './IDXDefinition'; 6 | export * from './MetaModel'; 7 | export * from './MetaProperty'; 8 | export * from './PropertyType'; 9 | export * from './Schema'; 10 | export * from './Table'; 11 | export * from './ValueTransformer'; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | max_line_length = 250 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [Makefile] 18 | indent_style = tab 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /src/sqlite3orm/utils/sequentialize.ts: -------------------------------------------------------------------------------- 1 | export declare type PromiseFactory = () => Promise; 2 | export declare type PromiseFactories = PromiseFactory[]; 3 | 4 | export function sequentialize(factories: PromiseFactories): Promise { 5 | return factories.reduce( 6 | (promise, factory) => 7 | promise.then((results) => factory().then((result) => results.concat(result))), 8 | Promise.resolve([]), 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /spec/jasmine-runner.js: -------------------------------------------------------------------------------- 1 | let Jasmine = require('jasmine'); 2 | let SpecReporter = require('jasmine-spec-reporter').SpecReporter; 3 | 4 | let jrunner = new Jasmine(); 5 | jrunner.env.clearReporters(); // remove default reporter logs 6 | jrunner.addReporter(new SpecReporter({ // add jasmine-spec-reporter 7 | spec: { 8 | displayPending: true 9 | } 10 | })); 11 | jrunner.loadConfigFile(); // load jasmine.json configuration 12 | jrunner.execute(); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | #--------------------------------------- 4 | # npm: 5 | 6 | /node_modules 7 | npm-debug.log* 8 | 9 | #--------------------------------------- 10 | # IDEs and editors: 11 | 12 | /.vscode 13 | 14 | #--------------------------------------- 15 | # compiled output: 16 | 17 | /dist 18 | /docs/sqlite3orm 19 | /.nyc_output 20 | /coverage 21 | /tmp 22 | 23 | #--------------------------------------- 24 | # temporary files: 25 | 26 | *:memory* 27 | file:sqlite3orm* 28 | test*.db 29 | file 30 | -------------------------------------------------------------------------------- /src/sqlite3orm/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export function wait( 2 | cond: () => boolean, 3 | timeout: number = 0, 4 | intervall: number = 100, 5 | ): Promise { 6 | return new Promise((resolve, reject) => { 7 | let counter = 0; 8 | const timer = setInterval(() => { 9 | if (cond()) { 10 | clearInterval(timer); 11 | resolve(); 12 | return; 13 | } 14 | if (timeout > 0 && ++counter * intervall >= timeout) { 15 | clearInterval(timer); 16 | reject(new Error('timeout reached')); 17 | return; 18 | } 19 | }, intervall); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | project: 'tsconfig.json', 11 | sourceType: 'module', 12 | }, 13 | plugins: [ 14 | 'eslint-plugin-prefer-arrow', 15 | 'eslint-plugin-import', 16 | 'eslint-plugin-no-null', 17 | 'eslint-plugin-jsdoc', 18 | '@typescript-eslint', 19 | 'deprecation', 20 | ], 21 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 22 | rules: { 23 | '@typescript-eslint/no-inferrable-types': 'off', 24 | 'no-empty': 'off', 25 | 'deprecation/deprecation': 'warn', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "removeComments": false, 7 | "stripInternal": false, 8 | "rootDir": "src/sqlite3orm", 9 | "outDir": "./dist", 10 | "declaration": true, 11 | "sourceMap": true, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": false, 17 | "preserveConstEnums": true, 18 | "skipLibCheck": true, 19 | "strictNullChecks": true, 20 | "strict": true, 21 | "useUnknownInCatchVariables": false, 22 | "typeRoots": ["node_modules/@types"], 23 | "types": ["node", "jasmine"] 24 | }, 25 | "include": ["./src/**/*"], 26 | "exclude": ["./dist/**/*", "./node_modules/**/*"] 27 | } 28 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/IDXDefinition.ts: -------------------------------------------------------------------------------- 1 | export interface IDXFieldDefinition { 2 | name: string; 3 | desc?: boolean; 4 | } 5 | 6 | export class IDXDefinition { 7 | readonly name: string; 8 | isUnique?: boolean; 9 | readonly fields: IDXFieldDefinition[]; 10 | 11 | get id(): string { 12 | return IDXDefinition.genericIndexId( 13 | this.name, 14 | this.fields.map((field) => (field.desc ? `${field.name} DESC` : field.name)), 15 | this.isUnique, 16 | ); 17 | } 18 | 19 | constructor(name: string, isUnique?: boolean) { 20 | this.name = name; 21 | this.isUnique = isUnique; 22 | this.fields = []; 23 | } 24 | 25 | static genericIndexId(name: string, fields: string[], isUnique?: boolean): string { 26 | let res = name; 27 | res += '('; 28 | res += fields.join(','); 29 | res += ')'; 30 | if (isUnique) { 31 | res += ':UNIQUE'; 32 | } 33 | return res; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/sqlite3orm/query/Filter.ts: -------------------------------------------------------------------------------- 1 | import { Where } from './Where'; 2 | 3 | export type Columns = { 4 | [K in keyof MT]?: boolean; // true: include, false: exclude 5 | }; 6 | 7 | export type OrderColumns = { 8 | [K in keyof MT]?: boolean; // true: ascending, false: descending 9 | }; 10 | 11 | export interface Filter { 12 | select?: Columns; 13 | where?: Where; 14 | order?: OrderColumns; 15 | limit?: number; 16 | offset?: number; 17 | 18 | tableAlias?: string; 19 | } 20 | 21 | export function isFilter(whereOrFilter?: Where | Filter): whereOrFilter is Filter { 22 | return whereOrFilter && 23 | ((whereOrFilter as Filter).select !== undefined || 24 | (whereOrFilter as Filter).where !== undefined || 25 | (whereOrFilter as Filter).order !== undefined || 26 | (whereOrFilter as Filter).limit !== undefined || 27 | (whereOrFilter as Filter).offset !== undefined || 28 | (whereOrFilter as Filter).tableAlias !== undefined) 29 | ? true 30 | : false; 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Workflow 2 | "on": 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | Build: 11 | runs-on: "${{ matrix.os }}" 12 | strategy: 13 | matrix: 14 | os: 15 | - ubuntu-20.04 16 | node-version: 17 | - 16.x 18 | steps: 19 | - name: "Set up Node.js ${{ matrix.node-version }}" 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: "${{ matrix.node-version }}" 23 | - uses: actions/checkout@v2 24 | - name: "Install dependencies" 25 | run: "npm ci" 26 | - name: "Build package" 27 | run: "npm run release:build" 28 | - name: "Coverage Report" 29 | run: | 30 | npm run coverage:report 31 | npm run coverage:html 32 | npm run coverage:codecov 33 | - name: Upload coverage to Codecov 34 | uses: codecov/codecov-action@v2 35 | with: 36 | directory: ./coverage/ 37 | fail_ci_if_error: true 38 | verbose: true 39 | -------------------------------------------------------------------------------- /src/sqlite3orm/dbcatalog/DbTableInfo.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export interface DbColumnTypeInfo { 3 | typeAffinity: string; 4 | notNull: boolean; 5 | defaultValue: any; 6 | } 7 | 8 | export interface DbColumnInfo extends DbColumnTypeInfo { 9 | name: string; 10 | type: string; 11 | } 12 | 13 | export interface DbIndexColumnInfo { 14 | name: string; 15 | desc: boolean; 16 | coll: string; // collating sequence 17 | key: boolean; // key (true) or auxiliary (false) column 18 | } 19 | 20 | export interface DbIndexInfo { 21 | name: string; 22 | unique: boolean; 23 | columns: DbIndexColumnInfo[]; 24 | partial: boolean; 25 | } 26 | 27 | export interface DbForeignKeyInfo { 28 | columns: string[]; 29 | refTable: string; 30 | refColumns: string[]; 31 | } 32 | 33 | export interface DbTableInfo { 34 | name: string; 35 | tableName: string; 36 | schemaName: string; 37 | columns: { [key: string]: DbColumnInfo }; 38 | primaryKey: string[]; 39 | autoIncrement?: boolean; 40 | indexes: { [key: string]: DbIndexInfo }; 41 | foreignKeys: { [key: string]: DbForeignKeyInfo }; 42 | } 43 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/FKDefinition.ts: -------------------------------------------------------------------------------- 1 | // import * as core from './core'; 2 | 3 | /** 4 | * class holding a foreign key definition 5 | * 6 | * @class FKDefinition 7 | */ 8 | 9 | export interface FKFieldDefinition { 10 | name: string; 11 | foreignColumnName: string; 12 | } 13 | 14 | export class FKDefinition { 15 | readonly name: string; 16 | readonly foreignTableName: string; 17 | readonly fields: FKFieldDefinition[]; 18 | 19 | get id(): string { 20 | return FKDefinition.genericForeignKeyId( 21 | this.fields.map((field) => field.name), 22 | this.foreignTableName, 23 | this.fields.map((field) => field.foreignColumnName), 24 | ); 25 | } 26 | 27 | public constructor(name: string, foreignTableName: string) { 28 | this.name = name; 29 | this.foreignTableName = foreignTableName; 30 | this.fields = []; 31 | } 32 | 33 | static genericForeignKeyId(fromCols: string[], toTable: string, toCols: string[]): string { 34 | let res = '('; 35 | res += fromCols.join(','); 36 | res += `) => ${toTable}(`; 37 | res += toCols.join(','); 38 | res += ')'; 39 | return res; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2021 Guenter Sandner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/sqlite3orm/index.ts: -------------------------------------------------------------------------------- 1 | export { AutoUpgrader, UpgradeInfo, UpgradeMode } from './AutoUpgrader'; 2 | export { BaseDAO, BaseDAOInsertMode, BaseDAOOptions } from './BaseDAO'; 3 | export { 4 | SQL_DEFAULT_SCHEMA, 5 | SQL_MEMORY_DB_PRIVATE, 6 | SQL_MEMORY_DB_SHARED, 7 | SQL_OPEN_CREATE, 8 | SQL_OPEN_DEFAULT, 9 | SQL_OPEN_DEFAULT_URI, 10 | SQL_OPEN_DEFAULT_NO_URI, 11 | SQL_OPEN_READONLY, 12 | SQL_OPEN_READWRITE, 13 | SQL_OPEN_URI, 14 | SQL_OPEN_SHAREDCACHE, 15 | SQL_OPEN_PRIVATECACHE, 16 | SqlConnectionPool, 17 | SqlDatabase, 18 | SqlDatabaseSettings, 19 | SqlRunResult, 20 | SqlStatement, 21 | SqlBackup, 22 | } from './core'; 23 | export { 24 | DbCatalogDAO, 25 | DbColumnInfo, 26 | DbForeignKeyInfo, 27 | DbIndexInfo, 28 | DbTableInfo, 29 | } from './dbcatalog'; 30 | export { 31 | field, 32 | Field, 33 | FieldOpts, 34 | fk, 35 | FKDefinition, 36 | FKFieldDefinition, 37 | getModelMetadata, 38 | id, 39 | IDXDefinition, 40 | IDXFieldDefinition, 41 | index, 42 | METADATA_MODEL_KEY, 43 | MetaModel, 44 | MetaProperty, 45 | PropertyType, 46 | Schema, 47 | schema, 48 | table, 49 | Table, 50 | TableOpts, 51 | ValueTransformer, 52 | } from './metadata'; 53 | export { 54 | Columns, 55 | Condition, 56 | Filter, 57 | OrderColumns, 58 | PropertyComparisons, 59 | PropertyPredicates, 60 | QueryModel, 61 | TABLEALIAS, 62 | Where, 63 | } from './query'; 64 | 65 | export * from './utils'; 66 | -------------------------------------------------------------------------------- /src/sqlite3orm/utils/identifiers.ts: -------------------------------------------------------------------------------- 1 | import { SQL_DEFAULT_SCHEMA } from '../core/SqlDatabase'; 2 | 3 | // ----------------------------------------------------------------- 4 | 5 | export function quoteSimpleIdentifier(name: string): string { 6 | return '"' + name.replace(/"/g, '""') + '"'; 7 | } 8 | 9 | export function backtickQuoteSimpleIdentifier(name: string): string { 10 | return '`' + name.replace(/`/g, '``') + '`'; 11 | } 12 | 13 | // ----------------------------------------------------------------- 14 | 15 | export function quoteIdentifiers(name: string): string[] { 16 | return name.split('.').map((value) => quoteSimpleIdentifier(value)); 17 | } 18 | 19 | export function quoteIdentifier(name: string): string { 20 | return quoteIdentifiers(name).join('.'); 21 | } 22 | 23 | // ----------------------------------------------------------------- 24 | 25 | export function unqualifyIdentifier(name: string): string { 26 | return name.split('.').pop() as string; 27 | } 28 | 29 | export function quoteAndUnqualifyIdentifier(name: string): string { 30 | return quoteSimpleIdentifier(unqualifyIdentifier(name)); 31 | } 32 | 33 | // ----------------------------------------------------------------- 34 | 35 | export function qualifiySchemaIdentifier(name: string, schema?: string): string { 36 | if (name.indexOf('.') !== -1) { 37 | /* istanbul ignore if */ 38 | if (schema && name.split('.').shift() !== schema) { 39 | throw new Error(`failed to qualify '${name}' by '${schema}`); 40 | } 41 | return name; 42 | } 43 | schema = schema || SQL_DEFAULT_SCHEMA; 44 | return `${schema}.${name}`; 45 | } 46 | 47 | export function splitSchemaIdentifier(name: string): { identName: string; identSchema?: string } { 48 | const identifiers = name.split('.'); 49 | 50 | if (identifiers.length >= 2) { 51 | return { identSchema: identifiers.shift(), identName: identifiers.join('.') }; 52 | } else { 53 | return { identName: identifiers[0] }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/sqlite3orm/query/QueryModelPredicates.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import { MetaModel } from '../metadata'; 3 | 4 | import { QueryOperation } from './QueryOperation'; 5 | import { QueryPropertyPredicate } from './QueryPropertyPredicate'; 6 | import { getPropertyComparison, getPropertyPredicates, ModelPredicates } from './Where'; 7 | 8 | export class QueryModelPredicates implements QueryOperation { 9 | subOperations: QueryPropertyPredicate[]; 10 | 11 | constructor(pred: ModelPredicates) { 12 | this.subOperations = []; 13 | const keys = Object.keys(pred); 14 | keys.forEach((propertyKey) => { 15 | const propertyPredicates = getPropertyPredicates(pred, propertyKey as keyof MT); 16 | 17 | if ( 18 | typeof propertyPredicates !== 'object' || 19 | propertyPredicates instanceof Date || 20 | propertyPredicates instanceof Promise 21 | ) { 22 | // shorthand form for 'eq' comparison 23 | this.subOperations.push(new QueryPropertyPredicate(propertyKey, 'eq', propertyPredicates)); 24 | } else { 25 | const comparisonKeys = Object.keys(propertyPredicates); 26 | comparisonKeys.forEach((comparisonOp) => { 27 | const comparison = getPropertyComparison(propertyPredicates, comparisonOp); 28 | this.subOperations.push( 29 | new QueryPropertyPredicate(propertyKey, comparisonOp, comparison), 30 | ); 31 | }); 32 | } 33 | }); 34 | } 35 | 36 | async toSql(metaModel: MetaModel, params: Object, tablePrefix: string): Promise { 37 | const parts: string[] = []; 38 | for (const predicate of this.subOperations) { 39 | const part = await predicate.toSql(metaModel, params, tablePrefix); 40 | /* istanbul ignore else */ 41 | if (part.length) { 42 | parts.push(part); 43 | } 44 | } 45 | if (!parts.length) { 46 | return ''; 47 | } 48 | return parts.join(' and '); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/sqlite3orm/core/SqlDatabaseSettings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * for a description of the pragma setting see: https://www.sqlite.org/pragma.html 3 | * for a description of the execution mode see: https://github.com/mapbox/node-sqlite3/wiki/Control-Flow 4 | */ 5 | 6 | export interface SqlDatabaseSettings { 7 | /* 8 | * PRAGMA schema.journal_mode = DELETE | TRUNCATE | PERSIST | MEMORY | WAL | OFF 9 | * for multiple schemas use e.g [ 'temp.OFF', 'main.WAL' ] 10 | */ 11 | journalMode?: string | string[]; 12 | /* 13 | * PRAGMA busy_timeout = milliseconds 14 | */ 15 | busyTimeout?: number; 16 | /* 17 | * PRAGMA schema.synchronous = OFF | NORMAL | FULL | EXTRA; 18 | * for multiple schemas use e.g [ 'temp.OFF', 'main.FULL' ] 19 | */ 20 | synchronous?: string | string[]; 21 | /* 22 | * PRAGMA case_sensitive_like = TRUE | FALSE 23 | */ 24 | caseSensitiveLike?: string; 25 | 26 | /* 27 | * PRAGMA foreign_keys = TRUE | FALSE 28 | */ 29 | foreignKeys?: string; 30 | 31 | /* 32 | * PRAGMA ignore_check_constraints = TRUE | FALSE 33 | */ 34 | ignoreCheckConstraints?: string; 35 | 36 | /* 37 | * PRAGMA query_only = TRUE | FALSE 38 | */ 39 | queryOnly?: string; 40 | 41 | /* 42 | * PRAGMA read_uncommitted = TRUE | FALSE 43 | */ 44 | readUncommitted?: string; 45 | 46 | /* 47 | * PRAGMA recursive_triggers = TRUE | FALSE 48 | */ 49 | recursiveTriggers?: string; 50 | 51 | /* 52 | * PRAGMA schema.secure_delete = TRUE | FALSE | FAST 53 | * for multiple schemas use e.g [ 'temp.OFF', 'main.FAST' ] 54 | */ 55 | secureDelete?: string | string[]; 56 | 57 | /* 58 | * SERIALIZE | PARALLELIZE 59 | */ 60 | executionMode?: string; 61 | 62 | /* 63 | * PRAGMA cipher_compatibility = 1 | 2 | 3 | 4 64 | * see: https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_compatibility 65 | * only available if node-sqlite3 has been compiled with sqlcipher support 66 | */ 67 | cipherCompatibility?: number; 68 | 69 | /* 70 | * PRAGMA key = 'passphrase'; 71 | * see: https://www.zetetic.net/sqlcipher/sqlcipher-api/#PRAGMA_key 72 | * only available if node-sqlite3 has been compiled with sqlcipher support 73 | */ 74 | key?: string; 75 | } 76 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/core/SqlBackup.spec.ts: -------------------------------------------------------------------------------- 1 | import { SqlDatabase, SQL_MEMORY_DB_PRIVATE } from '../../core'; 2 | 3 | describe('test SqlBackup', () => { 4 | const TARGET_DB_FILE = 'testBackup.db'; 5 | let sqlSourceDb: SqlDatabase; 6 | 7 | beforeEach(async () => { 8 | sqlSourceDb = new SqlDatabase(); 9 | await sqlSourceDb.open(SQL_MEMORY_DB_PRIVATE); 10 | await sqlSourceDb.exec( 11 | 'CREATE TABLE TEST (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, col VARCHAR(50))', 12 | ); 13 | await sqlSourceDb.run('INSERT INTO TEST (id, col) values (:a, :b)', { 14 | ':a': 0, 15 | ':b': 'testvalue 0', 16 | }); 17 | }); 18 | 19 | it('should backup and restore to/from file', async () => { 20 | const sqlBackup = await sqlSourceDb.backup(TARGET_DB_FILE); 21 | expect(sqlBackup.idle).toBe(true); 22 | expect(sqlBackup.completed).toBe(false); 23 | expect(sqlBackup.failed).toBe(false); 24 | expect(sqlBackup.progress).toBe(0); 25 | 26 | await sqlBackup.step(-1); 27 | expect(sqlBackup.idle).toBe(true); 28 | expect(sqlBackup.completed).toBe(true); 29 | expect(sqlBackup.failed).toBe(false); 30 | expect(sqlBackup.progress).toBe(100); 31 | expect(sqlBackup.pageCount).toBeGreaterThanOrEqual(1); 32 | 33 | await sqlSourceDb.exec('DELETE FROM TEST'); 34 | 35 | const res1 = await sqlSourceDb.all('SELECT id, col FROM TEST order by id'); 36 | expect(res1.length).toBe(0); 37 | 38 | // await sqlSourceDb.exec('DROP TABLE TEST'); 39 | 40 | const sqlRestore = await sqlSourceDb.backup(TARGET_DB_FILE, false); 41 | expect(sqlRestore.idle).toBe(true); 42 | expect(sqlRestore.completed).toBe(false); 43 | expect(sqlRestore.failed).toBe(false); 44 | expect(sqlRestore.progress).toBe(0); 45 | 46 | await sqlRestore.step(-1); 47 | expect(sqlRestore.idle).toBe(true); 48 | expect(sqlRestore.completed).toBe(true); 49 | expect(sqlRestore.failed).toBe(false); 50 | expect(sqlRestore.progress).toBe(100); 51 | expect(sqlBackup.pageCount).toBeGreaterThanOrEqual(1); 52 | 53 | const res2 = await sqlSourceDb.all('SELECT id, col FROM TEST order by id'); 54 | expect(res2.length).toBe(1); 55 | expect(res2[0].id).toBe(0); 56 | expect(res2[0].col).toBe('testvalue 0'); 57 | 58 | sqlBackup.finish(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/metadata/MultiDecoratorsPerModel.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { field, fk, id, index, table } from '../..'; 3 | 4 | // --------------------------------------------- 5 | 6 | describe('test multiple decorators per model', () => { 7 | // --------------------------------------------- 8 | it('two table decorators', async () => { 9 | try { 10 | @table({ name: 'MDM1:T' }) 11 | @table({ name: 'MDM1:T' }) 12 | class Model { 13 | @id({ name: 'MDM1:ID' }) 14 | id!: number; 15 | 16 | @field({ name: 'MDM1:COL' }) 17 | col?: number; 18 | } 19 | fail('should have thrown'); 20 | } catch (err) { 21 | expect((err.message as string).indexOf('already')).not.toBe(-1); 22 | } 23 | }); 24 | 25 | // --------------------------------------------- 26 | it('two field decorators', async () => { 27 | try { 28 | @table({ name: 'MDM2:T' }) 29 | class Model { 30 | @id({ name: 'MDM2:ID' }) 31 | id!: number; 32 | 33 | @field({ name: 'MDM2:COL' }) 34 | @field({ name: 'MDM2:COL2' }) 35 | col?: number; 36 | } 37 | fail('should have thrown'); 38 | } catch (err) { 39 | expect((err.message as string).indexOf('already')).not.toBe(-1); 40 | } 41 | }); 42 | 43 | // --------------------------------------------- 44 | it('two index decorators', async () => { 45 | try { 46 | @table({ name: 'MDM3:T' }) 47 | class Model { 48 | @id({ name: 'MDM3:ID' }) 49 | id!: number; 50 | 51 | @field({ name: 'MDM3:COL' }) 52 | @index('MPT3:IDX1') 53 | @index('MPT3:IDX1') 54 | col?: number; 55 | } 56 | fail('should have thrown'); 57 | } catch (err) { 58 | expect((err.message as string).indexOf('already')).not.toBe(-1); 59 | } 60 | }); 61 | 62 | // --------------------------------------------- 63 | it('conflicting foreign key decorators', async () => { 64 | try { 65 | @table({ name: 'MDM4:T' }) 66 | class Model { 67 | @id({ name: 'MDM4:ID' }) 68 | id!: number; 69 | 70 | @field({ name: 'MDM4:COL' }) 71 | @fk('MDM4:FK1', 'MDM4:T1', 'MDM4:ID') 72 | @fk('MDM4:FK1', 'MDM4:T1', 'MDM4:ID') 73 | col?: number; 74 | } 75 | fail('should have thrown'); 76 | } catch (err) { 77 | expect((err.message as string).indexOf('already')).not.toBe(-1); 78 | } 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/sqlite3orm/core/SqlBackup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | 4 | // online backup 5 | // https://github.com/mapbox/node-sqlite3/pull/1116 6 | // TODO(Backup API): typings not yet available 7 | import * as _dbg from 'debug'; 8 | const debug = _dbg('sqlite3orm:backup'); 9 | 10 | export class SqlBackup { 11 | private readonly backup: any; 12 | 13 | get idle(): boolean { 14 | return this.backup.idle; 15 | } 16 | 17 | get completed(): boolean { 18 | return this.backup.completed; 19 | } 20 | 21 | get failed(): boolean { 22 | return this.backup.failed; 23 | } 24 | 25 | /** 26 | * Returns an integer with the remaining number of pages left to copy 27 | * Returns -1 if `step` not yet called 28 | * 29 | */ 30 | get remaining(): number { 31 | return this.backup.remaining; 32 | } 33 | 34 | /** 35 | * Returns an integer with the total number of pages 36 | * Returns -1 if `step` not yet called 37 | * 38 | */ 39 | get pageCount(): number { 40 | return this.backup.pageCount; 41 | } 42 | 43 | /** 44 | * Returns the progress (percentage completion) 45 | * 46 | */ 47 | get progress(): number { 48 | const pageCount = this.pageCount; 49 | const remaining = this.remaining; 50 | if (pageCount === -1 || remaining === -1) { 51 | return 0; 52 | } 53 | return pageCount === 0 ? 100 : ((pageCount - remaining) / pageCount) * 100; 54 | } 55 | 56 | /** 57 | * Creates an instance of SqlBackup. 58 | * 59 | * @param backup 60 | */ 61 | constructor(backup: any) { 62 | this.backup = backup; 63 | debug(`backup initialized: page count: ${this.pageCount}`); 64 | } 65 | 66 | step(pages: number = -1): Promise { 67 | return new Promise((resolve, reject) => { 68 | /* istanbul ignore if */ 69 | if (!this.backup) { 70 | const err = new Error('backup handle not open'); 71 | debug(`step '${pages}' failed: ${err.message}`); 72 | reject(err); 73 | return; 74 | } 75 | this.backup.step(pages, (err: any) => { 76 | /* istanbul ignore if */ 77 | if (err) { 78 | debug(`step '${pages}' failed: ${err.message}`); 79 | reject(err); 80 | } 81 | debug(`step '${pages}' succeeded`); 82 | resolve(); 83 | }); 84 | }); 85 | } 86 | 87 | finish(): void { 88 | debug(`finished`); 89 | this.backup.finish(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/metadata/datatypes/DataTypeJson.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseDAO, field, id, schema, SQL_MEMORY_DB_PRIVATE, SqlDatabase, table } from '../../..'; 2 | 3 | const DATATYPE_JSON_TABLE = 'DJ:DATATYPE_JSON'; 4 | 5 | interface JsonData { 6 | notes: string; 7 | scores: number[]; 8 | } 9 | 10 | @table({ name: DATATYPE_JSON_TABLE, autoIncrement: true }) 11 | class DataTypeJson { 12 | @id({ name: 'id', dbtype: 'INTEGER NOT NULL' }) 13 | id!: number; 14 | 15 | @field({ name: 'my_json_text', dbtype: 'TEXT', isJson: true }) 16 | myJsonData?: JsonData; 17 | } 18 | 19 | describe('test Json data', () => { 20 | let sqldb: SqlDatabase; 21 | let dao: BaseDAO; 22 | // --------------------------------------------- 23 | beforeEach(async () => { 24 | try { 25 | sqldb = new SqlDatabase(); 26 | await sqldb.open(SQL_MEMORY_DB_PRIVATE); 27 | await schema().createTable(sqldb, DATATYPE_JSON_TABLE); 28 | dao = new BaseDAO(DataTypeJson, sqldb); 29 | } catch (err) { 30 | fail(err); 31 | } 32 | }); 33 | 34 | it('expect reading/writing Json properties from/to the database to succeed', async () => { 35 | try { 36 | // write 37 | const model: DataTypeJson = new DataTypeJson(); 38 | model.myJsonData = { notes: 'hello', scores: [3, 5, 1] }; 39 | await dao.insert(model); 40 | 41 | // read 42 | const model2: DataTypeJson = await dao.select(model); 43 | expect(model2.id).toBe(model.id); 44 | expect(model2.myJsonData).toBeDefined(); 45 | if (!model2.myJsonData) { 46 | throw new Error('this should not happen'); 47 | } 48 | expect(model2.myJsonData.notes).toBe(model.myJsonData.notes); 49 | expect(model2.myJsonData.scores.length).toBe(model2.myJsonData.scores.length); 50 | expect(model2.myJsonData.scores[0]).toBe(model2.myJsonData.scores[0]); 51 | expect(model2.myJsonData.scores[1]).toBe(model2.myJsonData.scores[1]); 52 | expect(model2.myJsonData.scores[2]).toBe(model2.myJsonData.scores[2]); 53 | } catch (err) { 54 | fail(err); 55 | } 56 | }); 57 | 58 | it('expect reading/writing Json properties from/to the database to succeed', async () => { 59 | try { 60 | // write 61 | const model: DataTypeJson = new DataTypeJson(); 62 | await dao.insert(model); 63 | 64 | // read 65 | const model2: DataTypeJson = await dao.select(model); 66 | expect(model2.id).toBe(model.id); 67 | expect(model2.myJsonData).toBeUndefined(); 68 | } catch (err) { 69 | fail(err); 70 | } 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/sqlite3orm/query/QueryCondition.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { MetaModel } from '../metadata'; 4 | 5 | import { QueryModelPredicates } from './QueryModelPredicates'; 6 | import { QueryOperation } from './QueryOperation'; 7 | import { Condition, isModelPredicates, LogicalOperatorType, ModelPredicates } from './Where'; 8 | 9 | export class QueryCondition implements QueryOperation { 10 | readonly op!: LogicalOperatorType; 11 | readonly subOperations: (QueryCondition | QueryModelPredicates)[]; 12 | sql: string; 13 | 14 | constructor(cond: Condition) { 15 | this.subOperations = []; 16 | this.sql = ''; 17 | const keys = Object.keys(cond); 18 | /* istanbul ignore if */ 19 | if (keys.length !== 1) { 20 | throw new Error(`unknown operation: ${keys.toString()}`); 21 | } 22 | const key = keys[0]; 23 | /* istanbul ignore if */ 24 | if (key !== 'not' && key !== 'and' && key !== 'or' && key !== 'sql') { 25 | throw new Error(`unknown operation: '${key}'`); 26 | } 27 | this.op = key; 28 | if (this.op === 'sql') { 29 | this.sql = (cond as any)[key] as string; 30 | } else if (this.op === 'not') { 31 | const value = (cond as any)[key] as Condition | ModelPredicates; 32 | if (isModelPredicates(value)) { 33 | this.subOperations.push(new QueryModelPredicates(value)); 34 | } else { 35 | this.subOperations.push(new QueryCondition(value)); 36 | } 37 | } else { 38 | const value = (cond as any)[key] as (Condition | ModelPredicates)[]; 39 | value.forEach((item) => { 40 | if (isModelPredicates(item)) { 41 | this.subOperations.push(new QueryModelPredicates(item)); 42 | } else { 43 | this.subOperations.push(new QueryCondition(item)); 44 | } 45 | }); 46 | } 47 | } 48 | 49 | async toSql(metaModel: MetaModel, params: Object, tablePrefix: string): Promise { 50 | if (this.op === 'sql') { 51 | return this.sql; 52 | } 53 | const parts: string[] = []; 54 | for (const subOperation of this.subOperations) { 55 | const part = await subOperation.toSql(metaModel, params, tablePrefix); 56 | if (part.length) { 57 | parts.push(part); 58 | } 59 | } 60 | if (!parts.length) { 61 | return ''; 62 | } 63 | switch (this.op) { 64 | case 'not': 65 | return `not (${parts[0]})`; 66 | case 'and': 67 | return '(' + parts.join(') and (') + ')'; 68 | case 'or': 69 | return '(' + parts.join(') or (') + ')'; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/metadata/datatypes/DataTypeOther.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseDAO, 3 | field, 4 | id, 5 | schema, 6 | SQL_MEMORY_DB_PRIVATE, 7 | SqlDatabase, 8 | table, 9 | ValueTransformer, 10 | } from '../../..'; 11 | 12 | const DATATYPE_OTHER_TABLE = 'DJ:DATATYPE_NUMBER'; 13 | 14 | const testTransformer: ValueTransformer = { 15 | toDB: (input) => (input == undefined ? null : input.toFixed(2)), 16 | fromDB: (input) => (input == null ? undefined : Number(input)), 17 | }; 18 | 19 | @table({ name: DATATYPE_OTHER_TABLE, autoIncrement: true }) 20 | class DataTypeOther { 21 | @id({ name: 'id', dbtype: 'INTEGER NOT NULL' }) 22 | id!: number; 23 | 24 | @field({ name: 'my_number_text', dbtype: 'TEXT' }) 25 | myNumberText?: number; 26 | 27 | @field({ name: 'my_string_real', dbtype: 'INTEGER' }) 28 | myStringInteger?: string; 29 | 30 | @field({ 31 | name: 'my_number_text2', 32 | dbtype: 'TEXT', 33 | transform: testTransformer, 34 | }) 35 | myNumberText2?: number; 36 | } 37 | 38 | describe('test Json data', () => { 39 | let sqldb: SqlDatabase; 40 | let dao: BaseDAO; 41 | // --------------------------------------------- 42 | beforeEach(async () => { 43 | try { 44 | sqldb = new SqlDatabase(); 45 | await sqldb.open(SQL_MEMORY_DB_PRIVATE); 46 | await schema().createTable(sqldb, DATATYPE_OTHER_TABLE); 47 | dao = new BaseDAO(DataTypeOther, sqldb); 48 | const prop = dao.metaModel.getProperty('myNumberText2'); 49 | expect(prop.transform.toDB).toBe(testTransformer.toDB); 50 | expect(prop.transform.fromDB).toBe(testTransformer.fromDB); 51 | } catch (err) { 52 | fail(err); 53 | } 54 | }); 55 | 56 | it('expect reading/writing number/string properties from/to the database as text/real to succeed', async () => { 57 | try { 58 | // write 59 | const model: DataTypeOther = new DataTypeOther(); 60 | model.myNumberText = 3.14; 61 | model.myStringInteger = '42'; 62 | model.myNumberText2 = 3.149; 63 | await dao.insert(model); 64 | 65 | // read 66 | const model2: DataTypeOther = await dao.select(model); 67 | 68 | expect(model2.id).toBe(model.id); 69 | expect(model2.myNumberText).toBe(3.14); 70 | expect(model2.myStringInteger).toBe('42'); 71 | expect(model2.myNumberText2).toBe(3.15); 72 | } catch (err) { 73 | fail(err); 74 | } 75 | }); 76 | 77 | it('expect reading/writing undefined properties from/to the database as text/real to succeed', async () => { 78 | try { 79 | // write 80 | const model: DataTypeOther = new DataTypeOther(); 81 | await dao.insert(model); 82 | 83 | // read 84 | const model2: DataTypeOther = await dao.select(model); 85 | expect(model2.id).toBe(model.id); 86 | expect(model2.myNumberText).toBeUndefined(); 87 | expect(model2.myStringInteger).toBeUndefined(); 88 | expect(model2.myNumberText2).toBeUndefined(); 89 | } catch (err) { 90 | fail(err); 91 | } 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlite3orm", 3 | "version": "2.6.4", 4 | "description": "ORM for sqlite3 and TypeScript/JavaScript", 5 | "main": "./index.js", 6 | "types": "./index.d.ts", 7 | "tags": [ 8 | "javascript", 9 | "typescript", 10 | "sqlite", 11 | "sqlite3", 12 | "sqlcipher", 13 | "ORM", 14 | "DAO", 15 | "schema", 16 | "database", 17 | "node", 18 | "electron" 19 | ], 20 | "author": { 21 | "email": "www.gms@gmx.at", 22 | "name": "Guenter Sandner" 23 | }, 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/gms1/node-sqlite3-orm.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/gms1/node-sqlite3-orm/issues" 31 | }, 32 | "homepage": "https://github.com/gms1/node-sqlite3-orm", 33 | "scripts": { 34 | "clean": "rimraf dist/*", 35 | "build": "tsc -p tsconfig.json", 36 | "rebuild": "npm run clean && npm run build", 37 | "lint": "eslint src --ext .js,.jsx,.ts,.tsx", 38 | "test:run": "ts-node --project tsconfig.json node_modules/jasmine/bin/jasmine.js", 39 | "test": "npm run rebuild && npm run test:run", 40 | "watch": "tsc -w -p tsconfig.json", 41 | "coverage:run": "nyc -e .ts -x \"**/*.spec.ts\" -x \"src/sqlite3orm/spec/**/*\" ts-node --project tsconfig.json node_modules/jasmine/bin/jasmine.js", 42 | "coverage:report": "nyc report --reporter=text-summary", 43 | "coverage:html": "nyc report --reporter=html", 44 | "coverage:codecov": "nyc report --reporter=json --disable=gcov", 45 | "docs": "compodoc .", 46 | "prepublishOnly": "echo \"ERROR: please use the dist-folder for publishing\" && exit 1", 47 | "release:build": "npm run clean && npm run build && npm run coverage:run && npm run lint && npm run _pubprep", 48 | "release:publish": "npm run docs && cd dist && npm --access public publish", 49 | "_pubprep": "build/sh/tsrun publish-prepare.ts", 50 | "postlint": "npm run prettier-diff", 51 | "prettier-diff": "prettier --list-different 'src/**/*.ts'", 52 | "prettier": "prettier --write 'src/**/*.ts'" 53 | }, 54 | "dependencies": { 55 | "debug": "^4.3.4", 56 | "reflect-metadata": "^0.1.13", 57 | "sqlite3": "^5.1.5" 58 | }, 59 | "devDependencies": { 60 | "@compodoc/compodoc": "^1.1.19", 61 | "@types/debug": "4.1.7", 62 | "@types/fs-extra": "^9.0.13", 63 | "@types/jasmine": "^4.0.3", 64 | "@types/node": "^17.0.24", 65 | "@types/sqlite3": "^3.1.8", 66 | "@typescript-eslint/eslint-plugin": "^5.29.0", 67 | "@typescript-eslint/parser": "^5.29.0", 68 | "eslint": "^8.18.0", 69 | "eslint-config-prettier": "^8.5.0", 70 | "eslint-plugin-deprecation": "^1.3.2", 71 | "eslint-plugin-import": "^2.26.0", 72 | "eslint-plugin-jsdoc": "^39.3.3", 73 | "eslint-plugin-no-null": "^1.0.2", 74 | "eslint-plugin-prefer-arrow": "^1.2.3", 75 | "fs-extra": "^10.1.0", 76 | "jasmine": "^4.2.1", 77 | "jasmine-spec-reporter": "^7.0.0", 78 | "nyc": "^15.1.0", 79 | "prettier": "^1.19.1", 80 | "rimraf": "^3.0.2", 81 | "ts-node": "^10.8.1", 82 | "typescript": "^4.7.4" 83 | }, 84 | "typescript": { 85 | "definition": "./index.d.ts" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/core/SqlStatement.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { SQL_MEMORY_DB_PRIVATE, SqlDatabase, SqlStatement } from '../..'; 3 | 4 | // --------------------------------------------- 5 | 6 | describe('test SqlStatement', () => { 7 | let sqldb: SqlDatabase; 8 | 9 | // --------------------------------------------- 10 | beforeEach(async () => { 11 | try { 12 | sqldb = new SqlDatabase(); 13 | await sqldb.open(SQL_MEMORY_DB_PRIVATE); 14 | await sqldb.exec( 15 | 'CREATE TABLE TEST (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, col VARCHAR(50))', 16 | ); 17 | await sqldb.run('INSERT INTO TEST (id, col) values (:a, :b)', { 18 | ':a': 0, 19 | ':b': 'testvalue 0', 20 | }); 21 | } catch (err) { 22 | fail(err); 23 | } 24 | }); 25 | 26 | // --------------------------------------------- 27 | it('expect basic prepared dml to succeed', async () => { 28 | let selStmt: SqlStatement; 29 | let insStmt: SqlStatement; 30 | selStmt = await sqldb.prepare('SELECT col FROM TEST WHERE id=?'); 31 | try { 32 | // prepare insert row 33 | insStmt = await sqldb.prepare('INSERT INTO TEST (id,col) values(?,?)'); 34 | let row: any; 35 | // insert id=1 col='testvalue 1' 36 | const res = await insStmt.run([1, 'testvalue 1']); 37 | expect(res.changes).toBe(1); 38 | // select inserted row having id=1 39 | row = await selStmt.get(1); 40 | expect(row.col).toBe('testvalue 1'); 41 | await selStmt.reset(); 42 | // select another row having id=0 43 | row = await selStmt.get(0); 44 | expect(row.col).toBe('testvalue 0'); 45 | // finalize statements 46 | await selStmt.finalize(); 47 | await insStmt.finalize(); 48 | } catch (err) { 49 | fail(err); 50 | } 51 | try { 52 | // statement is not prepared 53 | await selStmt.run(); 54 | fail('"run" should failed on finalized statement'); 55 | } catch (err) {} 56 | try { 57 | // statement is not prepared 58 | await selStmt.get(); 59 | fail('"get" should failed on finalized statement'); 60 | } catch (err) {} 61 | // prepare select where id>=? 62 | selStmt = await sqldb.prepare('SELECT id, col FROM TEST WHERE id>=? ORDER BY id'); 63 | try { 64 | // select all rows having id>0 65 | const allRows = await selStmt.all(0); 66 | expect(allRows.length).toBe(2); 67 | expect(allRows[0].id).toBe(0); 68 | expect(allRows[0].col).toBe('testvalue 0'); 69 | expect(allRows[1].id).toBe(1); 70 | expect(allRows[1].col).toBe('testvalue 1'); 71 | } catch (err) { 72 | fail(err); 73 | } 74 | try { 75 | // select all rows having id>0 using callback 76 | const allRows: any[] = []; 77 | await selStmt.each(0, (err: any, row: any) => allRows.push(row)); 78 | expect(allRows.length).toBe(2); 79 | expect(allRows[0].id).toBe(0); 80 | expect(allRows[0].col).toBe('testvalue 0'); 81 | expect(allRows[1].id).toBe(1); 82 | expect(allRows[1].col).toBe('testvalue 1'); 83 | } catch (err) { 84 | fail(err); 85 | } 86 | }); 87 | 88 | // --------------------------------------------- 89 | }); 90 | -------------------------------------------------------------------------------- /src/sqlite3orm/query/Where.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | type Primitive = string | number | boolean; 3 | 4 | export type ComparisonOperatorType = 5 | | 'eq' 6 | | 'neq' 7 | | 'gt' 8 | | 'gte' 9 | | 'lt' 10 | | 'lte' 11 | | 'isIn' 12 | | 'isNotIn' 13 | | 'isBetween' 14 | | 'isNotBetween' 15 | | 'isLike' 16 | | 'isNotLike' 17 | | 'isNull' 18 | | 'isNotNull'; 19 | 20 | export interface PropertyComparisons { 21 | eq?: T | Promise; 22 | neq?: T | Promise; 23 | gt?: T | Promise; 24 | gte?: T | Promise; 25 | lt?: T | Promise; 26 | lte?: T | Promise; 27 | 28 | isIn?: T[] | Promise; 29 | isNotIn?: T[] | Promise; 30 | isBetween?: [T | Promise, T | Promise]; 31 | isNotBetween?: [T | Promise, T | Promise]; 32 | isLike?: (T & string) | Promise; 33 | isNotLike?: (T & string) | Promise; 34 | isNull?: boolean; 35 | isNotNull?: boolean; 36 | } 37 | 38 | /* 39 | * ModelPredicates: 40 | * predicates defined on model properties to apply on table fields: 41 | * 42 | * usage: 43 | * interface Post { 44 | * id: number; 45 | * title: string; 46 | * author: string; 47 | * likes: number; 48 | * } 49 | * 50 | * const pred: ModelPredicates = { 51 | * title: 'hello' 52 | * }; 53 | * const pred2: ModelPredicates = { 54 | * title: 'hello', 55 | * likes: {isBetween: [0, 4]} 56 | * }; 57 | * 58 | * 59 | */ 60 | 61 | type ShortHandType = Primitive | Date; 62 | 63 | export type PropertyPredicates = 64 | | PropertyComparisons 65 | | (PT & ShortHandType) 66 | | Promise; 67 | 68 | export type ModelPredicates = { [K in keyof MT]?: PropertyPredicates } & { 69 | not?: never; 70 | or?: never; 71 | and?: never; 72 | sql?: never; 73 | }; 74 | 75 | export function getPropertyPredicates( 76 | modelPredicates: ModelPredicates, 77 | key: K, 78 | ): PropertyPredicates { 79 | return (modelPredicates[key] == undefined 80 | ? { eq: undefined } 81 | : modelPredicates[key]) as PropertyPredicates; 82 | } 83 | 84 | export function getPropertyComparison( 85 | propertyPredicate: PropertyPredicates, 86 | key: string, 87 | ): any { 88 | return (propertyPredicate as any)[key]; 89 | } 90 | 91 | /* 92 | * Condition: 93 | * condition defined on model to apply to the where - clause 94 | * 95 | * usage: 96 | * 97 | * const cond1: Condition = { 98 | * not: {title: 'foo'} 99 | * }; 100 | * const cond2: Condition = { 101 | * or: [{title: 'foo'}, {title: 'bar'}] 102 | * }; 103 | * const cond3: Condition = { 104 | * and: [{author: 'gms'}, {or: [{title: {isLike: '%hello%'}}, {title: {isLike: '%world%'}}]}] 105 | * }; 106 | * 107 | */ 108 | 109 | export type LogicalOperatorType = 'not' | 'or' | 'and' | 'sql'; 110 | 111 | export type Condition = 112 | | { 113 | not: Condition | ModelPredicates; 114 | } 115 | | { or: (Condition | ModelPredicates)[] } 116 | | { and: (Condition | ModelPredicates)[] } 117 | | { sql: string } 118 | | ModelPredicates; 119 | 120 | export function isModelPredicates(cond?: Condition): cond is ModelPredicates { 121 | return cond && 122 | (cond as any).not === undefined && 123 | (cond as any).or === undefined && 124 | (cond as any).and === undefined && 125 | (cond as any).sql === undefined 126 | ? true 127 | : false; 128 | } 129 | 130 | /* 131 | * Where 132 | * 133 | * alias for Condition|string 134 | * 135 | */ 136 | export type Where = Condition | string; 137 | -------------------------------------------------------------------------------- /src/sqlite3orm/core/SqlConnectionPoolDatabase.ts: -------------------------------------------------------------------------------- 1 | import * as _dbg from 'debug'; 2 | import { Database } from 'sqlite3'; 3 | 4 | import { SqlConnectionPool } from './SqlConnectionPool'; 5 | import { SQL_OPEN_DEFAULT, SqlDatabase } from './SqlDatabase'; 6 | import { SqlDatabaseSettings } from './SqlDatabaseSettings'; 7 | 8 | const debug = _dbg('sqlite3orm:database'); 9 | 10 | export class SqlConnectionPoolDatabase extends SqlDatabase { 11 | pool?: SqlConnectionPool; 12 | 13 | public close(): Promise { 14 | if (this.pool) { 15 | return this.pool.release(this); 16 | } else { 17 | return super.close(); 18 | } 19 | } 20 | 21 | public async open( 22 | databaseFile: string, 23 | mode?: number, 24 | settings?: SqlDatabaseSettings, 25 | ): Promise { 26 | /* istanbul ignore else */ 27 | if (this.isOpen()) { 28 | /* istanbul ignore else */ 29 | if (this.pool) { 30 | // stealing from pool 31 | // this connection should not be recycled by the pool 32 | // => temporary mark as dirty 33 | const oldDirty = this.dirty; 34 | this.dirty = true; 35 | await this.pool.release(this); 36 | this.dirty = oldDirty; 37 | } else { 38 | await super.close(); 39 | } 40 | } 41 | this.pool = undefined; 42 | return super.open(databaseFile, mode, settings); 43 | } 44 | 45 | /* 46 | @internal 47 | */ 48 | public openByPool( 49 | pool: SqlConnectionPool, 50 | databaseFile: string, 51 | mode?: number, 52 | settings?: SqlDatabaseSettings, 53 | ): Promise { 54 | return new Promise((resolve, reject) => { 55 | const db = new Database(databaseFile, mode || SQL_OPEN_DEFAULT, (err) => { 56 | if (err) { 57 | reject(err); 58 | } else { 59 | this.pool = pool; 60 | this.db = db; 61 | this.dbId = SqlDatabase.lastId++; 62 | this.databaseFile = databaseFile; 63 | debug(`${this.dbId}: opened`); 64 | resolve(); 65 | } 66 | }); 67 | }).then( 68 | (): Promise => { 69 | if (settings) { 70 | return this.applySettings(settings); 71 | } 72 | return Promise.resolve(); 73 | }, 74 | ); 75 | } 76 | 77 | /* 78 | @internal 79 | */ 80 | public closeByPool(): Promise { 81 | this.pool = undefined; 82 | return new Promise((resolve, reject) => { 83 | /* istanbul ignore if */ 84 | if (!this.db) { 85 | resolve(); 86 | } else { 87 | const db = this.db; 88 | debug(`${this.dbId}: close`); 89 | this.db = undefined; 90 | this.dbId = undefined; 91 | this.databaseFile = undefined; 92 | db.close((err) => { 93 | db.removeAllListeners(); 94 | /* istanbul ignore if */ 95 | if (err) { 96 | reject(err); 97 | } else { 98 | resolve(); 99 | } 100 | }); 101 | } 102 | }); 103 | } 104 | 105 | /* 106 | @internal 107 | */ 108 | public async recycleByPool( 109 | pool: SqlConnectionPool, 110 | sqldb: SqlConnectionPoolDatabase, 111 | settings?: SqlDatabaseSettings, 112 | ): Promise { 113 | /* istanbul ignore else */ 114 | if (sqldb.db) { 115 | try { 116 | await sqldb.endTransaction(false); 117 | } catch (err) {} 118 | sqldb.db.removeAllListeners(); 119 | // move 120 | this.db = sqldb.db; 121 | this.dbId = sqldb.dbId; 122 | this.databaseFile = sqldb.databaseFile; 123 | this.pool = pool; 124 | // reapply default settings 125 | if (settings) { 126 | try { 127 | await this.applySettings(settings); 128 | } catch (err) {} 129 | } 130 | } 131 | sqldb.db = undefined; 132 | sqldb.dbId = undefined; 133 | sqldb.databaseFile = undefined; 134 | sqldb.pool = undefined; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/query/issue74.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseDAO, field, id, QueryModel, SQL_MEMORY_DB_PRIVATE, SqlDatabase, table } from '../..'; 2 | 3 | @table({ name: 'ISSUE74_TABLE' }) 4 | class Issue74Model { 5 | @id({ name: 'id', dbtype: 'INTEGER NOT NULL' }) id!: number; 6 | 7 | @field({ name: 'loaded' }) 8 | loaded: boolean = false; 9 | } 10 | 11 | describe('test QueryModel', () => { 12 | let sqldb: SqlDatabase; 13 | 14 | beforeAll(async () => { 15 | sqldb = new SqlDatabase(); 16 | await sqldb.open(SQL_MEMORY_DB_PRIVATE); 17 | 18 | const issue74Dao: BaseDAO = new BaseDAO(Issue74Model, sqldb); 19 | await issue74Dao.createTable(); 20 | // await contactDao.createTable(); 21 | const issue74Model = new Issue74Model(); 22 | 23 | issue74Model.id = 1; 24 | issue74Model.loaded = false; 25 | 26 | await issue74Dao.insert(issue74Model); 27 | 28 | issue74Model.id = 2; 29 | issue74Model.loaded = true; 30 | await issue74Dao.insert(issue74Model); 31 | }); 32 | 33 | // --------------------------------------------- 34 | afterAll(async () => { 35 | try { 36 | const issue74Dao: BaseDAO = new BaseDAO(Issue74Model, sqldb); 37 | await issue74Dao.dropTable(); 38 | } catch (err) { 39 | fail(err); 40 | } 41 | }); 42 | 43 | it('worked: `not: { loaded: true }``', async () => { 44 | try { 45 | const issue74Model = new QueryModel(Issue74Model); 46 | const res = await issue74Model.selectAll(sqldb, { 47 | where: { 48 | not: { loaded: true }, 49 | }, 50 | }); 51 | expect(res.length).toBe(1); 52 | expect(res[0].id).toBe(1); 53 | expect(res[0].loaded).toBe(false); 54 | } catch (err) { 55 | fail(err); 56 | } 57 | }); 58 | 59 | it('worked: `not: { loaded: { eq: false } }``', async () => { 60 | try { 61 | const issue74Model = new QueryModel(Issue74Model); 62 | const res = await issue74Model.selectAll(sqldb, { 63 | where: { 64 | not: { loaded: { eq: false } }, 65 | }, 66 | }); 67 | expect(res.length).toBe(1); 68 | expect(res[0].id).toBe(2); 69 | } catch (err) { 70 | fail(err); 71 | } 72 | }); 73 | 74 | it('failed: `not: { loaded: false }``', async () => { 75 | try { 76 | const issue74Model = new QueryModel(Issue74Model); 77 | const res = await issue74Model.selectAll(sqldb, { 78 | where: { 79 | not: { loaded: false }, 80 | }, 81 | }); 82 | expect(res.length).toBe(1); 83 | expect(res.length).toBe(1); 84 | expect(res[0].id).toBe(2); 85 | } catch (err) { 86 | fail(err); 87 | } 88 | }); 89 | 90 | it('worked: `{ loaded: true }`', async () => { 91 | try { 92 | const issue74Model = new QueryModel(Issue74Model); 93 | const res = await issue74Model.selectAll(sqldb, { 94 | where: { 95 | loaded: true, 96 | }, 97 | }); 98 | expect(res.length).toBe(1); 99 | expect(res[0].id).toBe(2); 100 | expect(res[0].loaded).toBe(true); 101 | } catch (err) { 102 | fail(err); 103 | } 104 | }); 105 | 106 | it('worked: `{ loaded: { eq: false } }`', async () => { 107 | try { 108 | const issue74Model = new QueryModel(Issue74Model); 109 | const res = await issue74Model.selectAll(sqldb, { 110 | where: { 111 | loaded: { eq: false }, 112 | }, 113 | }); 114 | expect(res.length).toBe(1); 115 | expect(res[0].id).toBe(1); 116 | expect(res[0].loaded).toBe(false); 117 | } catch (err) { 118 | fail(err); 119 | } 120 | }); 121 | 122 | it('failed: `{ loaded: false }`', async () => { 123 | try { 124 | const issue74Model = new QueryModel(Issue74Model); 125 | const res = await issue74Model.selectAll(sqldb, { 126 | where: { 127 | loaded: false, 128 | }, 129 | }); 130 | expect(res.length).toBe(1); 131 | expect(res[0].id).toBe(1); 132 | expect(res[0].loaded).toBe(false); 133 | } catch (err) { 134 | fail(err); 135 | } 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /src/sqlite3orm/core/SqlStatement.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | // import * as core from './core'; 4 | 5 | import { Statement } from 'sqlite3'; 6 | 7 | export interface SqlRunResult { 8 | lastID: number; 9 | changes: number; 10 | } 11 | 12 | /** 13 | * A thin wrapper for the 'Statement' class from 'node-sqlite3' using Promises instead of callbacks 14 | * see 15 | * https://github.com/mapbox/node-sqlite3/wiki/API 16 | * 17 | * @export 18 | * @class SqlStatement 19 | */ 20 | export class SqlStatement { 21 | private readonly stmt: Statement; 22 | 23 | /** 24 | * Creates an instance of SqlStatement. 25 | * 26 | * @param stmt 27 | */ 28 | public constructor(stmt: Statement) { 29 | this.stmt = stmt; 30 | } 31 | 32 | /** 33 | * Bind the given parameters to the prepared statement 34 | * 35 | * @param params 36 | */ 37 | /* istanbul ignore next */ 38 | // see https://github.com/mapbox/node-sqlite3/issues/841 39 | public bind(...params: any[]): this { 40 | this.stmt.bind(params); 41 | return this; 42 | } 43 | 44 | /** 45 | * Reset a open cursor of the prepared statement preserving the parameter binding 46 | * Allows re-execute of the same query 47 | * 48 | * @returns {Promise} 49 | */ 50 | public reset(): Promise { 51 | return new Promise((resolve) => { 52 | this.stmt.reset(() => { 53 | resolve(); 54 | }); 55 | }); 56 | } 57 | 58 | /** 59 | * Finalizes a prepared statement ( freeing any resource used by this statement ) 60 | * 61 | * @returns {Promise} 62 | */ 63 | public finalize(): Promise { 64 | return new Promise((resolve, reject) => { 65 | this.stmt.finalize((err) => { 66 | if (err) { 67 | /* istanbul ignore next */ 68 | reject(err); 69 | } else { 70 | resolve(); 71 | } 72 | }); 73 | }); 74 | } 75 | 76 | /** 77 | * Runs a prepared statement with the specified parameters 78 | * 79 | * @param [params] - The parameters referenced in the statement; you can provide multiple parameters as array 80 | * @returns A promise 81 | */ 82 | public run(params?: any): Promise { 83 | return new Promise((resolve, reject) => { 84 | this.stmt.run(params, function(err: Error): void { 85 | if (err) { 86 | reject(err); 87 | } else { 88 | const res: SqlRunResult = { lastID: this.lastID, changes: this.changes }; 89 | resolve(res); 90 | } 91 | }); 92 | }); 93 | } 94 | 95 | /** 96 | * Runs a prepared statement with the specified parameters, fetching only the first row 97 | * 98 | * @param [params] - The parameters referenced in the statement; you can provide multiple parameters as array 99 | * @returns A promise 100 | */ 101 | public get(params?: any): Promise { 102 | return new Promise((resolve, reject) => { 103 | this.stmt.get(params, (err, row) => { 104 | if (err) { 105 | reject(err); 106 | } else { 107 | resolve(row); 108 | } 109 | }); 110 | }); 111 | } 112 | 113 | /** 114 | * Runs a prepared statement with the specified parameters, fetching all rows 115 | * 116 | * @param [params] - The parameters referenced in the statement; you can provide multiple parameters as array 117 | * @returns A promise 118 | */ 119 | public all(params?: any): Promise { 120 | return new Promise((resolve, reject) => { 121 | this.stmt.all(params, (err, rows) => { 122 | /* istanbul ignore if */ 123 | if (err) { 124 | reject(err); 125 | } else { 126 | resolve(rows); 127 | } 128 | }); 129 | }); 130 | } 131 | 132 | /** 133 | * Runs a prepared statement with the specified parameters, fetching all rows 134 | * using a callback for each row 135 | * 136 | * @param [params] 137 | * @param [callback] 138 | * @returns A promise 139 | */ 140 | public each(params?: any, callback?: (err: Error, row: any) => void): Promise { 141 | return new Promise((resolve, reject) => { 142 | this.stmt.each(params, callback, (err: Error, count: number) => { 143 | /* istanbul ignore if */ 144 | if (err) { 145 | reject(err); 146 | } else { 147 | resolve(count); 148 | } 149 | }); 150 | }); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/sqlite3orm/query/QueryPropertyPredicate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | /* eslint-disable @typescript-eslint/no-unused-vars */ 5 | import { KeyType, MetaModel, MetaProperty } from '../metadata'; 6 | 7 | import { QueryOperation } from './QueryOperation'; 8 | import { ComparisonOperatorType } from './Where'; 9 | 10 | export class QueryPropertyPredicate implements QueryOperation { 11 | op: ComparisonOperatorType; 12 | constructor(public propertyKey: KeyType, opName: string, public value: any) { 13 | switch (opName) { 14 | case 'eq': 15 | case 'neq': 16 | case 'gt': 17 | case 'gte': 18 | case 'lt': 19 | case 'lte': 20 | case 'isIn': 21 | case 'isNotIn': 22 | case 'isBetween': 23 | case 'isNotBetween': 24 | case 'isLike': 25 | case 'isNotLike': 26 | case 'isNull': 27 | case 'isNotNull': 28 | this.op = opName; 29 | break; 30 | /* istanbul ignore next */ 31 | default: 32 | throw new Error(`unknown comparison operation: '${opName}'`); 33 | } 34 | } 35 | 36 | async toSql(metaModel: MetaModel, params: Object, tablePrefix: string): Promise { 37 | const prop = metaModel.getProperty(this.propertyKey); 38 | let sql = `${tablePrefix}${prop.field.quotedName} `; 39 | const value = await this.value; 40 | 41 | sql += this.operatorSql(value); 42 | 43 | switch (this.op) { 44 | // no host variable: 45 | case 'isNull': 46 | case 'isNotNull': 47 | return sql; 48 | 49 | // one host variable: 50 | case 'eq': 51 | case 'neq': 52 | case 'gt': 53 | case 'gte': 54 | case 'lt': 55 | case 'lte': 56 | case 'isLike': 57 | case 'isNotLike': 58 | sql += ' ' + this.setHostParameter(prop, params, prop.valueToDB(value)); 59 | return sql; 60 | 61 | // two host variables: 62 | case 'isBetween': 63 | case 'isNotBetween': { 64 | /* istanbul ignore if */ 65 | if (!Array.isArray(value)) { 66 | throw new Error( 67 | `expected array parameter for BETWEEN-operation on '${this.propertyKey.toString()}`, 68 | ); 69 | } 70 | /* istanbul ignore if */ 71 | if (value.length !== 2) { 72 | throw new Error( 73 | `expected 2-tuple for BETWEEN-operation on '${this.propertyKey.toString()}`, 74 | ); 75 | } 76 | const from = await value[0]; 77 | const to = await value[1]; 78 | sql += ' ' + this.setHostParameter(prop, params, prop.valueToDB(from)); 79 | sql += ' AND ' + this.setHostParameter(prop, params, prop.valueToDB(to)); 80 | return `(${sql})`; 81 | } 82 | 83 | // multiple host variables: 84 | case 'isIn': 85 | case 'isNotIn': { 86 | /* istanbul ignore if */ 87 | if (!Array.isArray(value)) { 88 | throw new Error( 89 | `expected array parameter for IN-operation on '${this.propertyKey.toString()}`, 90 | ); 91 | } 92 | if (!value.length) { 93 | throw new Error(`expected a value for IN-operation on '${this.propertyKey.toString()}`); 94 | } 95 | const hostParams: any[] = []; 96 | for (const item of value) { 97 | hostParams.push(this.setHostParameter(prop, params, prop.valueToDB(item))); 98 | } 99 | sql += ' (' + hostParams.join(', ') + ')'; 100 | return sql; 101 | } 102 | 103 | /* istanbul ignore next */ 104 | default: 105 | throw new Error(`unknown operation: '${this.op}`); 106 | } 107 | } 108 | 109 | protected operatorSql(value: any): string { 110 | // add operator 111 | switch (this.op) { 112 | case 'isNull': 113 | return value ? 'ISNULL' : 'NOTNULL'; 114 | case 'isNotNull': 115 | return value ? 'NOTNULL' : 'ISNULL'; 116 | case 'eq': 117 | return '='; 118 | case 'neq': 119 | return '!='; 120 | case 'gt': 121 | return '>'; 122 | case 'gte': 123 | return '>='; 124 | case 'lt': 125 | return '<'; 126 | case 'lte': 127 | return '<='; 128 | case 'isLike': 129 | return 'LIKE'; 130 | case 'isNotLike': 131 | return 'NOT LIKE'; 132 | case 'isBetween': 133 | return 'BETWEEN'; 134 | case 'isNotBetween': 135 | return 'NOT BETWEEN'; 136 | case 'isIn': 137 | return 'IN'; 138 | case 'isNotIn': 139 | return 'NOT IN'; 140 | /* istanbul ignore next */ 141 | default: 142 | throw new Error(`unknown operation: '${this.op}`); 143 | } 144 | } 145 | 146 | protected setHostParameter(prop: MetaProperty, params: any, value: any): string { 147 | const namePrefix = prop.getHostParameterName('w$'); 148 | let nr = 1; 149 | let key = `${namePrefix}$`; 150 | while (Object.prototype.hasOwnProperty.call(params, key)) { 151 | nr++; 152 | key = `${namePrefix}$${nr}`; 153 | } 154 | params[key] = value; 155 | return key; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/DefaultValueTransformers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { ValueTransformer } from './ValueTransformer'; 4 | 5 | export class JsonTransformer implements ValueTransformer { 6 | toDB(input: any): string | null { 7 | return input == undefined ? null : JSON.stringify(input); 8 | } 9 | 10 | fromDB(input: string | null): any { 11 | return input == null ? undefined : JSON.parse(input); 12 | } 13 | } 14 | 15 | export class BooleanTextTransformer implements ValueTransformer { 16 | toDB(input: boolean | undefined): string | null { 17 | return input == undefined ? null : !input ? '0' : '1'; 18 | } 19 | 20 | fromDB(input: string | null): boolean | undefined { 21 | if (input == null) { 22 | return undefined; 23 | } 24 | if (input === '0' || input === 'false') { 25 | return false; 26 | } else if (input === '1' || input === 'true') { 27 | return true; 28 | } 29 | return undefined; 30 | } 31 | } 32 | 33 | export class BooleanNumberTransformer implements ValueTransformer { 34 | toDB(input: boolean | undefined): number | null { 35 | return input == undefined ? null : !input ? 0 : 1; 36 | } 37 | 38 | fromDB(input: number | null): boolean | undefined { 39 | return input == null ? undefined : !input ? false : true; 40 | } 41 | } 42 | 43 | export class DateTextTransformer implements ValueTransformer { 44 | toDB(input: Date | undefined): string | null { 45 | return input == undefined ? null : input.toISOString(); 46 | } 47 | 48 | fromDB(input: string | null): Date | undefined { 49 | return input == null ? undefined : new Date(Date.parse(input)); 50 | } 51 | } 52 | 53 | export class DateIntegerAsSecondsTransformer implements ValueTransformer { 54 | toDB(input: Date | undefined): number | null { 55 | return input == undefined ? null : Math.floor(input.getTime() / 1000); 56 | } 57 | 58 | fromDB(input: number | null): Date | undefined { 59 | return input == null ? undefined : new Date(Number.isInteger(input) ? input * 1000 : NaN); 60 | } 61 | } 62 | 63 | export class DateIntegerAsMillisecondsTransformer implements ValueTransformer { 64 | toDB(input: Date | undefined): number | null { 65 | return input == undefined ? null : input.getTime(); 66 | } 67 | 68 | fromDB(input: number | null): Date | undefined { 69 | return input == null ? undefined : new Date(Number.isInteger(input) ? input : NaN); 70 | } 71 | } 72 | 73 | export class NumberTextTransformer implements ValueTransformer { 74 | toDB(input: number | undefined): string | null { 75 | return input == undefined ? null : String(input); 76 | } 77 | 78 | fromDB(input: string | null): number | undefined { 79 | return input == null ? undefined : Number(input); 80 | } 81 | } 82 | 83 | export class NumberDefaultTransformer implements ValueTransformer { 84 | toDB(input: number | undefined): number | null { 85 | return input == undefined ? null : Number(input); 86 | } 87 | 88 | fromDB(input: number | null): number | undefined { 89 | return input == null ? undefined : Number(input); 90 | } 91 | } 92 | 93 | export class StringDefaultTransformer implements ValueTransformer { 94 | toDB(input: string | undefined): string | null { 95 | return input == undefined ? null : String(input); 96 | } 97 | 98 | fromDB(input: string | null): string | undefined { 99 | return input == null ? undefined : String(input); 100 | } 101 | } 102 | 103 | export class StringNumberTransformer implements ValueTransformer { 104 | toDB(input: string | undefined): number | null { 105 | return input == undefined ? null : Number(input); 106 | } 107 | 108 | fromDB(input: number | null): string | undefined { 109 | return input == null ? undefined : String(input); 110 | } 111 | } 112 | 113 | export class UnknownDefaultTransformer implements ValueTransformer { 114 | /* istanbul ignore next */ 115 | toDB(input: any | undefined): string | null { 116 | return input == undefined ? null : input; 117 | } 118 | 119 | /* istanbul ignore next */ 120 | fromDB(input: string | null): string | undefined { 121 | return input == null ? undefined : input; 122 | } 123 | } 124 | 125 | export class DefaultValueTransformers { 126 | readonly json: ValueTransformer = new JsonTransformer(); 127 | readonly booleanText: ValueTransformer = new BooleanTextTransformer(); 128 | readonly booleanNumber: ValueTransformer = new BooleanNumberTransformer(); 129 | readonly dateText: ValueTransformer = new DateTextTransformer(); 130 | readonly dateIntegerAsSeconds: ValueTransformer = new DateIntegerAsSecondsTransformer(); 131 | readonly dateIntegerAsMilliseconds: ValueTransformer = new DateIntegerAsMillisecondsTransformer(); 132 | readonly numberText: ValueTransformer = new NumberTextTransformer(); 133 | readonly numberDefault: ValueTransformer = new NumberDefaultTransformer(); 134 | readonly stringNumber: ValueTransformer = new StringNumberTransformer(); 135 | readonly stringDefault: ValueTransformer = new StringDefaultTransformer(); 136 | readonly unknownDefault: ValueTransformer = new UnknownDefaultTransformer(); 137 | } 138 | 139 | export const DEFAULT_VALUE_TRANSFORMERS = new DefaultValueTransformers(); 140 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/MetaProperty.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | /* eslint-disable @typescript-eslint/ban-types */ 4 | import { FieldOpts } from './decorators'; 5 | import { DEFAULT_VALUE_TRANSFORMERS } from './DefaultValueTransformers'; 6 | import { Field } from './Field'; 7 | import { KeyType, MetaModel } from './MetaModel'; 8 | import { PropertyType } from './PropertyType'; 9 | import { ValueTransformer } from './ValueTransformer'; 10 | 11 | export class MetaProperty { 12 | /** 13 | * The property type enum mapped to this field 14 | */ 15 | private _propertyType: PropertyType; 16 | get propertyType(): PropertyType { 17 | return this._propertyType; 18 | } 19 | 20 | private _field?: Field; 21 | public get field(): Field { 22 | /* istanbul ignore else */ 23 | if (this._field) { 24 | return this._field; 25 | } 26 | /* istanbul ignore next */ 27 | throw new Error( 28 | `meta model property '${this.className}.${this.key.toString()}' not fully initialized yet`, 29 | ); 30 | } 31 | 32 | private _transform!: ValueTransformer; 33 | public get transform(): ValueTransformer { 34 | return this._transform; 35 | } 36 | 37 | constructor(public readonly className: string, public readonly key: KeyType) { 38 | this._propertyType = PropertyType.UNKNOWN; 39 | } 40 | 41 | // called from decorator 42 | setPropertyType(propertyType: Function | string): void { 43 | let typeName: string; 44 | /* istanbul ignore else */ 45 | if (typeof propertyType === 'function') { 46 | typeName = propertyType.name.toLowerCase(); 47 | } else { 48 | typeName = propertyType.toLowerCase(); 49 | } 50 | switch (typeName) { 51 | case 'boolean': 52 | this._propertyType = PropertyType.BOOLEAN; 53 | break; 54 | case 'string': 55 | this._propertyType = PropertyType.STRING; 56 | break; 57 | case 'number': 58 | this._propertyType = PropertyType.NUMBER; 59 | break; 60 | case 'date': 61 | this._propertyType = PropertyType.DATE; 62 | break; 63 | default: 64 | this._propertyType = PropertyType.UNKNOWN; 65 | break; 66 | } 67 | } 68 | 69 | valueToDB(value: any): any { 70 | return this._transform.toDB(value); 71 | } 72 | 73 | getDBValueFromModel(model: any): any { 74 | return this._transform.toDB(Reflect.get(model, this.key)); 75 | } 76 | 77 | setDBValueIntoModel(model: any, value: any): void { 78 | Reflect.set(model, this.key, this._transform.fromDB(value)); 79 | } 80 | 81 | /** 82 | * Get the name for the corresponding host parameter 83 | * 84 | * @returns {string} 85 | */ 86 | public getHostParameterName(prefix?: string): string { 87 | prefix = prefix || ''; 88 | return `:${prefix}${this.key.toString()}`; 89 | } 90 | 91 | init(model: MetaModel, name: string, isIdentity: boolean, opts: FieldOpts): void { 92 | try { 93 | this._field = model.table.getOrAddTableField(name, isIdentity, opts, this.propertyType); 94 | } catch (err) { 95 | throw new Error( 96 | `property '${this.className}.${this.key.toString()}': failed to add field: ${err.message}`, 97 | ); 98 | } 99 | 100 | // add mapping from column name to this property 101 | model.mapColNameToProp.set(this._field.name, this); 102 | 103 | // init transform 104 | const typeAffinity = this.field.dbTypeInfo.typeAffinity; 105 | 106 | if (opts.transform) { 107 | this._transform = opts.transform; 108 | } else { 109 | if (this.field.isJson) { 110 | this._transform = DEFAULT_VALUE_TRANSFORMERS.json; 111 | } else { 112 | switch (this.propertyType) { 113 | /* BOOLEAN */ 114 | case PropertyType.BOOLEAN: 115 | if (typeAffinity === 'TEXT') { 116 | this._transform = DEFAULT_VALUE_TRANSFORMERS.booleanText; 117 | } else { 118 | this._transform = DEFAULT_VALUE_TRANSFORMERS.booleanNumber; 119 | } 120 | break; 121 | case PropertyType.DATE: 122 | if (typeAffinity === 'TEXT') { 123 | this._transform = DEFAULT_VALUE_TRANSFORMERS.dateText; 124 | } else { 125 | if (this._field.dateInMilliSeconds) { 126 | this._transform = DEFAULT_VALUE_TRANSFORMERS.dateIntegerAsMilliseconds; 127 | } else { 128 | this._transform = DEFAULT_VALUE_TRANSFORMERS.dateIntegerAsSeconds; 129 | } 130 | } 131 | break; 132 | case PropertyType.NUMBER: 133 | if (typeAffinity === 'TEXT') { 134 | this._transform = DEFAULT_VALUE_TRANSFORMERS.numberText; 135 | } else { 136 | this._transform = DEFAULT_VALUE_TRANSFORMERS.numberDefault; 137 | } 138 | break; 139 | case PropertyType.STRING: 140 | if (typeAffinity === 'TEXT') { 141 | this._transform = DEFAULT_VALUE_TRANSFORMERS.stringDefault; 142 | } else { 143 | this._transform = DEFAULT_VALUE_TRANSFORMERS.stringNumber; 144 | } 145 | break; 146 | default: 147 | this._transform = DEFAULT_VALUE_TRANSFORMERS.unknownDefault; 148 | break; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/metadata/decorators.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import { field, fk, id, index, table } from '../..'; 4 | 5 | // --------------------------------------------- 6 | 7 | describe('test decorators', () => { 8 | // --------------------------------------------- 9 | it('expect decorating class twice for same table to throw', () => { 10 | try { 11 | @table({ name: 'D:TABLE1_FOR_SAME_CLASS', autoIncrement: true }) 12 | @table({ name: 'D:TABLE1_FOR_SAME_CLASS', autoIncrement: true }) 13 | class ClassUsingDifferentTables { 14 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) id!: number; 15 | } 16 | fail('should have thrown'); 17 | } catch (err) {} 18 | }); 19 | 20 | // --------------------------------------------- 21 | it('expect decorating class for different tables to throw', () => { 22 | try { 23 | @table({ name: 'D:TABLE1_FOR_SAME_CLASS', autoIncrement: true }) 24 | @table({ name: 'D:TABLE2_FOR_SAME_CLASS', autoIncrement: true }) 25 | class ClassUsingDifferentTables { 26 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) id!: number; 27 | } 28 | fail('should have thrown'); 29 | } catch (err) {} 30 | }); 31 | 32 | // --------------------------------------------- 33 | it('expect decorating static property as field to throw', () => { 34 | try { 35 | @table({ name: 'D:TABLE_USING_STATIC_PROPERTY_FOR_FIELD', autoIncrement: true }) 36 | class TableUsingStaticProperyForField { 37 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) static id: number = 5; 38 | constructor() {} 39 | } 40 | fail('should have thrown'); 41 | } catch (err) {} 42 | }); 43 | 44 | // --------------------------------------------- 45 | it('expect decorating static property as index to throw', () => { 46 | try { 47 | @table({ name: 'D:TABLE_USING_STATIC_PROPERTY_FOR_INDEX', autoIncrement: true }) 48 | class TableUsingStaticProperyForIndex { 49 | @index('PARENTIDX') static parentId?: number; 50 | 51 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) id!: number; 52 | } 53 | fail('should have thrown'); 54 | } catch (err) {} 55 | }); 56 | 57 | // --------------------------------------------- 58 | it('expect decorating static property as foreign key to throw', () => { 59 | try { 60 | @table({ name: 'D:TABLE_USING_STATIC_PROPERTY_FOR_FK', autoIncrement: true }) 61 | class TableUsingStaticProperyForFk { 62 | @fk('PARENTIDX', 'ANOTHER_TABLE', 'ANOTHER_FIELD') static parentId?: number; 63 | 64 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) id!: number; 65 | } 66 | fail('should have thrown'); 67 | } catch (err) {} 68 | }); 69 | 70 | // --------------------------------------------- 71 | it('expect decorating property twice as same field to throw', () => { 72 | try { 73 | @table({ name: 'D:TABLE_USING_DUPLICATE_FIELD', autoIncrement: true }) 74 | class TableUsingDuplicateIndexOnField { 75 | @field({ name: 'PARENTID', dbtype: 'INTEGER' }) 76 | parentId?: number; 77 | 78 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) 79 | @field({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) 80 | id!: number; 81 | } 82 | fail('should have thrown'); 83 | } catch (err) {} 84 | }); 85 | 86 | // --------------------------------------------- 87 | it('expect decorating property as different fields to throw', () => { 88 | try { 89 | @table({ name: 'D:TABLE_USING_DUPLICATE_FIELD', autoIncrement: true }) 90 | class TableUsingDuplicateIndexOnField { 91 | @field({ name: 'PARENTID', dbtype: 'INTEGER' }) 92 | parentId?: number; 93 | 94 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) 95 | @field({ name: 'PARENTID', dbtype: 'INTEGER' }) 96 | id!: number; 97 | } 98 | fail('should have thrown'); 99 | } catch (err) {} 100 | }); 101 | 102 | // --------------------------------------------- 103 | it('expect decorating property twice for same index to throw', () => { 104 | try { 105 | @table({ name: 'D:TABLE_USING_DUPLICATE_INDEX_ON_FIELD', autoIncrement: true }) 106 | class TableUsingDuplicateIndexOnField { 107 | @index('PARENTIDX') 108 | @index('PARENTIDX') 109 | @field({ name: 'PARENTID', dbtype: 'INTEGER' }) 110 | parentId?: number; 111 | 112 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) id!: number; 113 | } 114 | fail('should have thrown'); 115 | } catch (err) {} 116 | }); 117 | 118 | // --------------------------------------------- 119 | it('expect decorating foreign key twice for same constraint name to throw', () => { 120 | try { 121 | @table({ name: 'D:PARENT_TABLE_FOR_DUPLICATE_FKS' }) 122 | class ParentTableForDuplicateFKs { 123 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) id!: number; 124 | @id({ name: 'ID2', dbtype: 'INTEGER NOT NULL' }) id2!: number; 125 | } 126 | 127 | @table({ name: 'D:TABLE_USING_DUPLICATE_FK', autoIncrement: true }) 128 | class TableUsingDuplicateFKs { 129 | @fk('PARENTIDX', 'PARENT_TABLE_FOR_DUPLICATE_FKS', 'ID') 130 | @fk('PARENTIDX', 'PARENT_TABLE_FOR_DUPLICATE_FKS', 'ID2') 131 | @field({ name: 'PARENTID1', dbtype: 'INTEGER' }) 132 | parentId1?: number; 133 | 134 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) id!: number; 135 | } 136 | fail('should have thrown'); 137 | } catch (err) {} 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/Field.ts: -------------------------------------------------------------------------------- 1 | import { backtickQuoteSimpleIdentifier } from '../utils'; 2 | import { DbColumnTypeInfo, DbCatalogDAO } from '../dbcatalog'; 3 | import { PropertyType } from './PropertyType'; 4 | import { FieldOpts } from './decorators'; 5 | import { schema } from './Schema'; 6 | 7 | /** 8 | * Class holding a field definition 9 | * 10 | * @export 11 | * @class Field 12 | */ 13 | export class Field { 14 | /** 15 | * The name of the column 16 | */ 17 | public name!: string; 18 | 19 | /** 20 | * The quoted field name 21 | */ 22 | get quotedName(): string { 23 | return backtickQuoteSimpleIdentifier(this.name); 24 | } 25 | 26 | private _dbDefaultType!: string; 27 | get dbDefaultType(): string { 28 | return this._dbDefaultType; 29 | } 30 | set dbDefaultType(dbType: string) { 31 | this._dbDefaultType = dbType; 32 | if (!this._dbtype) { 33 | this._dbTypeInfo = Field.parseDbType(this._dbDefaultType); 34 | } 35 | } 36 | 37 | /** 38 | * The type of the table column 39 | */ 40 | private _dbtype?: string; 41 | private _dbTypeInfo!: DbColumnTypeInfo; 42 | 43 | get dbtype(): string { 44 | return this._dbtype ? this._dbtype : this.dbDefaultType; 45 | } 46 | set dbtype(dbType: string) { 47 | this._dbtype = dbType; 48 | this._dbTypeInfo = Field.parseDbType(this._dbtype); 49 | } 50 | get isDbTypeDefined(): boolean { 51 | return this._dbtype ? true : false; 52 | } 53 | 54 | get dbTypeInfo(): DbColumnTypeInfo { 55 | return this._dbTypeInfo; 56 | } 57 | 58 | /** 59 | * If this property should be serialized/deserialized to the database as Json data 60 | */ 61 | private _isJson?: boolean; 62 | 63 | get isJson(): boolean { 64 | return this._isJson == undefined ? false : this._isJson; 65 | } 66 | set isJson(isJson: boolean) { 67 | this._isJson = isJson; 68 | } 69 | get isIsJsonDefined(): boolean { 70 | return this._isJson == undefined ? false : true; 71 | } 72 | 73 | private _dateInMilliSeconds?: boolean; 74 | get dateInMilliSeconds(): boolean { 75 | return this._dateInMilliSeconds == undefined 76 | ? schema().dateInMilliSeconds 77 | : this._dateInMilliSeconds; 78 | } 79 | set dateInMilliSeconds(val: boolean) { 80 | this._dateInMilliSeconds = val; 81 | } 82 | get isDateInMilliSecondsDefined(): boolean { 83 | return this._dateInMilliSeconds == undefined ? false : true; 84 | } 85 | 86 | /** 87 | * Flag if this field is part of the primary key 88 | */ 89 | isIdentity: boolean; 90 | 91 | /** 92 | * Creates an instance of Field. 93 | * 94 | */ 95 | public constructor( 96 | name: string, 97 | isIdentity?: boolean, 98 | opts?: FieldOpts, 99 | propertyType?: PropertyType, 100 | ) { 101 | this.name = name; 102 | this.isIdentity = !!isIdentity; 103 | 104 | this.setDbDefaultType(propertyType, opts); 105 | if (opts) { 106 | if (opts.dbtype) { 107 | this.dbtype = opts.dbtype; 108 | } 109 | if (opts.isJson != undefined) { 110 | this._isJson = opts.isJson; 111 | } 112 | if (opts.dateInMilliSeconds != undefined) { 113 | this._dateInMilliSeconds = opts.dateInMilliSeconds; 114 | } 115 | } 116 | } 117 | 118 | setDbDefaultType(propertyType?: PropertyType, opts?: FieldOpts): void { 119 | switch (propertyType) { 120 | case PropertyType.BOOLEAN: 121 | case PropertyType.DATE: 122 | if (opts && opts.notNull) { 123 | this.dbDefaultType = 'INTEGER NOT NULL'; 124 | } else { 125 | this.dbDefaultType = 'INTEGER'; 126 | } 127 | break; 128 | case PropertyType.NUMBER: 129 | if (this.isIdentity) { 130 | this.dbDefaultType = 'INTEGER NOT NULL'; 131 | } else { 132 | if (opts && opts.notNull) { 133 | this.dbDefaultType = 'REAL NOT NULL'; 134 | } else { 135 | this.dbDefaultType = 'REAL'; 136 | } 137 | } 138 | break; 139 | default: 140 | // otherwise 'TEXT' will be used as default 141 | if (opts && opts.notNull) { 142 | this.dbDefaultType = 'TEXT NOT NULL'; 143 | } else { 144 | this.dbDefaultType = 'TEXT'; 145 | } 146 | break; 147 | } 148 | } 149 | 150 | static parseDbType(dbtype: string): DbColumnTypeInfo { 151 | const typeDefMatches = /^\s*((\w+)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\))?)(.*)$/.exec(dbtype); 152 | 153 | /* istanbul ignore if */ 154 | if (!typeDefMatches) { 155 | throw new Error(`failed to parse '${dbtype}'`); 156 | } 157 | const typeAffinity = DbCatalogDAO.getTypeAffinity(typeDefMatches[2]); 158 | const rest = typeDefMatches[5]; 159 | 160 | const notNull = /\bNOT\s+NULL\b/i.exec(rest) ? true : false; 161 | 162 | let defaultValue; 163 | const defaultNumberMatches = /\bDEFAULT\s+([+-]?\d+(\.\d*)?)/i.exec(rest); 164 | if (defaultNumberMatches) { 165 | defaultValue = defaultNumberMatches[1]; 166 | } 167 | const defaultLiteralMatches = /\bDEFAULT\s+(('[^']*')+)/i.exec(rest); 168 | if (defaultLiteralMatches) { 169 | defaultValue = defaultLiteralMatches[1]; 170 | defaultValue.replace(/''/g, "'"); 171 | } 172 | const defaultExprMatches = /\bDEFAULT\s*\(([^)]*)\)/i.exec(rest); 173 | if (defaultExprMatches) { 174 | defaultValue = defaultExprMatches[1]; 175 | } 176 | 177 | // debug(`dbtype='${dbtype}'`); 178 | // debug(`type='${typeName}'`); 179 | // debug(`notNull='${notNull}'`); 180 | // debug(`default='${defaultValue}'`); 181 | return { typeAffinity, notNull, defaultValue }; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/ReadmeSample.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* eslint-disable prefer-const */ 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | import { BaseDAO, field, fk, id, index, schema, SqlDatabase, table } from '..'; 5 | 6 | // definition-part: 7 | 8 | @table({ name: 'USERS' }) 9 | class User { 10 | @id({ name: 'user_id', dbtype: 'INTEGER NOT NULL' }) 11 | userId!: number; 12 | 13 | @field({ name: 'user_loginname', dbtype: 'TEXT NOT NULL' }) 14 | userLoginName!: string; 15 | 16 | @field({ name: 'user_json', dbtype: 'TEXT', isJson: true }) 17 | userJsonData: any; 18 | 19 | @field({ name: 'user_deleted' }) 20 | deleted?: boolean; 21 | } 22 | 23 | @table({ name: 'CONTACTS', autoIncrement: true }) 24 | class Contact { 25 | @id({ name: 'contact_id', dbtype: 'INTEGER NOT NULL' }) 26 | contactId!: number; 27 | 28 | @field({ name: 'contact_email', dbtype: 'TEXT' }) 29 | emailAddress?: string; 30 | 31 | @field({ name: 'contact_mobile', dbtype: 'TEXT' }) 32 | mobile?: string; 33 | 34 | @field({ name: 'user_id', dbtype: 'INTEGER NOT NULL' }) 35 | @fk('fk_user_contacts', 'USERS', 'user_id') 36 | @index('idx_contacts_user') 37 | userId!: number; 38 | } 39 | 40 | async function runSample(): Promise { 41 | let sqldb = new SqlDatabase(); 42 | await sqldb.open(':memory:'); 43 | 44 | // Schema Creation 45 | await (async () => { 46 | // get the user_version from the database: 47 | let userVersion = await sqldb.getUserVersion(); 48 | 49 | // create all the tables if they do not exist: 50 | await schema().createTable(sqldb, 'USERS'); 51 | await schema().createTable(sqldb, 'CONTACTS'); 52 | await schema().createIndex(sqldb, 'CONTACTS', 'idx_contacts_user'); 53 | 54 | if (userVersion >= 1 && userVersion < 10) { 55 | // the 'CONTACTS' table has been introduced in user_version 1 56 | // a column 'contact_mobile' has been added to the 'CONTACTS' 57 | // table in user_version 10 58 | await schema().alterTableAddColumn(sqldb, 'CONTACTS', 'contact_mobile'); 59 | } 60 | await sqldb.setUserVersion(10); 61 | })(); 62 | 63 | // Read/Insert/Update/Delete using DAOs 64 | await (async () => { 65 | let userDAO = new BaseDAO(User, sqldb); 66 | let contactDAO = new BaseDAO(Contact, sqldb); 67 | 68 | // insert a user: 69 | let user = new User(); 70 | user.userId = 1; 71 | user.userLoginName = 'donald'; 72 | user.userJsonData = { lastScores: [10, 42, 31] }; 73 | user = await userDAO.insert(user); 74 | 75 | // insert a contact: 76 | let contact = new Contact(); 77 | contact.userId = 1; 78 | contact.emailAddress = 'donald@duck.com'; 79 | contact = await contactDAO.insert(contact); 80 | 81 | // update a contact: 82 | contact.mobile = '+49 123 456'; 83 | contact = await contactDAO.update(contact); 84 | 85 | // read a user: 86 | let userDonald = await userDAO.select(user); 87 | 88 | expect(userDonald.deleted).toBeFalsy(); 89 | 90 | // update a user partially: 91 | await userDAO.updatePartial({ userId: userDonald.userId, deleted: true }); 92 | 93 | userDonald = await userDAO.select(user); 94 | expect(userDonald.deleted).toBeTruthy(); 95 | 96 | // read all contacts (child) for a given user (parent): 97 | let contactsDonald1 = await contactDAO.selectAllOf('fk_user_contacts', User, userDonald); 98 | // or 99 | let contactsDonald2 = await userDAO.selectAllChildsOf('fk_user_contacts', Contact, userDonald); 100 | 101 | // read all users: 102 | let allUsers = await userDAO.selectAll(); 103 | 104 | // read all users having login-name starting with 'd': 105 | // (see section 'typesafe queries') 106 | let selectedUsers = await userDAO.selectAll({ userLoginName: { isLike: 'd%' } }); 107 | 108 | expect(selectedUsers.length).toBe(1); 109 | 110 | // read all users having a contact: 111 | let allUsersHavingContacts = await userDAO.selectAll( 112 | 'WHERE EXISTS(SELECT 1 FROM CONTACTS C WHERE C.user_id = T.user_id)', 113 | ); 114 | 115 | // read all contacts from 'duck.com': 116 | let allContactsFromDuckDotCom = await contactDAO.selectAll( 117 | 'WHERE contact_email like $contact_email', 118 | { $contact_email: '%@duck.com' }, 119 | ); 120 | 121 | // read user (parent) for a given contact (child) 122 | let userDonald1 = await userDAO.selectByChild('fk_user_contacts', Contact, contactsDonald1[0]); 123 | // or 124 | let userDonald2 = await contactDAO.selectParentOf('fk_user_contacts', User, contactsDonald2[0]); 125 | 126 | expect(userDonald.userId).toBe(user.userId); 127 | expect(userDonald.userLoginName).toBe(user.userLoginName); 128 | expect(userDonald.userJsonData.lastScores.length).toBe(user.userJsonData.lastScores.length); 129 | 130 | expect(userDonald1.userId).toBe(user.userId); 131 | expect(userDonald1.userLoginName).toBe(user.userLoginName); 132 | expect(userDonald1.userJsonData.lastScores.length).toBe(user.userJsonData.lastScores.length); 133 | 134 | expect(userDonald2.userId).toBe(user.userId); 135 | expect(userDonald2.userLoginName).toBe(user.userLoginName); 136 | expect(userDonald2.userJsonData.lastScores.length).toBe(user.userJsonData.lastScores.length); 137 | 138 | expect(contactsDonald1.length).toBe(1); 139 | expect(contactsDonald1[0].userId).toBe(contact.userId); 140 | expect(contactsDonald1[0].emailAddress).toBe(contact.emailAddress); 141 | expect(contactsDonald1[0].mobile).toBe(contact.mobile); 142 | 143 | expect(contactsDonald2.length).toBe(1); 144 | expect(contactsDonald2[0].userId).toBe(contact.userId); 145 | expect(contactsDonald2[0].emailAddress).toBe(contact.emailAddress); 146 | expect(contactsDonald2[0].mobile).toBe(contact.mobile); 147 | 148 | expect(allUsersHavingContacts.length).toBe(1); 149 | expect(allContactsFromDuckDotCom.length).toBe(1); 150 | })(); 151 | } 152 | 153 | describe('test README sample', () => { 154 | // --------------------------------------------- 155 | it('expect README sample to succeed', async () => { 156 | try { 157 | await runSample(); 158 | } catch (err) { 159 | fail(err); 160 | } 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/metadata/datatypes/DataTypeDate.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseDAO, field, id, schema, SQL_MEMORY_DB_PRIVATE, SqlDatabase, table } from '../../..'; 2 | 3 | const DATATYPE_DATE_TABLE = 'DD:DATATYPE_DATE'; 4 | 5 | @table({ name: DATATYPE_DATE_TABLE }) 6 | class DataTypeDate { 7 | @id({ name: 'id', dbtype: 'INTEGER NOT NULL' }) 8 | id!: number; 9 | 10 | @field({ name: 'my_date_text', dbtype: "TEXT DEFAULT(datetime('now') || 'Z')" }) 11 | myDate2Text?: Date; 12 | 13 | @field({ 14 | name: 'my_date_sec', 15 | dbtype: "INTEGER DEFAULT(CAST(strftime('%s','now') as INT))", 16 | dateInMilliSeconds: false, 17 | }) 18 | myDate2Seconds?: Date; 19 | 20 | @field({ 21 | name: 'my_date_milli', 22 | dbtype: "INTEGER DEFAULT(CAST((julianday('now') - 2440587.5)*86400000 AS INT))", 23 | dateInMilliSeconds: true, 24 | }) 25 | myDate2Milliseconds?: Date; 26 | } 27 | 28 | describe('test Date type', () => { 29 | let sqldb: SqlDatabase; 30 | let dao: BaseDAO; 31 | let lastModelId = 1; 32 | 33 | // --------------------------------------------- 34 | beforeEach(async () => { 35 | try { 36 | sqldb = new SqlDatabase(); 37 | await sqldb.open(SQL_MEMORY_DB_PRIVATE); 38 | await schema().createTable(sqldb, DATATYPE_DATE_TABLE); 39 | dao = new BaseDAO(DataTypeDate, sqldb); 40 | } catch (err) { 41 | fail(err); 42 | } 43 | }); 44 | 45 | it('expect writing Date properties to the database to succeed', async () => { 46 | try { 47 | const model: DataTypeDate = new DataTypeDate(); 48 | model.id = ++lastModelId; 49 | // setting to now: 50 | model.myDate2Seconds = model.myDate2Text = model.myDate2Milliseconds = new Date(); 51 | await dao.insert(model); 52 | 53 | const sqlstmt = await sqldb.prepare(`SELECT 54 | id, my_date_text, my_date_sec, my_date_milli 55 | FROM "${DATATYPE_DATE_TABLE}" 56 | WHERE id = :id`); 57 | const row = await sqlstmt.get({ ':id': model.id }); 58 | expect(row.id).toBe(model.id); 59 | expect(row.my_date_text).toBe(model.myDate2Text.toISOString()); 60 | expect(row.my_date_sec).toBe(Math.floor(model.myDate2Text.getTime() / 1000)); 61 | expect(row.my_date_milli).toBe(model.myDate2Text.getTime()); 62 | } catch (err) { 63 | fail(err); 64 | } 65 | }); 66 | 67 | it('expect reading Date properties from database to succeed', async () => { 68 | try { 69 | const writeModel: DataTypeDate = new DataTypeDate(); 70 | writeModel.id = ++lastModelId; 71 | writeModel.myDate2Text = writeModel.myDate2Seconds = writeModel.myDate2Milliseconds = new Date(); 72 | 73 | const sqlstmt = await sqldb.prepare(`INSERT INTO "${DATATYPE_DATE_TABLE}" 74 | (id, my_date_text, my_date_sec, my_date_milli) 75 | values 76 | (:id, :my_date_text, :my_date_sec, :my_date_milli)`); 77 | 78 | await sqlstmt.run({ 79 | ':id': writeModel.id, 80 | ':my_date_text': writeModel.myDate2Text.toISOString(), 81 | ':my_date_sec': Math.floor(writeModel.myDate2Seconds.getTime() / 1000), 82 | ':my_date_milli': writeModel.myDate2Milliseconds.getTime(), 83 | }); 84 | const readModel = await dao.select(writeModel); 85 | expect(readModel.myDate2Text instanceof Date).toBeTruthy(); 86 | expect(readModel.myDate2Seconds instanceof Date).toBeTruthy(); 87 | expect(readModel.myDate2Milliseconds instanceof Date).toBeTruthy(); 88 | 89 | expect(readModel.id).toBe(writeModel.id); 90 | expect(readModel.myDate2Text).toBe(writeModel.myDate2Text); 91 | expect(readModel.myDate2Seconds).toBe(writeModel.myDate2Seconds); 92 | expect(readModel.myDate2Milliseconds).toBe(writeModel.myDate2Milliseconds); 93 | } catch (err) { 94 | fail(err); 95 | } 96 | }); 97 | 98 | it('expect reading Date properties from database defaults to succeed', async () => { 99 | try { 100 | const writeModel: DataTypeDate = new DataTypeDate(); 101 | writeModel.id = ++lastModelId; 102 | 103 | const writeDate = new Date(); 104 | writeDate.setUTCMilliseconds(0); 105 | 106 | const sqlstmt = await sqldb.prepare(`INSERT INTO "${DATATYPE_DATE_TABLE}" 107 | (id) values (:id)`); 108 | await sqlstmt.run({ ':id': writeModel.id }); 109 | 110 | const readModel = await dao.select(writeModel); 111 | 112 | const readDate = new Date(); 113 | readDate.setUTCMilliseconds(0); 114 | readDate.setUTCSeconds(readDate.getUTCSeconds() + 1); 115 | 116 | expect(readModel.myDate2Text).toBeDefined(); 117 | expect(readModel.myDate2Seconds).toBeDefined(); 118 | expect(readModel.myDate2Milliseconds).toBeDefined(); 119 | 120 | expect(readModel.myDate2Text instanceof Date).toBeTruthy(); 121 | expect(readModel.myDate2Seconds instanceof Date).toBeTruthy(); 122 | expect(readModel.myDate2Milliseconds instanceof Date).toBeTruthy(); 123 | 124 | expect(readModel.id).toBe(writeModel.id); 125 | 126 | expect(+writeDate <= +(readModel.myDate2Text as Date)).toBeTruthy(); 127 | expect(+readDate >= +(readModel.myDate2Text as Date)).toBeTruthy(); 128 | 129 | expect(+writeDate <= +(readModel.myDate2Seconds as Date)).toBeTruthy(); 130 | expect(+readDate >= +(readModel.myDate2Seconds as Date)).toBeTruthy(); 131 | 132 | expect(+writeDate <= +(readModel.myDate2Milliseconds as Date)).toBeTruthy(); 133 | expect(+readDate >= +(readModel.myDate2Milliseconds as Date)).toBeTruthy(); 134 | } catch (err) { 135 | fail(err); 136 | } 137 | }); 138 | 139 | it('expect writing undefined Date properties to the database to succeed', async () => { 140 | try { 141 | const model: DataTypeDate = new DataTypeDate(); 142 | model.id = ++lastModelId; 143 | await dao.insert(model); 144 | 145 | const model2: DataTypeDate = await dao.select(model); 146 | expect(model2.id).toBe(model.id); 147 | expect(model2.myDate2Text).toBeUndefined(); 148 | expect(model2.myDate2Seconds).toBeUndefined(); 149 | expect(model2.myDate2Milliseconds).toBeUndefined(); 150 | } catch (err) { 151 | fail(err); 152 | } 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/metadata/ForeignKey.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | BaseDAO, 4 | field, 5 | fk, 6 | id, 7 | index, 8 | qualifiySchemaIdentifier, 9 | schema, 10 | SQL_MEMORY_DB_PRIVATE, 11 | SqlDatabase, 12 | table, 13 | } from '../..'; 14 | 15 | const PREFIX = 'FK:'; 16 | 17 | const PARENT_TABLE_NAME = `${PREFIX} P T`; 18 | const PARENT_TABLE_NAMEQ = qualifiySchemaIdentifier(PARENT_TABLE_NAME, 'main'); 19 | 20 | const PARENT_COL_ID1 = 'parent id'; 21 | const PARENT_COL_ID2 = 'parent id2'; 22 | const PARENT_COL_REF = 'ref'; 23 | 24 | const CHILD_TABLE_NAME = `${PREFIX} CT`; 25 | const CHILD_TABLE_NAMEQ = qualifiySchemaIdentifier(CHILD_TABLE_NAME, 'main'); 26 | 27 | const CHILD_IDX_NAME = `${PREFIX} CI`; 28 | const CHILD_IDX_NAMEQ = qualifiySchemaIdentifier(CHILD_IDX_NAME, 'main'); 29 | 30 | const CHILD_FK_ID_NAME = 'FK PARENT ID'; 31 | const CHILD_FK_REF_NAME = 'FK PARENT REF'; 32 | 33 | @table({ name: PARENT_TABLE_NAMEQ }) 34 | class Parent { 35 | @id({ name: PARENT_COL_ID1, dbtype: 'INTEGER NOT NULL' }) 36 | id1!: number; 37 | 38 | @id({ name: PARENT_COL_ID2, dbtype: 'INTEGER NOT NULL' }) 39 | id2!: number; 40 | 41 | @field({ name: PARENT_COL_REF, dbtype: 'TEXT' }) 42 | ref?: string; 43 | 44 | @field({ name: 'parent info', dbtype: 'TEXT' }) 45 | parentInfo?: string; 46 | } 47 | 48 | @table({ name: CHILD_TABLE_NAMEQ, withoutRowId: true }) 49 | class Child { 50 | @id({ name: 'child id', dbtype: 'INTEGER NOT NULL' }) 51 | id!: number; 52 | 53 | @field({ name: 'child info', dbtype: 'TEXT' }) 54 | childInfo?: string; 55 | 56 | @fk(CHILD_FK_ID_NAME, PARENT_TABLE_NAME, PARENT_COL_ID1) 57 | @field({ name: 'child parent id1', dbtype: 'INTEGER' }) 58 | @index(CHILD_IDX_NAMEQ) 59 | parentId1?: number; 60 | 61 | @fk(CHILD_FK_ID_NAME, PARENT_TABLE_NAME, PARENT_COL_ID2) 62 | @field({ name: 'child parent id2', dbtype: 'INTEGER' }) 63 | @index(CHILD_IDX_NAMEQ) 64 | parentId2?: number; 65 | 66 | @fk(CHILD_FK_REF_NAME, PARENT_TABLE_NAME, PARENT_COL_REF) 67 | @field({ name: 'child parent ref', dbtype: 'TEXT' }) 68 | ref?: string; 69 | } 70 | 71 | // NOTES: it seems sqlite3 does not support schema names for the referenced tabled in foreign key definitions 72 | // e.g try to change 'PARENT_TABLE_NAME2' to 'PARENT_TABLE_NAME' in the @fk decorators above 73 | 74 | async function createSchema(sqldb: SqlDatabase): Promise { 75 | // create all the tables if they do not exist: 76 | await schema().createTable(sqldb, PARENT_TABLE_NAME); 77 | await schema().createTable(sqldb, CHILD_TABLE_NAME); 78 | await schema().createIndex(sqldb, CHILD_TABLE_NAME, CHILD_IDX_NAME); 79 | } 80 | 81 | async function dropSchema(sqldb: SqlDatabase): Promise { 82 | // create all the tables if they do not exist: 83 | await schema().dropTable(sqldb, CHILD_TABLE_NAME); 84 | await schema().dropTable(sqldb, PARENT_TABLE_NAME); 85 | } 86 | 87 | // --------------------------------------------- 88 | 89 | describe('test Foreign Keys', () => { 90 | let sqldb: SqlDatabase; 91 | let parentDAO: BaseDAO; 92 | let childDAO: BaseDAO; 93 | 94 | // --------------------------------------------- 95 | beforeEach(async () => { 96 | try { 97 | sqldb = new SqlDatabase(); 98 | await sqldb.open(SQL_MEMORY_DB_PRIVATE); 99 | 100 | await createSchema(sqldb); 101 | 102 | parentDAO = new BaseDAO(Parent, sqldb); 103 | childDAO = new BaseDAO(Child, sqldb); 104 | } catch (err) { 105 | fail(err); 106 | } 107 | }); 108 | 109 | afterEach(async () => { 110 | try { 111 | await dropSchema(sqldb); 112 | parentDAO = undefined as any; 113 | childDAO = undefined as any; 114 | } catch (err) { 115 | fail(err); 116 | } 117 | }); 118 | 119 | it('expect selectAllOf for foreign key having multiple colums to work', async () => { 120 | let parent = new Parent(); 121 | const child = new Child(); 122 | try { 123 | parent.id1 = 1; 124 | parent.id2 = 1; 125 | parent.parentInfo = '1.1'; 126 | await parentDAO.insert(parent); 127 | 128 | parent.id1 = 2; 129 | parent.id2 = 1; 130 | parent.parentInfo = '2.1'; 131 | await parentDAO.insert(parent); 132 | 133 | parent.id1 = 2; 134 | parent.id2 = 2; 135 | parent.parentInfo = '2.2'; 136 | await parentDAO.insert(parent); 137 | 138 | parent.id1 = 3; 139 | parent.id2 = 1; 140 | parent.parentInfo = '3.1'; 141 | await parentDAO.insert(parent); 142 | 143 | child.id = 1; 144 | child.childInfo = '1 => null'; 145 | await childDAO.insert(child); 146 | 147 | child.id = 2; 148 | child.parentId1 = 2; 149 | child.parentId2 = 1; 150 | child.childInfo = '2 => 2.1'; 151 | await childDAO.insert(child); 152 | 153 | child.id = 3; 154 | child.parentId1 = 2; 155 | child.parentId2 = 2; 156 | child.childInfo = '3 => 2.2'; 157 | await childDAO.insert(child); 158 | 159 | child.id = 4; 160 | child.parentId1 = 2; 161 | child.parentId2 = 1; 162 | child.childInfo = '4 => 2.1'; 163 | await childDAO.insert(child); 164 | 165 | parent.id1 = 2; 166 | parent.id2 = 1; 167 | parent = await parentDAO.selectById(parent); 168 | 169 | let childs: Child[]; 170 | 171 | childs = await childDAO.selectAllOf(CHILD_FK_ID_NAME, Parent, parent); 172 | childs = childs.sort((a, b) => a.id - b.id); 173 | expect(childs.length).toBe(2); 174 | expect(childs[0].id).toBe(2); 175 | expect(childs[1].id).toBe(4); 176 | 177 | childs = await parentDAO.selectAllChildsOf(CHILD_FK_ID_NAME, Child, parent); 178 | childs = childs.sort((a, b) => a.id - b.id); 179 | expect(childs.length).toBe(2); 180 | expect(childs[0].id).toBe(2); 181 | expect(childs[1].id).toBe(4); 182 | 183 | parent = new Parent(); 184 | parent = await childDAO.selectParentOf(CHILD_FK_ID_NAME, Parent, childs[0]); 185 | expect(parent).toBeDefined(); 186 | expect(parent.id1).toBe(2); 187 | expect(parent.id2).toBe(1); 188 | expect(parent.parentInfo).toBe('2.1'); 189 | 190 | parent = new Parent(); 191 | parent = await parentDAO.selectByChild(CHILD_FK_ID_NAME, Child, childs[1]); 192 | expect(parent).toBeDefined(); 193 | expect(parent.id1).toBe(2); 194 | expect(parent.id2).toBe(1); 195 | expect(parent.parentInfo).toBe('2.1'); 196 | } catch (err) { 197 | fail(err); 198 | } 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/Schema.ts: -------------------------------------------------------------------------------- 1 | // import * as core from './core'; 2 | 3 | import { SqlDatabase } from '../core'; 4 | import { qualifiySchemaIdentifier } from '../utils'; 5 | 6 | import { TableOpts } from './decorators'; 7 | import { Table } from './Table'; 8 | 9 | /** 10 | * A singleton holding the database schema definitions 11 | * 12 | * @export 13 | * @class Schema 14 | */ 15 | export class Schema { 16 | /** 17 | * The one and only Schema instance 18 | * 19 | * @static 20 | */ 21 | public static schema: Schema; 22 | 23 | private readonly mapNameToTable!: Map; 24 | 25 | private _dateInMilliSeconds?: boolean; 26 | get dateInMilliSeconds(): boolean { 27 | return this._dateInMilliSeconds == undefined ? false : this._dateInMilliSeconds; 28 | } 29 | set dateInMilliSeconds(val: boolean) { 30 | this._dateInMilliSeconds = val; 31 | } 32 | 33 | /** 34 | * Creates an instance of Schema. 35 | * 36 | */ 37 | public constructor() { 38 | if (!Schema.schema) { 39 | // initialize the 'singleton' 40 | Schema.schema = this; 41 | 42 | this.mapNameToTable = new Map(); 43 | } 44 | return Schema.schema; 45 | } 46 | 47 | /** 48 | * lookup table definition for given table name 49 | * 50 | * @param name - The name of the table 51 | * @returns The table definition or undefined 52 | */ 53 | 54 | public hasTable(name: string): Table | undefined { 55 | return this.mapNameToTable.get(qualifiySchemaIdentifier(name)); 56 | } 57 | 58 | /** 59 | * get a table definition 60 | * 61 | * @param name - The name of the table 62 | * @returns The table definition 63 | */ 64 | public getTable(name: string): Table { 65 | const table = this.mapNameToTable.get(qualifiySchemaIdentifier(name)); 66 | if (!table) { 67 | throw new Error(`table '${name}' not registered yet`); 68 | } 69 | return table; 70 | } 71 | 72 | /** 73 | * add a table definition 74 | * 75 | * @param table - The table definition 76 | * @returns The table definition 77 | */ 78 | public getOrAddTable(name: string, opts: TableOpts): Table { 79 | const qname = qualifiySchemaIdentifier(name); 80 | let table = this.mapNameToTable.get(qname); 81 | 82 | if (!table) { 83 | table = new Table(name); 84 | this.mapNameToTable.set(qname, table); 85 | 86 | if (opts.withoutRowId != undefined) { 87 | table.withoutRowId = opts.withoutRowId; 88 | } 89 | if (opts.autoIncrement != undefined) { 90 | table.autoIncrement = opts.autoIncrement; 91 | } 92 | } else { 93 | if (opts.withoutRowId != undefined) { 94 | if (table.isWithoutRowIdDefined && opts.withoutRowId != table.withoutRowId) { 95 | throw new Error( 96 | `conflicting withoutRowId settings: new: ${opts.withoutRowId}, old ${table.withoutRowId}`, 97 | ); 98 | } 99 | table.withoutRowId = opts.withoutRowId; 100 | } 101 | if (opts.autoIncrement != undefined) { 102 | if (table.isAutoIncrementDefined && opts.autoIncrement != table.autoIncrement) { 103 | throw new Error( 104 | `conflicting autoIncrement settings: new: ${opts.autoIncrement}, old ${table.autoIncrement}`, 105 | ); 106 | } 107 | table.autoIncrement = opts.autoIncrement; 108 | } 109 | } 110 | 111 | return table; 112 | } 113 | 114 | /** 115 | * delete a table definition 116 | * 117 | * @param table - The table definition 118 | */ 119 | public deleteTable(name: string): void { 120 | this.mapNameToTable.delete(qualifiySchemaIdentifier(name)); 121 | } 122 | 123 | /** 124 | * get array of table definitions 125 | * 126 | * @returns The table definitions 127 | */ 128 | public getAllTables(): Table[] { 129 | return Array.from(this.mapNameToTable.values()); 130 | } 131 | 132 | /** 133 | * create a table in the database 134 | * 135 | * @param sqldb - The db connection 136 | * @param name - The name of the table 137 | * @returns A promise 138 | */ 139 | public createTable(sqldb: SqlDatabase, name: string, force?: boolean): Promise { 140 | const table = this.getTable(name); 141 | return sqldb.exec(table.getCreateTableStatement(force)); 142 | } 143 | 144 | /** 145 | * drop a table from the database 146 | * 147 | * @param sqldb - The db connection 148 | * @param name - The name of the table 149 | * @returns A promise 150 | */ 151 | public dropTable(sqldb: SqlDatabase, name: string): Promise { 152 | const table = this.getTable(name); 153 | return sqldb.exec(table.getDropTableStatement()); 154 | } 155 | 156 | /** 157 | * add a column/field to a database table 158 | * 159 | * @param sqldb - The db connection 160 | * @param tableName - The name of the table 161 | * @param colName - The name of the column 162 | * @returns A promise 163 | */ 164 | public alterTableAddColumn( 165 | sqldb: SqlDatabase, 166 | tableName: string, 167 | colName: string, 168 | ): Promise { 169 | const table = this.getTable(tableName); 170 | return sqldb.exec(table.getAlterTableAddColumnStatement(colName)); 171 | } 172 | 173 | /** 174 | * create an index in the database 175 | * 176 | * @param sqldb - The db connection 177 | * @param tableName - The name of the table 178 | * @param idxName - The name of the index 179 | * @param [unique] - create unique index 180 | * @returns A promise 181 | */ 182 | public createIndex( 183 | sqldb: SqlDatabase, 184 | tableName: string, 185 | idxName: string, 186 | unique?: boolean, 187 | ): Promise { 188 | const table = this.getTable(tableName); 189 | return sqldb.exec(table.getCreateIndexStatement(idxName, unique)); 190 | } 191 | 192 | /** 193 | * drop a table from the database 194 | * 195 | * @param sqldb - The db connection 196 | * @param tableName - The name of the table 197 | * @param idxName - The name of the index 198 | * @returns A promise 199 | */ 200 | public dropIndex(sqldb: SqlDatabase, tableName: string, idxName: string): Promise { 201 | const table = this.getTable(tableName); 202 | return sqldb.exec(table.getDropIndexStatement(idxName)); 203 | } 204 | } 205 | 206 | /** 207 | * get the Schema singleton 208 | * 209 | * @export 210 | * @returns {Schema} 211 | */ 212 | export function schema(): Schema { 213 | if (!Schema.schema) { 214 | new Schema(); 215 | } 216 | return Schema.schema; 217 | } 218 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | | Release | Notes | 4 | | --------- | --------------------------------------------------------------------------------------------------------------------------- | 5 | | 2.6.4 | maintenance release | 6 | | 2.6.3 | upgrade node-sqlite3 5.0.3 [TryGhost](https://github.com/TryGhost/node-sqlite3) | 7 | | 2.6.2 | selectOne method | 8 | | 2.6.1 | BaseDao option 'ignoreNoChanges': if set to `true` resolve 'updatePartialAll' and 'deleteAll' with `0` if nothing changed | 9 | | 2.6.0 | [2.6.0 changes](#260-changes) | 10 | | 2.5.5 | new feature: online backup support | 11 | | 2.5.1-4 | maintenance releases | 12 | | 2.5.0 | [2.5.0 changes](#250-changes) | 13 | | 2.4.18 | bug fix: query model: binding `false` values using shorthand form (#74) | 14 | | 2.4.2-17 | maintenance releases | 15 | | 2.4.14 | improved support for sqlcipher: new database settings for 'key' and 'cipherCompatibility' | 16 | | 2.4.2-13 | maintenance releases | 17 | | 2.4.1 | autoupgrade detection for autoIncrement changes | 18 | | 2.4.0 | [2.4.0 changes](#240-changes) | 19 | | 2.3.2 | customizable serialize/deserialize; support for date in milliseconds unix epoch | 20 | | 2.3.1 | descending index columns | 21 | | 2.3.0 | BaseDAO: added partial insert/update/update all, as well as delete all methods | 22 | | 2.2.0 | autoupgrade for automatically creating or upgrade tables and indexes in the database | 23 | | 2.1.0 | [2.1.0 changes](#210-changes) | 24 | | 2.0.0 | [2.0.0 changes](#200-changes) | 25 | | 1.0.1 | maintenance releases | 26 | | 1.0.0 | maintenance releases | 27 | | 0.0.20-24 | maintenance releases | 28 | | 0.0.19 | BaseDAO: added selectById/deleteById methods for convenience | 29 | | 0.0.15-18 | maintenance releases | 30 | | 0.0.14 | new @index decorator and create/drop - index methods | 31 | | 0.0.13 | BaseDAO: added createTable/dropTable/alterTableAddColumn methods for convenience | 32 | | 0.0.10-12 | maintenance releases | 33 | | 0.0.9 | possibility to map properties of complex type to a database column and serialize/deserialize this properties in JSON format | 34 | | 0.0.8 | SqlConnectionPool: allow connections to be garbage-collected if the connection pool is not limited by max-connections | 35 | | 0.0.7 | SqlConnectionPool: a new connection pool | 36 | | 0.0.6 | BaseDAO: ensure type safety for mapped properties of primitive or Date type | 37 | 38 | ## 2.6.0 changes 39 | 40 | ### new features 41 | 42 | - BaseDAO: new 'countAll' method 43 | - BaseDAO: new 'exists' method 44 | - [Online Backup](./README.md#online-backup) introduced in 2.5.2 45 | 46 | ## 2.5.0 changes 47 | 48 | ### new features 49 | 50 | - BaseDAO: new `replace` methods for [REPLACE command](https://www.sqlite.org/lang_replace.html) 51 | - BaseDAO: insert modes; see [insert modes](./README.md#basedao-insert-modes) 52 | 53 | ## 2.4.0 changes 54 | 55 | ### new features 56 | 57 | - typesafe queries 58 | 59 | ### breaking changes 60 | 61 | - MetaModel: get\*Statement() methods have been moved to the new QueryModel 62 | - BaseDAO: protected members have been moved to the new QueryModel 63 | 64 | ## 2.1.0 changes 65 | 66 | ### new features 67 | 68 | - DbCatalogDAO for reading schemas, tables, table-definitions, index-definitions and foreign key-definitions 69 | - SqlDatabaseSettings for applying pragma settings on opening a connection to a database 70 | 71 | ## 2.0.0 changes 72 | 73 | ### new features 74 | 75 | - support for mapping a table to multiple model classes 76 | - support for schema qualified table and index names 77 | - quoted identifiers 78 | - optional parameter 'isUnique' for the 'index' decorator 79 | - BaseDAO 80 | - selectAllChildsOf: same as calling selectAllOf from the child-DAO 81 | - selectByChild: select the parent of a child 82 | - selectParentOf: same as selectByChild from the child-DAO 83 | - debugging utility: [see debug](https://www.npmjs.com/package/debug) 84 | 85 | ### breaking changes 86 | 87 | - BaseDAO 88 | 89 | some protected member-functions have been changed (e.g setHostParam ) or removed (e.g setProperty ). 90 | 91 | - reflect metadata key 'METADATA_TABLE_KEY' 92 | 93 | previously 'METADATA_TABLE_KEY' has been used to reference a Table class instance, 94 | now the 'METADATA_MODEL_KEY' references a MetaModel class instance which is mapped to a Table class instance 95 | 96 | - Table/View 97 | 98 | all model (class and property) related things have been moved to the new MetaModel/MetaProperty classes 99 | the getters for the DML statements have been moved to MetaModel and deprecated getters have been removed 100 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/metadata/datatypes/DataTypeBoolean.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { BaseDAO, field, id, schema, SQL_MEMORY_DB_PRIVATE, SqlDatabase, table } from '../../..'; 3 | 4 | const DATATYPE_BOOLEAN_TABLE = 'DB:DATATYPE_BOOLEAN'; 5 | 6 | @table({ name: DATATYPE_BOOLEAN_TABLE }) 7 | class DataTypeBoolean { 8 | @id({ name: 'id', dbtype: 'INTEGER NOT NULL' }) 9 | id!: number; 10 | 11 | @field({ name: 'my_bool_text', dbtype: 'TEXT' }) 12 | myBool2Text?: boolean; 13 | 14 | @field({ name: 'my_bool_int', dbtype: 'INTEGER' }) 15 | myBool2Int?: boolean; 16 | 17 | @field({ name: 'my_bool_real', dbtype: 'REAL' }) 18 | myBool2Real?: boolean; 19 | } 20 | 21 | describe('test boolean type', () => { 22 | let sqldb: SqlDatabase; 23 | let dao: BaseDAO; 24 | let model: DataTypeBoolean = new DataTypeBoolean(); 25 | // --------------------------------------------- 26 | beforeEach(async () => { 27 | try { 28 | sqldb = new SqlDatabase(); 29 | await sqldb.open(SQL_MEMORY_DB_PRIVATE); 30 | await schema().createTable(sqldb, DATATYPE_BOOLEAN_TABLE); 31 | dao = new BaseDAO(DataTypeBoolean, sqldb); 32 | model.id = 1; 33 | } catch (err) { 34 | fail(err); 35 | } 36 | }); 37 | 38 | it('expect writing boolean properties to the database to succeed', async () => { 39 | try { 40 | const sqlstmt = await sqldb.prepare(`SELECT 41 | id, my_bool_text, my_bool_int, my_bool_real 42 | FROM "${DATATYPE_BOOLEAN_TABLE}" 43 | WHERE id = :id`); 44 | 45 | let row: any; 46 | 47 | // all true 48 | ++model.id; 49 | model.myBool2Text = true; 50 | model.myBool2Int = true; 51 | model.myBool2Real = true; 52 | await dao.insert(model); 53 | row = await sqlstmt.get({ ':id': model.id }); 54 | expect(row.id).toBe(model.id); 55 | expect(row.my_bool_text).toBe('1'); 56 | expect(row.my_bool_int).toBe(1); 57 | expect(row.my_bool_real).toBe(1); 58 | 59 | // all false 60 | ++model.id; 61 | model.myBool2Text = false; 62 | model.myBool2Int = false; 63 | model.myBool2Real = false; 64 | await dao.insert(model); 65 | row = await sqlstmt.get({ ':id': model.id }); 66 | expect(row.id).toBe(model.id); 67 | expect(row.my_bool_text).toBe('0'); 68 | expect(row.my_bool_int).toBe(0); 69 | expect(row.my_bool_real).toBe(0); 70 | 71 | // all undefined 72 | const oldid = ++model.id; 73 | model = new DataTypeBoolean(); 74 | model.id = oldid; 75 | await dao.insert(model); 76 | row = await sqlstmt.get({ ':id': model.id }); 77 | expect(row.id).toBe(model.id); 78 | expect(row.my_bool_text).toBe(null); 79 | expect(row.my_bool_int).toBe(null); 80 | expect(row.my_bool_real).toBe(null); 81 | 82 | await sqlstmt.finalize(); 83 | } catch (err) { 84 | fail(err); 85 | } 86 | }); 87 | 88 | it('expect reading boolean properties from database to succeed', async () => { 89 | try { 90 | const sqlstmt = await sqldb.prepare(`INSERT INTO "${DATATYPE_BOOLEAN_TABLE}" 91 | (id, my_bool_text, my_bool_int, my_bool_real) 92 | values 93 | (:id, :my_bool_text, :my_bool_int, :my_bool_real)`); 94 | 95 | // all true 96 | ++model.id; 97 | await sqlstmt.run({ 98 | ':id': model.id, 99 | ':my_bool_text': true, 100 | ':my_bool_int': true, 101 | ':my_bool_real': true, 102 | }); 103 | model = await dao.select(model); 104 | expect(typeof model.myBool2Text).toBe('boolean'); 105 | expect(typeof model.myBool2Int).toBe('boolean'); 106 | expect(typeof model.myBool2Real).toBe('boolean'); 107 | expect(model.myBool2Text).toBeTruthy(); 108 | expect(model.myBool2Int).toBeTruthy(); 109 | expect(model.myBool2Real).toBeTruthy(); 110 | 111 | // all false 112 | ++model.id; 113 | await sqlstmt.run({ 114 | ':id': model.id, 115 | ':my_bool_text': false, 116 | ':my_bool_int': false, 117 | ':my_bool_real': false, 118 | }); 119 | model = await dao.select(model); 120 | expect(typeof model.myBool2Text).toBe('boolean'); 121 | expect(typeof model.myBool2Int).toBe('boolean'); 122 | expect(typeof model.myBool2Real).toBe('boolean'); 123 | expect(model.myBool2Text).toBeFalsy(); 124 | expect(model.myBool2Int).toBeFalsy(); 125 | expect(model.myBool2Real).toBeFalsy(); 126 | 127 | // all undefined 128 | ++model.id; 129 | await sqlstmt.run({ 130 | ':id': model.id, 131 | ':my_bool_text': undefined, 132 | ':my_bool_int': undefined, 133 | ':my_bool_real': undefined, 134 | }); 135 | model = await dao.select(model); 136 | expect(typeof model.myBool2Text).toBe('undefined'); 137 | expect(typeof model.myBool2Int).toBe('undefined'); 138 | expect(typeof model.myBool2Real).toBe('undefined'); 139 | 140 | // all null 141 | ++model.id; 142 | await sqlstmt.run({ 143 | ':id': model.id, 144 | ':my_bool_text': null, 145 | ':my_bool_int': null, 146 | ':my_bool_real': null, 147 | }); 148 | model = await dao.select(model); 149 | expect(typeof model.myBool2Text).toBe('undefined'); 150 | expect(typeof model.myBool2Int).toBe('undefined'); 151 | expect(typeof model.myBool2Real).toBe('undefined'); 152 | 153 | // myBool2Text is '0' 154 | ++model.id; 155 | await sqlstmt.run({ 156 | ':id': model.id, 157 | ':my_bool_text': '0', 158 | ':my_bool_int': undefined, 159 | ':my_bool_real': undefined, 160 | }); 161 | model = await dao.select(model); 162 | expect(typeof model.myBool2Text).toBe('boolean'); 163 | expect(model.myBool2Text).toBeFalsy(); 164 | 165 | // myBool2Text is '1' 166 | ++model.id; 167 | await sqlstmt.run({ 168 | ':id': model.id, 169 | ':my_bool_text': '1', 170 | ':my_bool_int': undefined, 171 | ':my_bool_real': undefined, 172 | }); 173 | model = await dao.select(model); 174 | expect(typeof model.myBool2Text).toBe('boolean'); 175 | expect(model.myBool2Text).toBeTruthy(); 176 | 177 | // myBool2Text is 'false' 178 | ++model.id; 179 | await sqlstmt.run({ 180 | ':id': model.id, 181 | ':my_bool_text': 'false', 182 | ':my_bool_int': undefined, 183 | ':my_bool_real': undefined, 184 | }); 185 | model = await dao.select(model); 186 | expect(typeof model.myBool2Text).toBe('boolean'); 187 | expect(model.myBool2Text).toBeFalsy(); 188 | 189 | // myBool2Text is 'true' 190 | ++model.id; 191 | await sqlstmt.run({ 192 | ':id': model.id, 193 | ':my_bool_text': 'true', 194 | ':my_bool_int': undefined, 195 | ':my_bool_real': undefined, 196 | }); 197 | model = await dao.select(model); 198 | expect(typeof model.myBool2Text).toBe('boolean'); 199 | expect(model.myBool2Text).toBeTruthy(); 200 | 201 | await sqlstmt.finalize(); 202 | } catch (err) { 203 | fail(err); 204 | } 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /src/sqlite3orm/core/SqlConnectionPool.ts: -------------------------------------------------------------------------------- 1 | import * as _dbg from 'debug'; 2 | import { wait } from '../utils/wait'; 3 | 4 | import { SqlConnectionPoolDatabase } from './SqlConnectionPoolDatabase'; 5 | import { SQL_OPEN_CREATE, SQL_OPEN_DEFAULT, SqlDatabase } from './SqlDatabase'; 6 | import { SqlDatabaseSettings } from './SqlDatabaseSettings'; 7 | 8 | const debug = _dbg('sqlite3orm:pool'); 9 | 10 | /** 11 | * A simple connection pool 12 | * 13 | * @export 14 | * @class SqlConnectionPool 15 | */ 16 | export class SqlConnectionPool { 17 | private databaseFile?: string; 18 | 19 | private mode: number; 20 | 21 | private min: number; 22 | 23 | private max: number; 24 | 25 | private readonly inPool: SqlConnectionPoolDatabase[]; 26 | 27 | private readonly inUse: Set; 28 | 29 | private settings?: SqlDatabaseSettings; 30 | 31 | private _opening?: Promise; 32 | 33 | get poolSize(): number { 34 | return this.inPool.length; 35 | } 36 | get openSize(): number { 37 | return this.inUse.size; 38 | } 39 | 40 | /** 41 | * Creates an instance of SqlConnectionPool. 42 | * 43 | */ 44 | constructor(public readonly name: string = '') { 45 | this.databaseFile = undefined; 46 | this.mode = SQL_OPEN_DEFAULT; 47 | this.inUse = new Set(); 48 | this.inPool = []; 49 | this.min = this.max = 0; 50 | } 51 | 52 | /** 53 | * Open a database connection pool 54 | * 55 | * @param databaseFile - The path to the database file or URI 56 | * @param [mode=SQL_OPEN_DEFAULT] - A bit flag combination of: SQL_OPEN_CREATE | 57 | * SQL_OPEN_READONLY | SQL_OPEN_READWRITE 58 | * @param [min=1] minimum connections which should be opened by this connection pool 59 | * @param [max=0] maximum connections which can be opened by this connection pool 60 | * @returns A promise 61 | */ 62 | async open( 63 | databaseFile: string, 64 | mode: number = SQL_OPEN_DEFAULT, 65 | min: number = 1, 66 | max: number = 0, 67 | settings?: SqlDatabaseSettings, 68 | ): Promise { 69 | if (this._opening) { 70 | try { 71 | await this._opening; 72 | if (this.databaseFile === databaseFile && (mode & ~SQL_OPEN_CREATE) === this.mode) { 73 | // already opened 74 | return; 75 | } 76 | } catch (err) {} 77 | } 78 | this._opening = this.openInternal(databaseFile, mode, min, max, settings); 79 | try { 80 | await this._opening; 81 | } catch (err) { 82 | return Promise.reject(err); 83 | } finally { 84 | this._opening = undefined; 85 | } 86 | return; 87 | } 88 | 89 | protected async openInternal( 90 | databaseFile: string, 91 | mode: number = SQL_OPEN_DEFAULT, 92 | min: number = 1, 93 | max: number = 0, 94 | settings?: SqlDatabaseSettings, 95 | ): Promise { 96 | try { 97 | await this.close(); 98 | } catch (err) {} 99 | try { 100 | this.databaseFile = databaseFile; 101 | this.mode = mode; 102 | this.min = min; 103 | this.max = max; 104 | this.settings = settings; 105 | this.inPool.length = 0; 106 | 107 | const promises: Promise[] = []; 108 | 109 | if (this.min < 1) { 110 | this.min = 1; 111 | } 112 | let sqldb = new SqlConnectionPoolDatabase(); 113 | await sqldb.openByPool(this, this.databaseFile, this.mode, this.settings); 114 | this.inPool.push(sqldb); 115 | 116 | this.mode &= ~SQL_OPEN_CREATE; 117 | for (let i = 1; i < this.min; i++) { 118 | sqldb = new SqlConnectionPoolDatabase(); 119 | promises.push(sqldb.openByPool(this, this.databaseFile, this.mode, this.settings)); 120 | this.inPool.push(sqldb); 121 | } 122 | await Promise.all(promises); 123 | if (this.name.length) { 124 | SqlConnectionPool.openNamedPools.set(this.name, this); 125 | } 126 | debug( 127 | `pool ${this.name}: opened: ${this.inUse.size} connections open (${this.inPool.length} in pool)`, 128 | ); 129 | } catch (err) { 130 | try { 131 | await this.close(); 132 | } catch (_ignore) {} 133 | debug(`pool ${this.name}: opening ${databaseFile} failed: ${err.message}`); 134 | return Promise.reject(err); 135 | } 136 | } 137 | 138 | /** 139 | * Close the database connection pool 140 | * 141 | * @returns A promise 142 | */ 143 | async close(): Promise { 144 | try { 145 | if (this.databaseFile) { 146 | if (this.inUse.size) { 147 | debug( 148 | `pool ${this.name}: closing: forcibly closing ${this.inUse.size} opened connections (${this.inPool.length} in pool)`, 149 | ); 150 | } else { 151 | debug( 152 | `pool ${this.name}: closing: ${this.inUse.size} connections open (${this.inPool.length} in pool)`, 153 | ); 154 | } 155 | } 156 | if (this.name.length) { 157 | SqlConnectionPool.openNamedPools.delete(this.name); 158 | } 159 | this.databaseFile = undefined; 160 | this.mode = SQL_OPEN_DEFAULT; 161 | const promises: Promise[] = []; 162 | this.inPool.forEach((value) => { 163 | promises.push(value.closeByPool()); 164 | }); 165 | this.inPool.length = 0; 166 | this.inUse.forEach((value) => { 167 | promises.push(value.closeByPool()); 168 | }); 169 | this.inUse.clear(); 170 | await Promise.all(promises); 171 | } catch (err) /* istanbul ignore next */ { 172 | debug(`pool ${this.name}: closing failed: ${err.message}`); 173 | return Promise.reject(err); 174 | } 175 | } 176 | 177 | /** 178 | * test if this connection pool is connected to a database file 179 | */ 180 | isOpen(): boolean { 181 | return !!this.databaseFile; 182 | } 183 | 184 | /** 185 | * get a connection from the pool 186 | * 187 | * @param [timeout=0] The timeout to wait for a connection ( 0 is infinite ) 188 | * @returns A promise of the db connection 189 | */ 190 | async get(timeout: number = 0): Promise { 191 | try { 192 | let sqldb: SqlConnectionPoolDatabase | undefined; 193 | const cond = () => this.inPool.length > 0; 194 | if (this.max > 0 && !cond() && this.inUse.size >= this.max) { 195 | await wait(cond, timeout); 196 | } 197 | if (this.inPool.length > 0) { 198 | sqldb = this.inPool.shift() as SqlConnectionPoolDatabase; 199 | this.inUse.add(sqldb); 200 | debug( 201 | `pool ${this.name}: ${this.inUse.size} connections open (${this.inPool.length} in pool)`, 202 | ); 203 | return sqldb; 204 | } 205 | if (!this.databaseFile) { 206 | throw new Error(`connection pool not opened`); 207 | } 208 | sqldb = new SqlConnectionPoolDatabase(); 209 | await sqldb.openByPool(this, this.databaseFile, this.mode, this.settings); 210 | this.inUse.add(sqldb); 211 | debug( 212 | `pool ${this.name}: ${this.inUse.size} connections open (${this.inPool.length} in pool)`, 213 | ); 214 | return sqldb; 215 | } catch (err) { 216 | debug(`pool ${this.name}: getting connection from pool failed: ${err.message}`); 217 | return Promise.reject(err); 218 | } 219 | } 220 | 221 | /** 222 | * release a connection to the pool 223 | * 224 | * @param sqldb - The db connection 225 | */ 226 | async release(sqldb: SqlDatabase): Promise { 227 | /* istanbul ignore if */ 228 | if (!(sqldb instanceof SqlConnectionPoolDatabase) || this !== sqldb.pool) { 229 | // not opened by this pool 230 | return sqldb.close(); 231 | } 232 | this.inUse.delete(sqldb); 233 | /* istanbul ignore else */ 234 | if (sqldb.isOpen()) { 235 | if (sqldb.dirty || this.inPool.length >= this.min) { 236 | // close database connection 237 | await sqldb.closeByPool(); 238 | } else { 239 | // transfer database connection 240 | const newsqldb = new SqlConnectionPoolDatabase(); 241 | await newsqldb.recycleByPool(this, sqldb, this.settings); 242 | this.inPool.push(newsqldb); 243 | } 244 | debug( 245 | `pool ${this.name}: ${this.inUse.size} connections open (${this.inPool.length} in pool)`, 246 | ); 247 | } 248 | } 249 | 250 | static readonly openNamedPools: Map = new Map< 251 | string, 252 | SqlConnectionPool 253 | >(); 254 | } 255 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/decorators.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import 'reflect-metadata'; 3 | 4 | import { MetaModel, KeyType } from './MetaModel'; 5 | import { ValueTransformer } from './ValueTransformer'; 6 | 7 | export const METADATA_MODEL_KEY = 'sqlite3orm:model'; 8 | 9 | /** 10 | * Options for the '@table' class decorator 11 | * 12 | * @export 13 | * @interface TableOpts 14 | */ 15 | 16 | export interface TableOpts { 17 | /** 18 | * [name] - The name of the table 19 | */ 20 | name?: string; 21 | 22 | /** 23 | * [withoutRowId] - Flag to indicate if table should be created using the 'WITHOUT ROWID' 24 | * clause 25 | */ 26 | withoutRowId?: boolean; 27 | /** 28 | * [autoIncrement] - Flag to indicate if AUTOINCREMENT should be added to single-column INTEGER 29 | * primary keys 30 | */ 31 | autoIncrement?: boolean; 32 | } 33 | 34 | /** 35 | * Options for the property decorators '@field' and '@id' 36 | * 37 | * @export 38 | * @interface FieldOpts 39 | */ 40 | 41 | export interface FieldOpts { 42 | /** 43 | * [name] - The name of the table field 44 | */ 45 | name?: string; 46 | /** 47 | * [dbtype] - The column definition 48 | */ 49 | dbtype?: string; 50 | /** 51 | * [isJson] - Flag to indicate if field should be persisted as json string 52 | */ 53 | isJson?: boolean; 54 | /* 55 | * [notNull] - Flag to indicate if this field is required (ignored if `dbtype` is explicitly set) 56 | */ 57 | notNull?: boolean; 58 | /* 59 | * [dateInMilliSeconds] - If date is stored as integer use milliseconds instead of seconds 60 | */ 61 | dateInMilliSeconds?: boolean; 62 | 63 | /** 64 | * [transform] - serialize/deserialize functions 65 | */ 66 | transform?: ValueTransformer; 67 | } 68 | 69 | /** 70 | * Get the model metadata 71 | * 72 | * @param target - The constructor of the class 73 | * @returns The meta model 74 | */ 75 | export function getModelMetadata(target: Function): MetaModel { 76 | if (!Reflect.hasOwnMetadata(METADATA_MODEL_KEY, target.prototype)) { 77 | Reflect.defineMetadata(METADATA_MODEL_KEY, new MetaModel(target.name), target.prototype); 78 | } 79 | return Reflect.getMetadata(METADATA_MODEL_KEY, target.prototype); 80 | } 81 | 82 | /** 83 | * Helper function for decorating a class and map it to a database table 84 | * 85 | * @param target - The constructor of the class 86 | * @param [opts] - The options for this table 87 | */ 88 | function decorateTableClass(target: Function, opts: TableOpts): void { 89 | const metaModel = getModelMetadata(target); 90 | metaModel.init(opts); 91 | } 92 | 93 | /** 94 | * Helper function for decorating a property and map it to a table field 95 | * 96 | * @param target - The decorated class 97 | * @param key - The decorated property 98 | * @param [opts] - The options for this field 99 | * @param [isIdentity=false] - Indicator if this field belongs to the 100 | * primary key 101 | * @returns The field class instance 102 | */ 103 | function decorateFieldProperty( 104 | target: Object | Function, 105 | key: KeyType, 106 | opts: FieldOpts, 107 | isIdentity: boolean, 108 | ): void { 109 | if (typeof target === 'function') { 110 | // not decorating static property 111 | throw new Error( 112 | `decorating static property '${key.toString()}' using field-decorator is not supported`, 113 | ); 114 | } 115 | 116 | const metaModel = getModelMetadata(target.constructor); 117 | const metaProp = metaModel.getOrAddProperty(key); 118 | /* istanbul ignore if */ 119 | if (typeof key === 'number') { 120 | key = key.toString(); 121 | } 122 | metaProp.setPropertyType(Reflect.getMetadata('design:type', target, key)); 123 | metaModel.setPropertyField(key, isIdentity, opts); 124 | } 125 | 126 | /** 127 | * Helper function for decorating a property and map it to a foreign key field 128 | * 129 | * @param target - The decorated class 130 | * @param key - The decorated property 131 | * @param constraintName - The name for the foreign key constraint 132 | * @param foreignTableName - The referenced table name 133 | * @param foreignTableField - The referenced table field 134 | * @returns - The field class instance 135 | */ 136 | function decorateForeignKeyProperty( 137 | target: Object | Function, 138 | key: KeyType, 139 | constraintName: string, 140 | foreignTableName: string, 141 | foreignTableField: string, 142 | ): void { 143 | if (typeof target === 'function') { 144 | // not decorating static property 145 | throw new Error( 146 | `decorating static property '${key.toString()}' using fk-decorator is not supported`, 147 | ); 148 | } 149 | 150 | const metaModel = getModelMetadata(target.constructor); 151 | metaModel.setPropertyForeignKey(key, constraintName, foreignTableName, foreignTableField); 152 | } 153 | 154 | /** 155 | * Helper function for decorating a property and map it to an index field 156 | * 157 | * @param target - The decorated class 158 | * @param key - The decorated property 159 | * @param indexName - The name for the index 160 | * @param [isUnique] - is a unique index 161 | * @param [desc] - descending order for this column 162 | * @returns The field class instance 163 | */ 164 | function decorateIndexProperty( 165 | target: Object | Function, 166 | key: KeyType, 167 | indexName: string, 168 | isUnique?: boolean, 169 | desc?: boolean, 170 | ): void { 171 | if (typeof target === 'function') { 172 | // not decorating static property 173 | throw new Error( 174 | `decorating static property '${key.toString()}' using index-decorator is not supported`, 175 | ); 176 | } 177 | 178 | const metaModel = getModelMetadata(target.constructor); 179 | metaModel.setPropertyIndexKey(key, indexName, isUnique, desc); 180 | } 181 | 182 | /*****************************************************************************************/ 183 | /* decorators: 184 | 185 | /** 186 | * The class decorator for mapping a database table to a class 187 | * 188 | * @export 189 | * @param [opts] 190 | * @returns The decorator function 191 | */ 192 | export function table(opts: TableOpts = {}): (target: Function) => void { 193 | return (target: Function) => decorateTableClass(target, opts); 194 | } 195 | 196 | /** 197 | * The property decorator for mapping a table field to a class property 198 | * 199 | * @export 200 | * @param [name] - The name of the field; defaults to the property name 201 | * @param [dbtype] - The type of the field; defaults to 'TEXT' 202 | * @returns The decorator function 203 | */ 204 | export function field(opts: FieldOpts = {}): (target: Object, key: KeyType) => void { 205 | return (target: Object, key: KeyType) => { 206 | decorateFieldProperty(target, key, opts, false); 207 | }; 208 | } 209 | 210 | /** 211 | * The id decorator for mapping a field of the primary key to a class property 212 | * 213 | * @export 214 | * @param [name] - The name of the field; defaults to the property name 215 | * @param [dbtype] - The type of the field; defaults to 'TEXT' 216 | * @returns The decorator function 217 | */ 218 | export function id(opts: FieldOpts = {}): (target: Object, key: KeyType) => void { 219 | return (target: Object, key: KeyType) => { 220 | decorateFieldProperty(target, key, opts, true); 221 | }; 222 | } 223 | 224 | /** 225 | * The fk decorator for mapping a class property to be part of a foreign key 226 | * constraint 227 | * 228 | * @export 229 | * @param constraintName - The constraint name 230 | * @param foreignTableName - The referenced table name 231 | * @param foreignTableField - The referenced table field 232 | * @returns The decorator function 233 | */ 234 | export function fk( 235 | constraintName: string, 236 | foreignTableName: string, 237 | foreignTableField: string, 238 | ): (target: Object, key: KeyType) => void { 239 | return (target: Object, key: KeyType) => { 240 | decorateForeignKeyProperty(target, key, constraintName, foreignTableName, foreignTableField); 241 | }; 242 | } 243 | 244 | /** 245 | * The index decorator for mapping a class property to be part of an index 246 | * 247 | * @export 248 | * @param indexName - The index name 249 | * @param [isUnique] - index is unique 250 | * @param [desc] - descending order for this column 251 | * @returns The decorator function 252 | */ 253 | export function index( 254 | indexName: string, 255 | isUnique?: boolean, 256 | desc?: boolean, 257 | ): (target: Object, key: KeyType) => void { 258 | return (target: Object, key: KeyType) => { 259 | decorateIndexProperty(target, key, indexName, isUnique, desc); 260 | }; 261 | } 262 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/MetaModel.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | import { MetaProperty } from './MetaProperty'; 4 | import { TableOpts, FieldOpts } from './decorators'; 5 | import { Table } from './Table'; 6 | import { schema } from './Schema'; 7 | import { FKDefinition } from './FKDefinition'; 8 | import { IDXDefinition } from './IDXDefinition'; 9 | import { QueryModelCache } from '../query/QueryModelBase'; 10 | 11 | export type KeyType = string | number | symbol; 12 | 13 | interface PropertyFieldOptions { 14 | name: string; 15 | isIdentity: boolean; 16 | opts: FieldOpts; 17 | } 18 | 19 | interface PropertyForeignKeyOptions { 20 | constraintName: string; 21 | foreignTableName: string; 22 | foreignTableField: string; 23 | } 24 | 25 | interface PropertyIndexOptions { 26 | name: string; 27 | isUnique?: boolean; 28 | desc?: boolean; 29 | } 30 | 31 | interface PropertyOptions { 32 | field?: Map; 33 | fk?: Map>; 34 | index?: Map>; 35 | } 36 | 37 | export class MetaModel { 38 | public readonly properties: Map; 39 | public readonly mapColNameToProp: Map; 40 | 41 | private _table?: Table; 42 | get table(): Table { 43 | /* istanbul ignore else */ 44 | if (this._table) { 45 | return this._table; 46 | } 47 | /* istanbul ignore next */ 48 | throw new Error(`meta model '${this.name}' not fully initialized yet`); 49 | } 50 | 51 | private opts: PropertyOptions; 52 | 53 | qmCache!: QueryModelCache; // initialized by QueryModel (BaseDAO,..) 54 | 55 | constructor(public readonly name: string) { 56 | this.properties = new Map(); 57 | this.mapColNameToProp = new Map(); 58 | this.opts = {}; 59 | } 60 | 61 | hasProperty(key: KeyType): MetaProperty | undefined { 62 | return this.properties.get(key); 63 | } 64 | 65 | getProperty(key: KeyType): MetaProperty { 66 | const prop = this.properties.get(key); 67 | if (prop) { 68 | return prop; 69 | } 70 | throw new Error(`property '${key.toString()}' not defined for meta model '${this.name}'`); 71 | } 72 | 73 | getOrAddProperty(key: KeyType): MetaProperty { 74 | let prop = this.properties.get(key); 75 | if (!prop) { 76 | prop = new MetaProperty(this.name, key); 77 | this.properties.set(key, prop); 78 | } 79 | return prop; 80 | } 81 | 82 | setPropertyField(key: KeyType, isIdentity: boolean, opts: FieldOpts): void { 83 | this.getOrAddProperty(key); 84 | if (!this.opts.field) { 85 | this.opts.field = new Map(); 86 | } 87 | let fieldOpts = this.opts.field.get(key); 88 | if (fieldOpts) { 89 | throw new Error( 90 | `property '${this.name}.${key.toString()}' already mapped to '${fieldOpts.name}'`, 91 | ); 92 | } 93 | fieldOpts = { name: opts.name || key.toString(), isIdentity, opts }; 94 | this.opts.field.set(key, fieldOpts); 95 | } 96 | 97 | setPropertyForeignKey( 98 | key: KeyType, 99 | constraintName: string, 100 | foreignTableName: string, 101 | foreignTableField: string, 102 | ): void { 103 | this.getOrAddProperty(key); 104 | if (!this.opts.fk) { 105 | this.opts.fk = new Map>(); 106 | } 107 | let propertyFkOpts = this.opts.fk.get(key); 108 | if (!propertyFkOpts) { 109 | propertyFkOpts = new Map(); 110 | this.opts.fk.set(key, propertyFkOpts); 111 | } 112 | if (propertyFkOpts.has(constraintName)) { 113 | throw new Error( 114 | `property '${ 115 | this.name 116 | }.${key.toString()}' already mapped to foreign key '${constraintName}'`, 117 | ); 118 | } 119 | propertyFkOpts.set(constraintName, { constraintName, foreignTableName, foreignTableField }); 120 | } 121 | 122 | setPropertyIndexKey(key: KeyType, indexName: string, isUnique?: boolean, desc?: boolean): void { 123 | this.getOrAddProperty(key); 124 | if (!this.opts.index) { 125 | this.opts.index = new Map>(); 126 | } 127 | let propertyIdxOpts = this.opts.index.get(key); 128 | if (!propertyIdxOpts) { 129 | propertyIdxOpts = new Map(); 130 | this.opts.index.set(key, propertyIdxOpts); 131 | } 132 | if (propertyIdxOpts.has(indexName)) { 133 | throw new Error( 134 | `property '${this.name}.${key.toString()}' already mapped to index '${indexName}'`, 135 | ); 136 | } 137 | propertyIdxOpts.set(indexName, { name: indexName, isUnique, desc }); 138 | } 139 | 140 | init(tableOpts: TableOpts): void { 141 | if (this._table) { 142 | throw new Error(`meta model '${this.name}' already mapped to '${this._table.name}'`); 143 | } 144 | const tableName = tableOpts.name || this.name; 145 | try { 146 | this._table = schema().getOrAddTable(tableName, tableOpts); 147 | } catch (err) { 148 | throw new Error(`meta model '${this.name}': failed to add table: ${err.message}`); 149 | } 150 | 151 | const idxDefs = new Map(); 152 | const fkDefs = new Map(); 153 | 154 | /* istanbul ignore if */ 155 | if (!this.opts.field) { 156 | this.opts.field = new Map(); 157 | } 158 | 159 | // after all the decoraters have run and a table has been created 160 | // we are able to fully initialize all properties: 161 | this.properties.forEach((prop, key) => { 162 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 163 | let fieldOpts = this.opts.field!.get(key); 164 | /* istanbul ignore if */ 165 | if (!fieldOpts) { 166 | fieldOpts = { name: key.toString(), isIdentity: false, opts: {} }; 167 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 168 | this.opts.field!.set(key, fieldOpts); 169 | } 170 | prop.init(this, fieldOpts.name, fieldOpts.isIdentity, fieldOpts.opts); 171 | 172 | const allPropIdxOpts = this.opts.index && this.opts.index.get(key); 173 | if (allPropIdxOpts) { 174 | allPropIdxOpts.forEach((propIdxOpts, idxName) => { 175 | let idxDef = idxDefs.get(idxName); 176 | if (!idxDef) { 177 | idxDef = new IDXDefinition(idxName, propIdxOpts.isUnique); 178 | idxDefs.set(idxName, idxDef); 179 | } else { 180 | // test for conflicting isUniqe setting 181 | if (propIdxOpts.isUnique != undefined) { 182 | if (idxDef.isUnique != undefined && propIdxOpts.isUnique !== idxDef.isUnique) { 183 | throw new Error( 184 | `property '${ 185 | this.name 186 | }.${prop.key.toString()}': conflicting index uniqueness setting`, 187 | ); 188 | } 189 | idxDef.isUnique = propIdxOpts.isUnique; 190 | } 191 | } 192 | idxDef.fields.push({ name: prop.field.name, desc: propIdxOpts.desc }); 193 | }); 194 | } 195 | 196 | const allPropFkOpts = this.opts.fk && this.opts.fk.get(key); 197 | if (allPropFkOpts) { 198 | allPropFkOpts.forEach((propFkOpts, constraintName) => { 199 | let fkDef = fkDefs.get(constraintName); 200 | if (!fkDef) { 201 | fkDef = new FKDefinition(constraintName, propFkOpts.foreignTableName); 202 | fkDefs.set(constraintName, fkDef); 203 | } else { 204 | // test for conflicting foreign table setting 205 | if (propFkOpts.foreignTableName !== fkDef.foreignTableName) { 206 | throw new Error( 207 | `property '${ 208 | this.name 209 | }.${prop.key.toString()}': conflicting foreign table setting: new: '${ 210 | propFkOpts.foreignTableName 211 | }', old '${fkDef.foreignTableName}'`, 212 | ); 213 | } 214 | } 215 | fkDef.fields.push({ 216 | name: prop.field.name, 217 | foreignColumnName: propFkOpts.foreignTableField, 218 | }); 219 | }); 220 | } 221 | }); 222 | 223 | idxDefs.forEach((idxDef) => { 224 | this.table.addIDXDefinition(idxDef); 225 | }); 226 | 227 | fkDefs.forEach((fkDef) => { 228 | this.table.addFKDefinition(fkDef); 229 | }); 230 | 231 | this.table.models.add(this); 232 | this.opts = {}; 233 | } 234 | 235 | destroy(): void { 236 | if (this._table) { 237 | this._table.models.delete(this); 238 | if (!this.table.models.size) { 239 | schema().deleteTable(this._table.name); 240 | } 241 | this._table = undefined; 242 | (this.properties as any) = new Map(); 243 | (this.mapColNameToProp as any) = new Map(); 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/sqlite3orm/dbcatalog/DbCatalogDAO.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { SQL_DEFAULT_SCHEMA, SqlDatabase } from '../core'; 3 | import { FKDefinition } from '../metadata'; 4 | import { quoteSimpleIdentifier, splitSchemaIdentifier } from '../utils'; 5 | 6 | import { 7 | DbColumnInfo, 8 | DbForeignKeyInfo, 9 | DbIndexColumnInfo, 10 | DbIndexInfo, 11 | DbTableInfo, 12 | } from './DbTableInfo'; 13 | 14 | export class DbCatalogDAO { 15 | sqldb: SqlDatabase; 16 | 17 | constructor(sqldb: SqlDatabase) { 18 | this.sqldb = sqldb; 19 | } 20 | 21 | readSchemas(): Promise { 22 | const schemas: string[] = []; 23 | return this.sqldb.all(`PRAGMA database_list`).then((res) => { 24 | res.forEach((db) => schemas.push(db.name)); 25 | return schemas; 26 | }); 27 | } 28 | 29 | readTables(schemaName: string): Promise { 30 | const tables: string[] = []; 31 | const quotedSchemaName = quoteSimpleIdentifier(schemaName); 32 | return this.sqldb 33 | .all(`select * from ${quotedSchemaName}.sqlite_master where type='table'`) 34 | .then((res) => { 35 | res.forEach((tab) => tables.push(tab.name)); 36 | return tables; 37 | }); 38 | } 39 | 40 | async readTableInfo(tableName: string, schemaName?: string): Promise { 41 | try { 42 | const { identName, identSchema } = splitSchemaIdentifier(tableName); 43 | tableName = identName; 44 | schemaName = identSchema || schemaName || SQL_DEFAULT_SCHEMA; 45 | 46 | const quotedName = quoteSimpleIdentifier(tableName); 47 | const quotedSchema = quoteSimpleIdentifier(schemaName); 48 | 49 | // TODO: sqlite3 issue regarding schema queries from multiple connections 50 | // The result of table_info seems to be somehow cached, so subsequent calls to table_info may return wrong results 51 | // The scenario where this problem was detected: 52 | // connection 1: PRAGMA table_info('FOO_TABLE') => ok (no data) 53 | // connection 2: PRAGMA table_info('FOO_TABLE') => ok (no data) 54 | // connection 2: CREATE TABLE FOO_TABLE (...) 55 | // connection 3: PRAGMA table_info('FOO_TABLE') => ok (data) 56 | // connection 2: PRAGMA table_info('FOO_TABLE') => ok (data) 57 | // connection 1: PRAGMA table_info('FOO_TABLE') => NOT OK (NO DATA) 58 | // known workarounds: 59 | // 1) perform all schema discovery and schema modifications from the same connection 60 | // 2) if using a connection pool, do not recycle a connection after performing schema queries 61 | // 3) not verified yet: using shared cache 62 | 63 | // workaround for issue described above (required by e.g 'loopback-connector-sqlite3x') 64 | this.sqldb.dirty = true; 65 | 66 | const tableInfo = await this.callSchemaQueryPragma('table_info', quotedName, quotedSchema); 67 | if (tableInfo.length === 0) { 68 | return undefined; 69 | } 70 | const idxList = await this.callSchemaQueryPragma('index_list', quotedName, quotedSchema); 71 | const fkList = await this.callSchemaQueryPragma('foreign_key_list', quotedName, quotedSchema); 72 | 73 | const info: DbTableInfo = { 74 | name: `${schemaName}.${tableName}`, 75 | tableName, 76 | schemaName, 77 | columns: {}, 78 | primaryKey: [], 79 | indexes: {}, 80 | foreignKeys: {}, 81 | }; 82 | 83 | tableInfo 84 | .sort((colA, colB) => colA.pk - colB.pk) 85 | .forEach((col) => { 86 | const colInfo: DbColumnInfo = { 87 | name: col.name, 88 | type: col.type, 89 | typeAffinity: DbCatalogDAO.getTypeAffinity(col.type), 90 | notNull: !!col.notnull, 91 | defaultValue: col.dflt_value, 92 | }; 93 | info.columns[col.name] = colInfo; 94 | if (col.pk) { 95 | info.primaryKey.push(col.name); 96 | } 97 | }); 98 | 99 | if ( 100 | info.primaryKey.length === 1 && 101 | info.columns[info.primaryKey[0]].typeAffinity === 'INTEGER' 102 | ) { 103 | // dirty hack to check if this column is autoincrementable 104 | // not checked: if autoincrement is part of column/index/foreign key name 105 | // not checked: if autoincrement is part of default literal text 106 | // however, test is sufficient for autoupgrade 107 | const schema = quotedSchema || '"main"'; 108 | const res = await this.sqldb.all( 109 | `select * from ${schema}.sqlite_master where type='table' and name=:tableName and UPPER(sql) like '%AUTOINCREMENT%'`, 110 | { ':tableName': tableName }, 111 | ); 112 | if (res && res.length === 1) { 113 | info.autoIncrement = true; 114 | } 115 | } 116 | 117 | const promises: Promise[] = []; 118 | idxList.forEach((idx) => { 119 | if (idx.origin !== 'pk') { 120 | promises.push( 121 | new Promise((resolve, reject) => { 122 | const idxInfo: DbIndexInfo = { 123 | name: idx.name, 124 | unique: !!idx.unique, 125 | partial: !!idx.partial, 126 | columns: [], 127 | }; 128 | this.callSchemaQueryPragma( 129 | 'index_xinfo', 130 | quoteSimpleIdentifier(idx.name), 131 | quotedSchema, 132 | ) 133 | .then((xinfo) => { 134 | xinfo 135 | .sort((idxColA, idxColB) => idxColA.seqno - idxColB.seqno) 136 | .forEach((idxCol) => { 137 | if (idxCol.cid >= 0) { 138 | const idxColInfo: DbIndexColumnInfo = { 139 | name: idxCol.name, 140 | desc: !!idxCol.desc, 141 | coll: idxCol.coll, 142 | key: !!idxCol.key, 143 | }; 144 | idxInfo.columns.push(idxColInfo); 145 | } 146 | }); 147 | return idxInfo; 148 | }) 149 | .then((val) => resolve(val)) 150 | .catch(/* istanbul ignore next */ (err) => reject(err)); 151 | }), 152 | ); 153 | } 154 | }); 155 | const indexInfos = await Promise.all(promises); 156 | indexInfos.forEach((idxInfo) => { 157 | info.indexes[idxInfo.name] = idxInfo; 158 | }); 159 | 160 | // NOTE: because we are currently not able to discover the FK constraint name 161 | // (not reported by 'foreign_key_list' pragma) 162 | // we are currently using a 'genericForeignKeyId' here, which is readable, but does not look like an identifier 163 | let lastId: number; 164 | let lastFk: any; 165 | let fromCols: string[] = []; 166 | let toCols: string[] = []; 167 | fkList 168 | .sort((fkA, fkB) => fkA.id * 1000 + fkA.seq - (fkB.id * 1000 + fkB.seq)) 169 | .forEach((fk) => { 170 | if (lastId === fk.id) { 171 | // continue 172 | fromCols.push(fk.from); 173 | toCols.push(fk.to); 174 | } else { 175 | // old fk 176 | if (lastFk) { 177 | const fkInfo: DbForeignKeyInfo = { 178 | refTable: lastFk.table, 179 | columns: fromCols, 180 | refColumns: toCols, 181 | }; 182 | info.foreignKeys[ 183 | FKDefinition.genericForeignKeyId(fromCols, lastFk.table, toCols) 184 | ] = fkInfo; 185 | } 186 | // new fk 187 | lastId = fk.id; 188 | lastFk = fk; 189 | fromCols = []; 190 | toCols = []; 191 | fromCols.push(fk.from); 192 | toCols.push(fk.to); 193 | } 194 | }); 195 | if (lastFk) { 196 | const fkInfo: DbForeignKeyInfo = { 197 | refTable: lastFk.table, 198 | columns: fromCols, 199 | refColumns: toCols, 200 | }; 201 | info.foreignKeys[FKDefinition.genericForeignKeyId(fromCols, lastFk.table, toCols)] = fkInfo; 202 | } 203 | return info; 204 | } catch (err) { 205 | /* istanbul ignore next */ 206 | return Promise.reject(err); 207 | } 208 | } 209 | 210 | protected callSchemaQueryPragma( 211 | pragmaName: string, 212 | identifierName: string, 213 | identifierSchema: string, 214 | ): Promise { 215 | return this.sqldb.all(`PRAGMA ${identifierSchema}.${pragmaName}(${identifierName})`); 216 | } 217 | 218 | static getTypeAffinity(typeDef: string): string { 219 | const type = typeDef.toUpperCase(); 220 | if (type.indexOf('INT') !== -1) { 221 | return 'INTEGER'; 222 | } 223 | const textMatches = /(CHAR|CLOB|TEXT)/.exec(type); 224 | if (textMatches) { 225 | return 'TEXT'; 226 | } 227 | if (type.indexOf('BLOB') !== -1) { 228 | return 'BLOB'; 229 | } 230 | const realMatches = /(REAL|FLOA|DOUB)/.exec(type); 231 | if (realMatches) { 232 | return 'REAL'; 233 | } 234 | return 'NUMERIC'; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/sqlite3orm/spec/dbcatalog/DbCatalogDAO.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 3 | import { 4 | BaseDAO, 5 | DbCatalogDAO, 6 | field, 7 | fk, 8 | FKDefinition, 9 | id, 10 | index, 11 | SQL_MEMORY_DB_PRIVATE, 12 | SqlDatabase, 13 | table, 14 | } from '../..'; 15 | 16 | const PREFIX = 'DC'; 17 | 18 | const PARENT_TABLE = `${PREFIX}:PARENT TABLE`; 19 | const PARENT_TABLEQ = `main.${PARENT_TABLE}`; 20 | const CHILD_TABLE = `${PREFIX}:CHILD TABLE`; 21 | const CHILD_TABLEQ = `main.${CHILD_TABLE}`; 22 | 23 | @table({ name: PARENT_TABLEQ }) 24 | class ParentTable { 25 | @id({ name: 'ID1', dbtype: 'INTEGER NOT NULL' }) 26 | id1!: number; 27 | 28 | @id({ name: 'ID2', dbtype: 'INTEGER NOT NULL' }) 29 | id2!: number; 30 | } 31 | 32 | @table({ name: CHILD_TABLEQ, autoIncrement: true }) 33 | class ChildTable { 34 | @field({ name: 'PID2', dbtype: 'INTEGER' }) 35 | @fk('PARENT1', PARENT_TABLE, 'ID2') 36 | @index('PIDX1') 37 | pid2?: number; 38 | 39 | @field({ name: 'PID1', dbtype: 'INTEGER' }) 40 | @fk('PARENT1', PARENT_TABLE, 'ID1') 41 | @index('PIDX1') 42 | pid1?: number; 43 | 44 | @id({ name: 'ID', dbtype: 'INTEGER NOT NULL' }) 45 | id!: number; 46 | 47 | @field({ name: 'PID3', dbtype: 'INTEGER' }) 48 | @fk('PARENT2', PARENT_TABLE, 'ID1') 49 | @index('PIDX2') 50 | pid3?: number; 51 | 52 | @field({ name: 'PID4', dbtype: 'INTEGER' }) 53 | @fk('PARENT2', PARENT_TABLE, 'ID2') 54 | @index('PIDX2', false, true) 55 | pid4?: number; 56 | } 57 | 58 | @table({ name: CHILD_TABLEQ }) 59 | class ChildTableSubset { 60 | @field({ name: 'PID3', dbtype: 'INTEGER' }) 61 | @fk('PARENT2', PARENT_TABLE, 'ID1') 62 | @index('PIDX2') 63 | pid3?: number; 64 | 65 | @field({ name: 'PID4', dbtype: 'INTEGER' }) 66 | @fk('PARENT2', PARENT_TABLE, 'ID2') 67 | @index('PIDX2', false, true) 68 | pid4?: number; 69 | } 70 | 71 | // --------------------------------------------- 72 | 73 | describe('test DbTableInfo.discover', () => { 74 | let sqldb: SqlDatabase; 75 | let parentDao: BaseDAO; 76 | let childDao: BaseDAO; 77 | let dbCatDao: DbCatalogDAO; 78 | 79 | // --------------------------------------------- 80 | beforeEach(async () => { 81 | try { 82 | sqldb = new SqlDatabase(); 83 | await sqldb.open(SQL_MEMORY_DB_PRIVATE); 84 | 85 | parentDao = new BaseDAO(ParentTable, sqldb); 86 | childDao = new BaseDAO(ChildTable, sqldb); 87 | dbCatDao = new DbCatalogDAO(sqldb); 88 | 89 | await parentDao.createTable(); 90 | await childDao.createTable(); 91 | await childDao.createIndex('PIDX1'); 92 | await childDao.createIndex('PIDX2'); 93 | } catch (err) { 94 | fail(err); 95 | } 96 | }); 97 | 98 | // --------------------------------------------- 99 | afterEach(async () => { 100 | try { 101 | await childDao.dropTable(); 102 | await parentDao.dropTable(); 103 | } catch (err) { 104 | fail(err); 105 | } 106 | }); 107 | 108 | // --------------------------------------------- 109 | it('expect discovered schema info to match ', async () => { 110 | try { 111 | const schemas = await dbCatDao.readSchemas(); 112 | expect(schemas).toBeDefined(); 113 | expect(schemas[0]).toBe('main'); 114 | 115 | let tables = await dbCatDao.readTables('main'); 116 | 117 | expect(tables).toBeDefined(); 118 | tables = tables.filter((t) => t.indexOf(PREFIX) === 0).sort((a, b) => a.localeCompare(b)); 119 | expect(tables.length).toBe(2); 120 | expect(tables[0]).toBe(CHILD_TABLE); 121 | expect(tables[1]).toBe(PARENT_TABLE); 122 | 123 | const invalidInfo = await dbCatDao.readTableInfo('NOT EXISTING TABLE'); 124 | expect(invalidInfo).toBeUndefined(); 125 | } catch (err) { 126 | fail(err); 127 | } 128 | }); 129 | 130 | // --------------------------------------------- 131 | it('expect discovered table info to match ', async () => { 132 | try { 133 | const parentInfo = await dbCatDao.readTableInfo(PARENT_TABLEQ); 134 | const childInfo = await dbCatDao.readTableInfo(CHILD_TABLEQ); 135 | expect(parentInfo).toBeDefined(); 136 | expect(parentInfo!.name).toBe(PARENT_TABLEQ); 137 | 138 | expect(Object.keys(parentInfo!.columns).length).toBe(2); 139 | expect(parentInfo!.autoIncrement).toBeFalsy(); 140 | 141 | expect(parentInfo!.columns.ID1).toBeDefined(); 142 | expect(parentInfo!.columns.ID1.type).toBe('INTEGER'); 143 | expect(parentInfo!.columns.ID1.notNull).toBe(true); 144 | expect(parentInfo!.columns.ID1.defaultValue).toBe(null); 145 | 146 | expect(parentInfo!.columns.ID2).toBeDefined(); 147 | expect(parentInfo!.columns.ID2).toBeDefined(); 148 | expect(parentInfo!.columns.ID2.type).toBe('INTEGER'); 149 | expect(parentInfo!.columns.ID2.notNull).toBe(true); 150 | expect(parentInfo!.columns.ID2.defaultValue).toBe(null); 151 | 152 | expect(parentInfo!.primaryKey.length).toBe(2); 153 | expect(parentInfo!.primaryKey[0]).toBe('ID1'); 154 | expect(parentInfo!.primaryKey[1]).toBe('ID2'); 155 | 156 | expect(Object.keys(parentInfo!.indexes).length).toBe(0); 157 | expect(Object.keys(parentInfo!.foreignKeys).length).toBe(0); 158 | 159 | expect(childInfo).toBeDefined(); 160 | expect(childInfo!.name).toBe(CHILD_TABLEQ); 161 | expect(childInfo!.autoIncrement).toBeTruthy(); 162 | 163 | expect(Object.keys(childInfo!.columns).length).toBe(5); 164 | 165 | expect(childInfo!.columns.ID).toBeDefined(); 166 | expect(childInfo!.columns.ID.type).toBe('INTEGER'); 167 | expect(childInfo!.columns.ID.notNull).toBe(true); 168 | expect(childInfo!.columns.ID.defaultValue).toBe(null); 169 | 170 | expect(childInfo!.columns.PID1).toBeDefined(); 171 | expect(childInfo!.columns.PID1.type).toBe('INTEGER'); 172 | expect(childInfo!.columns.PID1.notNull).toBe(false); 173 | expect(childInfo!.columns.PID1.defaultValue).toBe(null); 174 | 175 | expect(childInfo!.columns.PID2).toBeDefined(); 176 | expect(childInfo!.columns.PID2.type).toBe('INTEGER'); 177 | expect(childInfo!.columns.PID2.notNull).toBe(false); 178 | expect(childInfo!.columns.PID2.defaultValue).toBe(null); 179 | 180 | expect(childInfo!.columns.PID3).toBeDefined(); 181 | expect(childInfo!.columns.PID3.type).toBe('INTEGER'); 182 | expect(childInfo!.columns.PID3.notNull).toBe(false); 183 | expect(childInfo!.columns.PID3.defaultValue).toBe(null); 184 | 185 | expect(childInfo!.columns.PID4).toBeDefined(); 186 | expect(childInfo!.columns.PID4.type).toBe('INTEGER'); 187 | expect(childInfo!.columns.PID4.notNull).toBe(false); 188 | expect(childInfo!.columns.PID4.defaultValue).toBe(null); 189 | 190 | expect(childInfo!.primaryKey.length).toBe(1); 191 | expect(childInfo!.primaryKey[0]).toBe('ID'); 192 | 193 | expect(Object.keys(childInfo!.indexes).length).toBe(2); 194 | expect(childInfo!.indexes.PIDX1).toBeDefined(); 195 | expect(childInfo!.indexes.PIDX1.name).toBe('PIDX1'); 196 | expect(childInfo!.indexes.PIDX1.unique).toBe(false); 197 | expect(childInfo!.indexes.PIDX1.partial).toBe(false); 198 | expect(childInfo!.indexes.PIDX1.columns).toBeDefined(); 199 | expect(childInfo!.indexes.PIDX1.columns.length).toBe(2); 200 | 201 | expect(childInfo!.indexes.PIDX1.columns[0].name).toBe('PID2'); 202 | expect(childInfo!.indexes.PIDX1.columns[0].desc).toBe(false); 203 | expect(childInfo!.indexes.PIDX1.columns[1].name).toBe('PID1'); 204 | expect(childInfo!.indexes.PIDX1.columns[1].desc).toBe(false); 205 | 206 | expect(Object.keys(childInfo!.foreignKeys).length).toBe(2); 207 | 208 | const fkName1 = FKDefinition.genericForeignKeyId(['PID2', 'PID1'], PARENT_TABLE, [ 209 | 'ID2', 210 | 'ID1', 211 | ]); 212 | expect(childInfo!.foreignKeys[fkName1]).toBeDefined(); 213 | expect(childInfo!.foreignKeys[fkName1].refTable).toBe(PARENT_TABLE); 214 | 215 | expect(childInfo!.foreignKeys[fkName1].columns.length).toBe(2); 216 | expect(childInfo!.foreignKeys[fkName1].columns[0]).toBe('PID2'); 217 | expect(childInfo!.foreignKeys[fkName1].columns[1]).toBe('PID1'); 218 | 219 | expect(childInfo!.foreignKeys[fkName1].refColumns.length).toBe(2); 220 | expect(childInfo!.foreignKeys[fkName1].refColumns[0]).toBe('ID2'); 221 | expect(childInfo!.foreignKeys[fkName1].refColumns[1]).toBe('ID1'); 222 | 223 | const fkName2 = FKDefinition.genericForeignKeyId(['PID3', 'PID4'], PARENT_TABLE, [ 224 | 'ID1', 225 | 'ID2', 226 | ]); 227 | expect(childInfo!.foreignKeys[fkName2]).toBeDefined(); 228 | expect(childInfo!.foreignKeys[fkName2].refTable).toBe(PARENT_TABLE); 229 | 230 | expect(childInfo!.foreignKeys[fkName2].columns.length).toBe(2); 231 | expect(childInfo!.foreignKeys[fkName2].columns[0]).toBe('PID3'); 232 | expect(childInfo!.foreignKeys[fkName2].columns[1]).toBe('PID4'); 233 | 234 | expect(childInfo!.foreignKeys[fkName2].refColumns.length).toBe(2); 235 | expect(childInfo!.foreignKeys[fkName2].refColumns[0]).toBe('ID1'); 236 | expect(childInfo!.foreignKeys[fkName2].refColumns[1]).toBe('ID2'); 237 | } catch (err) { 238 | fail(err); 239 | } 240 | }); 241 | 242 | // --------------------------------------------- 243 | it("expect type affinity for 'BLOB' to be 'BLOB'", () => { 244 | expect(DbCatalogDAO.getTypeAffinity('BLOB')).toBe('BLOB'); 245 | }); 246 | 247 | // --------------------------------------------- 248 | it("expect type affinity for 'FOO' to be 'NUMERIC' (default)", () => { 249 | expect(DbCatalogDAO.getTypeAffinity('FOO')).toBe('NUMERIC'); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /src/sqlite3orm/query/QueryModel.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/ban-types */ 3 | import { SqlDatabase } from '../core'; 4 | 5 | import { Filter } from './Filter'; 6 | import { QueryCondition } from './QueryCondition'; 7 | import { QueryModelBase } from './QueryModelBase'; 8 | import { QueryModelPredicates } from './QueryModelPredicates'; 9 | import { isModelPredicates, Where } from './Where'; 10 | 11 | export class QueryModel extends QueryModelBase { 12 | constructor(type: { new (): T }) { 13 | super(type); 14 | } 15 | 16 | /** 17 | * count all models using an optional filter 18 | * 19 | * @param sqldb - The database connection 20 | * @param [filter] - An optional Filter-object 21 | * @param [params] - An optional object with additional host parameter 22 | * @returns promise of the number of models 23 | */ 24 | async countAll(sqldb: SqlDatabase, filter?: Filter, params?: Object): Promise { 25 | try { 26 | params = Object.assign({}, params); 27 | const select = await this.getSelectStatementForColumnExpression( 28 | 'COUNT(*) as result', 29 | filter || {}, 30 | params, 31 | ); 32 | const row: any = await sqldb.get(select, params); 33 | return row.result || 0; 34 | } catch (e /* istanbul ignore next */) { 35 | return Promise.reject(new Error(`count '${this.table.name}' failed: ${e.message}`)); 36 | } 37 | } 38 | 39 | /** 40 | * check if model exist using an optional filter 41 | * 42 | * @param sqldb - The database connection 43 | * @param [filter] - An optional Filter-object 44 | * @param [params] - An optional object with additional host parameter 45 | * @returns promise for boolean result 46 | */ 47 | async exists(sqldb: SqlDatabase, filter?: Filter, params?: Object): Promise { 48 | try { 49 | params = Object.assign({}, params); 50 | const subQuery = await this.getSelectStatementForColumnExpression('1', filter || {}, params); 51 | const select = `SELECT EXISTS(\n${subQuery}) as result\n`; 52 | const row: any = await sqldb.get(select, params); 53 | return row.result ? true : false; 54 | } catch (e /* istanbul ignore next */) { 55 | return Promise.reject(new Error(`count '${this.table.name}' failed: ${e.message}`)); 56 | } 57 | } 58 | 59 | /** 60 | * Select one model using an optional filter 61 | * 62 | * @param sqldb - The database connection 63 | * @param [filter] - An optional Filter-object 64 | * @param [params] - An optional object with additional host parameter 65 | * @returns A promise of the selected model instance; rejects if result is not exactly one row 66 | */ 67 | async selectOne(sqldb: SqlDatabase, filter?: Filter, params?: Object): Promise { 68 | try { 69 | params = Object.assign({}, params); 70 | const select = await this.getSelectStatement(this.toSelectAllColumnsFilter(filter), params); 71 | const rows: any[] = await sqldb.all(select, params); 72 | if (rows.length != 1) { 73 | return Promise.reject( 74 | new Error(`select '${this.table.name}' failed: unexpectedly got ${rows.length} rows`), 75 | ); 76 | } 77 | return this.updateModelFromRow(new this.type(), rows[0]); 78 | } catch (e) { 79 | return Promise.reject(new Error(`select '${this.table.name}' failed: ${e.message}`)); 80 | } 81 | } 82 | 83 | /** 84 | * Select all models using an optional filter 85 | * 86 | * @param sqldb - The database connection 87 | * @param [filter] - An optional Filter-object 88 | * @param [params] - An optional object with additional host parameter 89 | * @returns A promise of array of model instances 90 | */ 91 | async selectAll(sqldb: SqlDatabase, filter?: Filter, params?: Object): Promise { 92 | try { 93 | params = Object.assign({}, params); 94 | const select = await this.getSelectStatement(this.toSelectAllColumnsFilter(filter), params); 95 | const rows: any[] = await sqldb.all(select, params); 96 | const results: T[] = []; 97 | rows.forEach((row) => { 98 | results.push(this.updateModelFromRow(new this.type(), row)); 99 | }); 100 | return results; 101 | } catch (e) { 102 | return Promise.reject(new Error(`select '${this.table.name}' failed: ${e.message}`)); 103 | } 104 | } 105 | 106 | /** 107 | * Select all partial models using a filter 108 | * 109 | * @param sqldb - The database connection 110 | * @param filter - A Filter-object 111 | * @param [params] - An optional object with additional host parameter 112 | * @returns A promise of array of partial models 113 | */ 114 | async selectPartialAll( 115 | sqldb: SqlDatabase, 116 | filter: Filter, 117 | params?: Object, 118 | ): Promise[]> { 119 | try { 120 | params = Object.assign({}, params); 121 | const select = await this.getSelectStatement(filter, params); 122 | const rows: any[] = await sqldb.all(select, select); 123 | const results: Partial[] = []; 124 | rows.forEach((row) => { 125 | results.push(this.getPartialFromRow(row)); 126 | }); 127 | return results; 128 | } catch (e /* istanbul ignore next */) { 129 | return Promise.reject(new Error(`select '${this.table.name}' failed: ${e.message}`)); 130 | } 131 | } 132 | 133 | /** 134 | * Select a given model by ID 135 | * 136 | * @param sqldb - The database connection 137 | * @param model - The input/output model 138 | * @returns A promise of the model instance 139 | */ 140 | async selectModel(sqldb: SqlDatabase, model: T): Promise { 141 | try { 142 | const row = await sqldb.get( 143 | this.getSelectByIdStatement(), 144 | this.bindPrimaryKeyInputParams(model), 145 | ); 146 | model = this.updateModelFromRow(model, row); 147 | } catch (e /* istanbul ignore next */) { 148 | return Promise.reject(new Error(`select '${this.table.name}' failed: ${e.message}`)); 149 | } 150 | return model; 151 | } 152 | 153 | /** 154 | * Select a model by given partial model 155 | * 156 | * @param sqldb - The database connection 157 | * @param input - The partial model providing the ID 158 | * @returns A promise of the model 159 | */ 160 | async selectModelById(sqldb: SqlDatabase, input: Partial): Promise { 161 | let model: T = new this.type(); 162 | try { 163 | const row = await sqldb.get( 164 | this.getSelectByIdStatement(), 165 | this.bindPrimaryKeyInputParams(input), 166 | ); 167 | model = this.updateModelFromRow(model, row); 168 | } catch (e /* istanbul ignore next */) { 169 | return Promise.reject(new Error(`select '${this.table.name}' failed: ${e.message}`)); 170 | } 171 | return model; 172 | } 173 | 174 | /* 175 | * select each model using a callback 176 | */ 177 | async selectEach( 178 | sqldb: SqlDatabase, 179 | callback: (err: Error, model: T) => void, 180 | filter?: Filter, 181 | params?: Object, 182 | ): Promise { 183 | try { 184 | params = Object.assign({}, params); 185 | const select = await this.getSelectStatement(this.toSelectAllColumnsFilter(filter), params); 186 | const res = await sqldb.each(select, params, (err, row) => { 187 | // TODO: err? 188 | callback(err, this.updateModelFromRow(new this.type(), row)); 189 | }); 190 | return res; 191 | } catch (e) { 192 | return Promise.reject(new Error(`select '${this.table.name}' failed: ${e.message}`)); 193 | } 194 | } 195 | 196 | public async getWhereClause(filter: Filter, params: Object): Promise { 197 | if (!filter || !filter.where) { 198 | return ''; 199 | } 200 | let where: Where = filter.where; 201 | if (typeof where === 'string') { 202 | where = where.trimStart(); 203 | if (!where.length) { 204 | return ''; 205 | } 206 | if (where.substring(0, 5).toUpperCase() !== 'WHERE') { 207 | return `WHERE ${where}`; 208 | } 209 | return where; 210 | } 211 | const tableAlias = filter.tableAlias ? filter.tableAlias : undefined; 212 | const tablePrefix = tableAlias && tableAlias.length ? `${tableAlias}.` : ''; 213 | 214 | let oper: QueryCondition | QueryModelPredicates; 215 | if (isModelPredicates(where)) { 216 | oper = new QueryModelPredicates(where); 217 | } else { 218 | oper = new QueryCondition(where); 219 | } 220 | 221 | const whereClause = await oper.toSql(this.metaModel, params, tablePrefix); 222 | return whereClause.length ? `WHERE ${whereClause}` : whereClause; 223 | } 224 | 225 | protected async getSelectStatement(filter: Filter, params: Object): Promise { 226 | try { 227 | let sql = this.getSelectAllStatement(this.getSelectColumns(filter), filter.tableAlias); 228 | sql += await this.getNonColumnClauses(filter, params); 229 | return sql; 230 | } catch (e) { 231 | return Promise.reject(e); 232 | } 233 | } 234 | 235 | protected async getSelectStatementForColumnExpression( 236 | colexpr: string, 237 | filter: Filter, 238 | params: Object, 239 | ): Promise { 240 | try { 241 | let sql = this.getSelectAllStatementForColumnExpression(colexpr, filter.tableAlias); 242 | sql += await this.getNonColumnClauses(filter, params); 243 | return sql; 244 | } catch (e /* istanbul ignore next */) { 245 | return Promise.reject(e); 246 | } 247 | } 248 | 249 | protected getSelectColumns(filter: Filter): (keyof T)[] | undefined { 250 | if (!filter.select) { 251 | return undefined; 252 | } 253 | const columns: (keyof T)[] = []; 254 | for (const key in filter.select) { 255 | if (Object.prototype.hasOwnProperty.call(filter.select, key) && filter.select[key]) { 256 | const prop = this.metaModel.properties.get(key); 257 | if (!prop) { 258 | continue; 259 | } 260 | columns.push(key); 261 | } 262 | } 263 | return columns.length ? columns : undefined; 264 | } 265 | 266 | protected async getNonColumnClauses(filter: Filter, params: Object): Promise { 267 | let sql = ''; 268 | const whereClause = await this.getWhereClause(filter, params); 269 | if (whereClause.length) { 270 | sql += ` ${whereClause}\n`; 271 | } 272 | const orderByClause = this.getOrderByClause(filter); 273 | if (orderByClause.length) { 274 | sql += ` ${orderByClause}\n`; 275 | } 276 | const limitClause = this.getLimitClause(filter); 277 | if (limitClause.length) { 278 | sql += ` ${limitClause}\n`; 279 | } 280 | const offsetClause = this.getOffsetClause(filter); 281 | if (offsetClause.length) { 282 | sql += ` ${offsetClause}\n`; 283 | } 284 | return sql; 285 | } 286 | 287 | protected getOrderByClause(filter?: Filter): string { 288 | if (!filter || !filter.order) { 289 | return ''; 290 | } 291 | const columns: string[] = []; 292 | for (const key in filter.order) { 293 | /* istanbul ignore if */ 294 | if (!Object.prototype.hasOwnProperty.call(filter.order, key)) { 295 | continue; 296 | } 297 | const prop = this.metaModel.properties.get(key); 298 | if (!prop) { 299 | continue; 300 | } 301 | if (filter.order[key]) { 302 | columns.push(prop.field.quotedName); 303 | } else { 304 | columns.push(`${prop.field.quotedName} DESC`); 305 | } 306 | } 307 | if (!columns.length) { 308 | return ''; 309 | } 310 | const tableAlias = filter.tableAlias ? filter.tableAlias : undefined; 311 | const tablePrefix = tableAlias && tableAlias.length ? `${tableAlias}.` : ''; 312 | return `ORDER BY ${tablePrefix}` + columns.join(`, ${tablePrefix}`); 313 | } 314 | 315 | protected getLimitClause(filter?: Filter): string { 316 | if (!filter || !filter.limit) { 317 | return ''; 318 | } 319 | return `LIMIT ${filter.limit}`; 320 | } 321 | 322 | protected getOffsetClause(filter?: Filter): string { 323 | if (!filter || !filter.offset) { 324 | return ''; 325 | } 326 | return ` OFFSET ${filter.offset}`; 327 | } 328 | 329 | protected toSelectAllColumnsFilter(filter?: Filter): Filter { 330 | const res = Object.assign({}, filter); 331 | if (res.select) { 332 | delete res.select; 333 | } 334 | return res; 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/sqlite3orm/query/QueryModelBase.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | import { METADATA_MODEL_KEY, MetaModel, MetaProperty, Table } from '../metadata'; 5 | 6 | export const TABLEALIAS = 'T'; 7 | 8 | export class QueryModelBase { 9 | readonly type: { new (): T }; 10 | readonly metaModel: MetaModel; 11 | readonly table: Table; 12 | 13 | constructor(type: { new (): T }) { 14 | this.type = type; 15 | this.metaModel = Reflect.getMetadata(METADATA_MODEL_KEY, type.prototype); 16 | if (!this.metaModel) { 17 | throw new Error(`no table-definition defined on prototype of ${this.type.name}'`); 18 | } 19 | this.table = this.metaModel.table; 20 | if (!this.metaModel.qmCache) { 21 | this.metaModel.qmCache = this.buildCache(); 22 | } 23 | } 24 | 25 | /** 26 | * Get 'SELECT ALL'-statement 27 | * 28 | * @returns The sql-statement 29 | */ 30 | public getSelectAllStatement(keys?: K[], tableAlias?: string): string { 31 | tableAlias = tableAlias || ''; 32 | const tablePrefix = tableAlias.length ? `${tableAlias}.` : ''; 33 | const props = this.getPropertiesFromKeys(keys); 34 | let stmt = 'SELECT\n'; 35 | stmt += 36 | ` ${tablePrefix}` + props.map((prop) => prop.field.quotedName).join(`,\n ${tablePrefix}`); 37 | stmt += `\nFROM ${this.table.quotedName} ${tableAlias}\n`; 38 | return stmt; 39 | } 40 | 41 | /** 42 | * Get 'SELECT BY PRIMARY KEY'-statement 43 | * 44 | * @returns The sql-statement 45 | */ 46 | public getSelectByIdStatement(keys?: K[], tableAlias?: string): string { 47 | const tablePrefix = tableAlias && tableAlias.length ? `${tableAlias}.` : ''; 48 | let stmt = this.getSelectAllStatement(keys, tableAlias); 49 | stmt += 'WHERE\n'; 50 | stmt += 51 | ` ${tablePrefix}` + this.metaModel.qmCache.primaryKeyPredicates.join(` AND ${tablePrefix}`); 52 | return stmt; 53 | } 54 | 55 | /** 56 | * Get 'SELECT ALL'-statement using provided column expression 57 | * 58 | * @returns The sql-statement 59 | */ 60 | public getSelectAllStatementForColumnExpression(colexpr: string, tableAlias?: string): string { 61 | tableAlias = tableAlias || ''; 62 | return `SELECT\n${colexpr}\nFROM ${this.table.quotedName} ${tableAlias}\n`; 63 | } 64 | 65 | /** 66 | * Get 'UPDATE ALL' statement 67 | * 68 | * @returns The sql-statement 69 | */ 70 | public getUpdateAllStatement(keys?: K[]): string { 71 | let props = this.getPropertiesFromKeys(keys); 72 | props = props.filter((prop) => !prop.field.isIdentity); 73 | 74 | /* istanbul ignore if */ 75 | if (!props.length) { 76 | throw new Error(`no columns to update'`); 77 | } 78 | 79 | let stmt = `UPDATE ${this.table.quotedName} SET\n `; 80 | stmt += props 81 | .map((prop) => `${prop.field.quotedName} = ${prop.getHostParameterName()}`) 82 | .join(',\n '); 83 | return stmt; 84 | } 85 | 86 | /** 87 | * Get 'UPDATE BY PRIMARY KEY' statement 88 | * 89 | * @returns The sql-statement 90 | */ 91 | public getUpdateByIdStatement(keys?: K[]): string { 92 | let stmt = this.getUpdateAllStatement(keys); 93 | stmt += '\nWHERE\n '; 94 | stmt += this.metaModel.qmCache.primaryKeyPredicates.join(' AND '); 95 | return stmt; 96 | } 97 | 98 | /** 99 | * Get 'DELETE ALL'-statement 100 | * 101 | * @returns The sql-statement 102 | */ 103 | public getDeleteAllStatement(): string { 104 | return `DELETE FROM ${this.table.quotedName}`; 105 | } 106 | 107 | /** 108 | * Get 'DELETE BY PRIMARY KEY'-statement 109 | * 110 | * @returns The sql-statement 111 | */ 112 | public getDeleteByIdStatement(): string { 113 | let stmt = this.getDeleteAllStatement(); 114 | stmt += '\nWHERE\n '; 115 | stmt += this.metaModel.qmCache.primaryKeyPredicates.join(' AND '); 116 | return stmt; 117 | } 118 | 119 | /** 120 | * Get 'INSERT INTO'-statement 121 | * 122 | * @returns The sql-statement 123 | */ 124 | public getInsertIntoStatement(keys?: K[]): string { 125 | const props = this.getPropertiesFromKeys(keys); 126 | if (!props.length) { 127 | return `INSERT INTO ${this.table.quotedName} DEFAULT VALUES`; 128 | } 129 | 130 | let stmt = `INSERT INTO ${this.table.quotedName} (\n `; 131 | stmt += props.map((prop) => prop.field.quotedName).join(', '); 132 | stmt += '\n) VALUES (\n '; 133 | stmt += props.map((prop) => prop.getHostParameterName()).join(', '); 134 | stmt += '\n)'; 135 | return stmt; 136 | } 137 | 138 | /** 139 | * Get 'REPLACE INTO'-statement 140 | * 141 | * @returns The sql-statement 142 | */ 143 | public getInsertOrReplaceStatement(keys?: K[]): string { 144 | const props = this.getPropertiesFromKeys(keys); 145 | if (!props.length) { 146 | return `INSERT OR REPLACE INTO ${this.table.quotedName} DEFAULT VALUES`; 147 | } 148 | let stmt = `INSERT OR REPLACE INTO ${this.table.quotedName} (\n `; 149 | stmt += props.map((prop) => prop.field.quotedName).join(', '); 150 | stmt += '\n) VALUES (\n '; 151 | stmt += props.map((prop) => prop.getHostParameterName()).join(', '); 152 | stmt += '\n)'; 153 | return stmt; 154 | } 155 | 156 | /** 157 | * Get a select-condition for a foreign key constraint 158 | * 159 | * @param constraintName - The constraint name 160 | * @returns The partial where-clause 161 | */ 162 | public getForeignKeyPredicates(constraintName: string): string[] | undefined { 163 | return this.metaModel.qmCache.foreignKeyPredicates.get(constraintName); 164 | } 165 | 166 | /** 167 | * Get the foreign key (child) properties for a foreign key constraint 168 | * 169 | * @param constraintName - The constraint name 170 | * @returns The properties holding the foreign key 171 | */ 172 | public getForeignKeyProps(constraintName: string): MetaProperty[] | undefined { 173 | return this.metaModel.qmCache.foreignKeyProps.get(constraintName); 174 | } 175 | 176 | /** 177 | * Get the reference (parent) columns for a foreign key constraint 178 | * 179 | * @param constraintName - The constraint name 180 | * @returns The referenced column names 181 | */ 182 | public getForeignKeyRefCols(constraintName: string): string[] | undefined { 183 | return this.metaModel.qmCache.foreignKeyRefCols.get(constraintName); 184 | } 185 | 186 | public getPropertiesFromKeys(keys?: (keyof T)[], addIdentity?: boolean): MetaProperty[] { 187 | if (!keys) { 188 | return Array.from(this.metaModel.properties.values()); 189 | } 190 | const res: Map = new Map(); 191 | keys.forEach((key) => { 192 | const prop = this.metaModel.properties.get(key); 193 | if (!prop) { 194 | return; 195 | } 196 | res.set(key, prop); 197 | }); 198 | /* istanbul ignore if */ 199 | if (addIdentity) { 200 | // for later use 201 | this.metaModel.qmCache.primaryKeyProps 202 | .filter((prop: MetaProperty) => !res.has(prop.key)) 203 | .forEach((prop) => { 204 | res.set(prop.key, prop); 205 | }); 206 | } 207 | return Array.from(res.values()); 208 | } 209 | 210 | public getPropertiesFromColumnNames( 211 | cols: string[], 212 | notFoundCols?: string[], 213 | ): MetaProperty[] | undefined { 214 | const resProps: MetaProperty[] = []; 215 | /* istanbul ignore if */ 216 | if (!notFoundCols) { 217 | notFoundCols = []; 218 | } 219 | cols.forEach((colName) => { 220 | const refProp = this.metaModel.mapColNameToProp.get(colName); 221 | /* istanbul ignore else */ 222 | if (refProp) { 223 | resProps.push(refProp); 224 | } else { 225 | (notFoundCols as string[]).push(colName); 226 | } 227 | }); 228 | /* istanbul ignore if */ 229 | if (notFoundCols.length) { 230 | return undefined; 231 | } 232 | return resProps; 233 | } 234 | 235 | public setHostParam(hostParams: any, prop: MetaProperty, model: Partial): void { 236 | hostParams[prop.getHostParameterName()] = prop.getDBValueFromModel(model); 237 | } 238 | 239 | public setHostParamValue(hostParams: any, prop: MetaProperty, value: any): void { 240 | hostParams[prop.getHostParameterName()] = value; 241 | } 242 | 243 | public updateModelFromRow(model: T, row: any): T { 244 | this.metaModel.properties.forEach((prop) => { 245 | prop.setDBValueIntoModel(model, row[prop.field.name]); 246 | }); 247 | return model; 248 | } 249 | 250 | public getPartialFromRow(row: any): Partial { 251 | const res: Partial = {}; 252 | this.metaModel.properties.forEach((prop) => { 253 | if (row[prop.field.name] !== undefined) { 254 | prop.setDBValueIntoModel(res, row[prop.field.name]); 255 | } 256 | }); 257 | return res; 258 | } 259 | 260 | public bindForeignParams( 261 | foreignQueryModel: QueryModelBase, 262 | constraintName: string, 263 | foreignObject: F, 264 | more: Object = {}, 265 | ): Object { 266 | const hostParams: Object = Object.assign({}, more); 267 | const fkProps = this.getForeignKeyProps(constraintName); 268 | const refCols = this.getForeignKeyRefCols(constraintName); 269 | 270 | /* istanbul ignore if */ 271 | if (!fkProps || !refCols || fkProps.length !== refCols.length) { 272 | throw new Error( 273 | `bind information for '${constraintName}' in table '${this.table.name}' is incomplete`, 274 | ); 275 | } 276 | 277 | const refNotFoundCols: string[] = []; 278 | const refProps = foreignQueryModel.getPropertiesFromColumnNames(refCols, refNotFoundCols); 279 | /* istanbul ignore if */ 280 | if (!refProps || refNotFoundCols.length) { 281 | const s = '"' + refNotFoundCols.join('", "') + '"'; 282 | throw new Error( 283 | `in '${foreignQueryModel.metaModel.name}': no property mapped to these fields: ${s}`, 284 | ); 285 | } 286 | 287 | for (let i = 0; i < fkProps.length; ++i) { 288 | const fkProp = fkProps[i]; 289 | const refProp = refProps[i]; 290 | this.setHostParamValue(hostParams, fkProp, refProp.getDBValueFromModel(foreignObject)); 291 | } 292 | return hostParams; 293 | } 294 | 295 | public bindAllInputParams(model: Partial, keys?: (keyof T)[], addIdentity?: boolean): Object { 296 | const hostParams: Object = {}; 297 | const props = this.getPropertiesFromKeys(keys, addIdentity); 298 | props.forEach((prop) => { 299 | this.setHostParam(hostParams, prop, model); 300 | }); 301 | return hostParams; 302 | } 303 | 304 | /* istanbul ignore next */ 305 | // obsolete 306 | public bindNonPrimaryKeyInputParams(model: Partial, keys?: (keyof T)[]): Object { 307 | const hostParams: Object = {}; 308 | const props = this.getPropertiesFromKeys(keys); 309 | props 310 | .filter((prop) => !prop.field.isIdentity) 311 | .forEach((prop) => { 312 | this.setHostParam(hostParams, prop, model); 313 | }); 314 | return hostParams; 315 | } 316 | 317 | public bindPrimaryKeyInputParams(model: Partial): Object { 318 | const hostParams: Object = {}; 319 | this.metaModel.qmCache.primaryKeyProps.forEach((prop: MetaProperty) => { 320 | this.setHostParam(hostParams, prop, model); 321 | }); 322 | return hostParams; 323 | } 324 | 325 | private buildCache(): QueryModelCache { 326 | /* istanbul ignore if */ 327 | if (!this.metaModel.properties.size) { 328 | throw new Error(`class '${this.metaModel.name}': does not have any mapped properties`); 329 | } 330 | 331 | // primary key predicates 332 | const props = Array.from(this.metaModel.properties.values()); 333 | const primaryKeyProps = props.filter((prop) => prop.field.isIdentity); 334 | const primaryKeyPredicates = primaryKeyProps.map( 335 | (prop) => `${prop.field.quotedName}=${prop.getHostParameterName()}`, 336 | ); 337 | 338 | // -------------------------------------------------------------- 339 | // generate SELECT-fk condition 340 | const foreignKeyPredicates = new Map(); 341 | const foreignKeyProps = new Map(); 342 | const foreignKeyRefCols = new Map(); 343 | 344 | this.table.mapNameToFKDef.forEach((fkDef, constraintName) => { 345 | const fkProps: MetaProperty[] = []; 346 | fkDef.fields.forEach((fkField) => { 347 | const prop = this.metaModel.mapColNameToProp.get(fkField.name); 348 | /* istanbul ignore else */ 349 | if (prop) { 350 | fkProps.push(prop); 351 | } 352 | }); 353 | /* istanbul ignore else */ 354 | if (fkProps.length === fkDef.fields.length) { 355 | const selectCondition = fkProps.map( 356 | (prop) => `${prop.field.quotedName}=${prop.getHostParameterName()}`, 357 | ); 358 | foreignKeyPredicates.set(constraintName, selectCondition); 359 | foreignKeyProps.set(constraintName, fkProps); 360 | foreignKeyRefCols.set( 361 | constraintName, 362 | fkDef.fields.map((field) => field.foreignColumnName), 363 | ); 364 | } 365 | }); 366 | return { 367 | primaryKeyProps, 368 | primaryKeyPredicates, 369 | foreignKeyPredicates, 370 | foreignKeyProps, 371 | foreignKeyRefCols, 372 | }; 373 | } 374 | } 375 | 376 | export interface QueryModelCache { 377 | primaryKeyProps: MetaProperty[]; 378 | primaryKeyPredicates: string[]; 379 | foreignKeyPredicates: Map; 380 | foreignKeyProps: Map; 381 | foreignKeyRefCols: Map; 382 | } 383 | -------------------------------------------------------------------------------- /src/sqlite3orm/metadata/Table.ts: -------------------------------------------------------------------------------- 1 | import * as core from '../core/core'; 2 | import { 3 | qualifiySchemaIdentifier, 4 | quoteAndUnqualifyIdentifier, 5 | quoteIdentifier, 6 | quoteSimpleIdentifier, 7 | splitSchemaIdentifier, 8 | } from '../utils'; 9 | 10 | import { FieldOpts } from './decorators'; 11 | import { Field } from './Field'; 12 | import { FKDefinition } from './FKDefinition'; 13 | import { IDXDefinition } from './IDXDefinition'; 14 | import { MetaModel } from './MetaModel'; 15 | import { PropertyType } from './PropertyType'; 16 | 17 | /** 18 | * Class holding a table definition (name of the table and fields in the table) 19 | * 20 | * @export 21 | * @class Table 22 | */ 23 | export class Table { 24 | get quotedName(): string { 25 | return quoteIdentifier(this.name); 26 | } 27 | 28 | get schemaName(): string | undefined { 29 | return splitSchemaIdentifier(this.name).identSchema; 30 | } 31 | 32 | /** 33 | * Flag to indicate if this table should be created with the 'WITHOUT 34 | * ROWID'-clause 35 | */ 36 | private _withoutRowId?: boolean; 37 | 38 | get withoutRowId(): boolean { 39 | return this._withoutRowId == undefined ? false : this._withoutRowId; 40 | } 41 | set withoutRowId(withoutRowId: boolean) { 42 | this._withoutRowId = withoutRowId; 43 | } 44 | get isWithoutRowIdDefined(): boolean { 45 | return this._withoutRowId == undefined ? false : true; 46 | } 47 | 48 | /** 49 | * Flag to indicate if AUTOINCREMENT should be enabled for a table having a 50 | * single-column INTEGER primary key 51 | * and withoutRowId is disabled 52 | */ 53 | private _autoIncrement?: boolean; 54 | 55 | get autoIncrement(): boolean { 56 | return this._autoIncrement == undefined ? false : this._autoIncrement; 57 | } 58 | set autoIncrement(autoIncrement: boolean) { 59 | this._autoIncrement = autoIncrement; 60 | } 61 | get isAutoIncrementDefined(): boolean { 62 | return this._autoIncrement == undefined ? false : true; 63 | } 64 | 65 | /** 66 | * The fields defined for this table 67 | */ 68 | readonly fields: Field[] = []; 69 | 70 | /** 71 | * The field mapped to the primary key; only set if using the 72 | * primary key column is alias for the rowId. 73 | */ 74 | private _rowIdField: Field | undefined; 75 | 76 | get rowIdField(): Field | undefined { 77 | return this._rowIdField; 78 | } 79 | 80 | /** 81 | * The field mapped to the primary key; only set if using the 82 | * AUTOINCREMENT feature 83 | */ 84 | private _autoIncrementField: Field | undefined; 85 | 86 | get autoIncrementField(): Field | undefined { 87 | return this._autoIncrementField; 88 | } 89 | 90 | // map column name to a field definition 91 | readonly mapNameToField: Map; 92 | 93 | // map column name to a identity field definition 94 | readonly mapNameToIdentityField: Map; 95 | 96 | // map constraint name to foreign key definition 97 | readonly mapNameToFKDef: Map; 98 | 99 | // map index name to index key definition 100 | readonly mapNameToIDXDef: Map; 101 | 102 | readonly models: Set; 103 | 104 | /** 105 | * Creates an instance of Table. 106 | * 107 | * @param name - The table name (containing the schema name if specified) 108 | */ 109 | public constructor(public readonly name: string) { 110 | this.mapNameToField = new Map(); 111 | this.mapNameToIdentityField = new Map(); 112 | this.mapNameToFKDef = new Map(); 113 | this.mapNameToIDXDef = new Map(); 114 | this.fields = []; 115 | this.models = new Set(); 116 | } 117 | 118 | /** 119 | * Test if table has a column with the given column name 120 | * 121 | * @param colName - The name of the column 122 | */ 123 | public hasTableField(name: string): Field | undefined { 124 | return this.mapNameToField.get(name); 125 | } 126 | 127 | /** 128 | * Get the field definition for the given column name 129 | * 130 | * @param colName - The name of the column 131 | * @returns The field definition 132 | */ 133 | public getTableField(name: string): Field { 134 | const field = this.mapNameToField.get(name) as Field; 135 | if (!field) { 136 | throw new Error(`table '${this.name}': field '${name}' not registered yet`); 137 | } 138 | return field; 139 | } 140 | 141 | /** 142 | * Add a table field to this table 143 | * 144 | * @param name - The name of the column 145 | * @param isIdentity 146 | * @param [opts] 147 | * @param [propertyType] 148 | * @returns The field definition 149 | */ 150 | public getOrAddTableField( 151 | name: string, 152 | isIdentity: boolean, 153 | opts?: FieldOpts, 154 | propertyType?: PropertyType, 155 | ): Field { 156 | let field = this.mapNameToField.get(name); 157 | if (!field) { 158 | // create field 159 | field = new Field(name, isIdentity, opts, propertyType); 160 | this.fields.push(field); 161 | this.mapNameToField.set(field.name, field); 162 | 163 | if (field.isIdentity) { 164 | this.mapNameToIdentityField.set(field.name, field); 165 | } 166 | } else { 167 | // merge field 168 | if (field.isIdentity !== isIdentity) { 169 | throw new Error( 170 | `conflicting identity setting: new: ${isIdentity}, old: ${field.isIdentity}`, 171 | ); 172 | } 173 | if (opts && opts.dbtype) { 174 | if (field.isDbTypeDefined && field.dbtype !== opts.dbtype) { 175 | throw new Error( 176 | `conflicting dbtype setting: new: '${opts.dbtype}', old: '${field.dbtype}'`, 177 | ); 178 | } 179 | field.dbtype = opts.dbtype; 180 | } 181 | if (opts && opts.isJson != undefined) { 182 | if (field.isIsJsonDefined && field.isJson !== opts.isJson) { 183 | throw new Error(`conflicting json setting: new: ${opts.isJson}, old: ${field.isJson}`); 184 | } 185 | field.isJson = opts.isJson; 186 | } 187 | if (opts && opts.dateInMilliSeconds != undefined) { 188 | if ( 189 | field.isDateInMilliSecondsDefined && 190 | field.dateInMilliSeconds !== opts.dateInMilliSeconds 191 | ) { 192 | throw new Error( 193 | `conflicting dateInMilliSeconds setting: new: ${opts.dateInMilliSeconds}, old: ${field.dateInMilliSeconds}`, 194 | ); 195 | } 196 | field.dateInMilliSeconds = opts.dateInMilliSeconds; 197 | } 198 | } 199 | if (field.isIdentity) { 200 | if ( 201 | !this.withoutRowId && 202 | this.mapNameToIdentityField.size === 1 && 203 | field.dbTypeInfo.typeAffinity === 'INTEGER' 204 | ) { 205 | this._rowIdField = field; 206 | if (this.autoIncrement) { 207 | this._autoIncrementField = field; 208 | } else { 209 | this._autoIncrementField = undefined; 210 | } 211 | } else { 212 | this._autoIncrementField = undefined; 213 | this._rowIdField = undefined; 214 | } 215 | } 216 | return field; 217 | } 218 | 219 | public hasFKDefinition(name: string): FKDefinition | undefined { 220 | return this.mapNameToFKDef.get(name); 221 | } 222 | 223 | public getFKDefinition(name: string): FKDefinition { 224 | const constraint = this.mapNameToFKDef.get(name); 225 | if (!constraint) { 226 | throw new Error(`table '${this.name}': foreign key constraint ${name} not registered yet`); 227 | } 228 | return constraint; 229 | } 230 | 231 | public addFKDefinition(fkDef: FKDefinition): FKDefinition { 232 | const oldFkDef = this.mapNameToFKDef.get(fkDef.name); 233 | if (!oldFkDef) { 234 | this.mapNameToFKDef.set(fkDef.name, fkDef); 235 | } else { 236 | // check conflicts 237 | if (oldFkDef.id !== fkDef.id) { 238 | core.debugORM( 239 | `table '${this.name}': conflicting foreign key definition for '${fkDef.name}'`, 240 | ); 241 | core.debugORM(` old: ${oldFkDef.id}`); 242 | core.debugORM(` new: ${fkDef.id}`); 243 | throw new Error( 244 | `table '${this.name}': conflicting foreign key definition for '${fkDef.name}'`, 245 | ); 246 | } 247 | } 248 | return fkDef; 249 | } 250 | 251 | public hasIDXDefinition(name: string): IDXDefinition | undefined { 252 | // NOTE: creating a index in schema1 on a table in schema2 is not supported by Sqlite3 253 | // so using qualifiedIndentifier is currently not required 254 | return this.mapNameToIDXDef.get(qualifiySchemaIdentifier(name, this.schemaName)); 255 | } 256 | 257 | public getIDXDefinition(name: string): IDXDefinition { 258 | // NOTE: creating a index in schema1 on a table in schema2 is not supported by Sqlite3 259 | // so using qualifiedIndentifier is currently not required 260 | const idxDef = this.mapNameToIDXDef.get(qualifiySchemaIdentifier(name, this.schemaName)); 261 | if (!idxDef) { 262 | throw new Error(`table '${this.name}': index ${name} not registered yet`); 263 | } 264 | return idxDef; 265 | } 266 | 267 | public addIDXDefinition(idxDef: IDXDefinition): IDXDefinition { 268 | // NOTE: creating a index in schema1 on a table in schema2 is not supported by Sqlite3 269 | // so using qualifiedIndentifier is currently not required 270 | const qname = qualifiySchemaIdentifier(idxDef.name, this.schemaName); 271 | const oldIdxDef = this.mapNameToIDXDef.get(qname); 272 | if (!oldIdxDef) { 273 | this.mapNameToIDXDef.set(qname, idxDef); 274 | } else { 275 | // check conflicts 276 | if (oldIdxDef.id !== idxDef.id) { 277 | core.debugORM(`table '${this.name}': conflicting index definition for '${idxDef.name}'`); 278 | core.debugORM(` old: ${oldIdxDef.id}`); 279 | core.debugORM(` new: ${idxDef.id}`); 280 | throw new Error(`table '${this.name}': conflicting index definition '${idxDef.name}'`); 281 | } 282 | } 283 | return idxDef; 284 | } 285 | 286 | /** 287 | * Get 'CREATE TABLE'-statement using 'IF NOT EXISTS'-clause 288 | * 289 | * @returns The sql-statement 290 | */ 291 | public getCreateTableStatement(force?: boolean): string { 292 | return this.createCreateTableStatement(force); 293 | } 294 | 295 | /** 296 | * Get 'DROP TABLE'-statement 297 | * 298 | * @returns {string} 299 | */ 300 | public getDropTableStatement(): string { 301 | return `DROP TABLE IF EXISTS ${this.quotedName}`; 302 | } 303 | 304 | /** 305 | * Get 'ALTER TABLE...ADD COLUMN'-statement for the given column 306 | * 307 | * @param colName - The name of the column to add to the table 308 | * @returns The sql-statment 309 | */ 310 | public getAlterTableAddColumnStatement(colName: string): string { 311 | let stmt = `ALTER TABLE ${this.quotedName}`; 312 | 313 | const field = this.getTableField(colName); 314 | stmt += ` ADD COLUMN ${field.quotedName} ${field.dbtype}`; 315 | return stmt; 316 | } 317 | 318 | /** 319 | * Get 'CREATE [UNIQUE] INDEX'-statement using 'IF NOT EXISTS'-clause 320 | * 321 | * @returns The sql-statement 322 | */ 323 | public getCreateIndexStatement(idxName: string, unique?: boolean): string { 324 | const idxDef = this.hasIDXDefinition(idxName); 325 | if (!idxDef) { 326 | throw new Error( 327 | `table '${this.name}': index '${idxName}' is not defined on table '${this.name}'`, 328 | ); 329 | } 330 | if (unique == undefined) { 331 | unique = idxDef.isUnique ? true : false; 332 | } 333 | 334 | const idxCols = idxDef.fields.map( 335 | (field) => quoteSimpleIdentifier(field.name) + (field.desc ? ' DESC' : ''), 336 | ); 337 | return ( 338 | 'CREATE ' + 339 | (unique ? 'UNIQUE ' : ' ') + 340 | `INDEX IF NOT EXISTS ${quoteIdentifier(idxName)} ON ${quoteAndUnqualifyIdentifier( 341 | this.name, 342 | )} ` + 343 | `(` + 344 | idxCols.join(', ') + 345 | ')' 346 | ); 347 | } 348 | 349 | /** 350 | * Get 'DROP TABLE'-statement 351 | * 352 | * @returns The sql-statement 353 | */ 354 | public getDropIndexStatement(idxName: string): string { 355 | const idxDef = this.hasIDXDefinition(idxName); 356 | if (!idxDef) { 357 | throw new Error( 358 | `table '${this.name}': index '${idxName}' is not defined on table '${this.name}'`, 359 | ); 360 | } 361 | return `DROP INDEX IF EXISTS ${quoteIdentifier(idxName)}`; 362 | } 363 | 364 | /** 365 | * Generate SQL Statements 366 | * 367 | */ 368 | public createCreateTableStatement(force?: boolean, addFields?: Field[]): string { 369 | const colNamesPK: string[] = []; 370 | const colDefs: string[] = []; 371 | 372 | const quotedTableName = this.quotedName; 373 | 374 | /* istanbul ignore if */ 375 | if (!this.fields.length) { 376 | throw new Error(`table '${this.name}': does not have any fields defined`); 377 | } 378 | 379 | this.fields.forEach((field) => { 380 | const quotedFieldName = field.quotedName; 381 | let colDef = `${quotedFieldName} ${field.dbtype}`; 382 | 383 | if (field.isIdentity) { 384 | colNamesPK.push(quotedFieldName); 385 | if (this.mapNameToIdentityField.size === 1) { 386 | colDef += ' PRIMARY KEY'; 387 | if (this.autoIncrementField) { 388 | colDef += ' AUTOINCREMENT'; 389 | } 390 | } 391 | } 392 | colDefs.push(colDef); 393 | }); 394 | if (addFields) { 395 | addFields.forEach((field) => { 396 | const quotedFieldName = field.quotedName; 397 | colDefs.push(`${quotedFieldName} ${field.dbtype}`); 398 | }); 399 | } 400 | // -------------------------------------------------------------- 401 | // generate CREATE TABLE statement 402 | let stmt = 'CREATE TABLE '; 403 | if (!force) { 404 | stmt += 'IF NOT EXISTS '; 405 | } 406 | stmt += `${quotedTableName} (\n `; 407 | 408 | // add column definitions 409 | stmt += colDefs.join(',\n '); 410 | if (this.mapNameToIdentityField.size > 1) { 411 | // add multi-column primary key ćonstraint: 412 | stmt += ',\n CONSTRAINT PRIMARY_KEY PRIMARY KEY ('; 413 | stmt += colNamesPK.join(', '); 414 | stmt += ')'; 415 | } 416 | 417 | // add foreign key constraint definition: 418 | this.mapNameToFKDef.forEach((fk, fkName) => { 419 | /* istanbul ignore if */ 420 | if (!fk.fields.length || fk.fields.length !== fk.fields.length) { 421 | throw new Error( 422 | `table '${this.name}': foreign key constraint '${fkName}' definition is incomplete`, 423 | ); 424 | } 425 | stmt += `,\n CONSTRAINT ${quoteSimpleIdentifier(fk.name)}\n`; 426 | stmt += ` FOREIGN KEY (`; 427 | stmt += fk.fields.map((field) => quoteSimpleIdentifier(field.name)).join(', '); 428 | stmt += ')\n'; 429 | 430 | // if fk.foreignTableName has qualifier it must match the qualifier of this.name 431 | const { identName, identSchema } = splitSchemaIdentifier(fk.foreignTableName); 432 | 433 | const tableSchema = this.schemaName; 434 | /* istanbul ignore next */ 435 | if ( 436 | identSchema && 437 | ((identSchema === 'main' && tableSchema && tableSchema !== identSchema) || 438 | (identSchema !== 'main' && (!tableSchema || tableSchema !== identSchema))) 439 | ) { 440 | throw new Error( 441 | `table '${this.name}': foreign key '${fkName}' references table in wrong schema: '${fk.foreignTableName}'`, 442 | ); 443 | } 444 | 445 | stmt += ` REFERENCES ${quoteSimpleIdentifier(identName)} (`; 446 | stmt += 447 | fk.fields.map((field) => quoteSimpleIdentifier(field.foreignColumnName)).join(', ') + 448 | ') ON DELETE CASCADE'; // TODO: hard-coded 'ON DELETE CASCADE' 449 | stmt += '\n'; 450 | }); 451 | stmt += '\n)'; 452 | if (this.withoutRowId) { 453 | stmt += ' WITHOUT ROWID'; 454 | } 455 | return stmt; 456 | } 457 | } 458 | --------------------------------------------------------------------------------