Code Monkey home page Code Monkey logo

node-sqlite3-orm's Introduction

npm version Build Status Coverage Status DeepScan Grade Dependency Status Known Vulnerabilities

PRs Welcome

node-sqlite3-orm

This module allows you to map your model, written in JavaScript or TypeScript, to a database schema using SQLite Version 3. node-sqlite3-orm is designed to work with new JavaScript Decorators, Promises and the async/await feature. It also supports typesafe database queries and refactoring, with a filter syntax designed to serialize safely without any SQL injection possibility

NOTE: Your contribution is highly welcome! Feel free to pick-up a TODO-item or add yours.

Introduction

node-sqlite3-orm provides you with the ability to create the database schema for the mapped model and to store and retrieve the mapped data to and from the database,

import {table, id, field, index, fk, FieldOpts, TableOpts} from 'sqlite3orm';

@table({name: 'USERS'})
class User {
  @id({name: 'user_id', dbtype: 'INTEGER NOT NULL'})
  userId: number;

  @field({name: 'user_loginname', dbtype: 'TEXT NOT NULL'})
  userLoginName: string;

  @field({name: 'user_json', dbtype: 'TEXT', isJson: true})
  userJsonData: any;

  @field({name: 'user_deleted'})
  deleted?: boolean;

}

@table({name: 'CONTACTS', autoIncrement: true})
class Contact {
  @id({name: 'contact_id', dbtype: 'INTEGER NOT NULL'})
  contactId: number;

  @field({name: 'contact_email', dbtype: 'TEXT'})
  emailAddress: string;

  @field({name: 'contact_mobile', dbtype: 'TEXT'})
  mobile: string;

  @field({name: 'user_id', dbtype: 'INTEGER NOT NULL'})
  @fk('fk_user_contacts', 'USERS', 'user_id')
  @index('idx_contacts_user')
  userId: number;
}

With node-sqlite3-orm you have full control over the names for tables, fields, indexes and foreign key constraints in the mapped database schema.

NOTE: Properties without a node-sqlite3-orm decorator will not be mapped to the database.

NOTE: you can use the 'temp' qualifier to create a temporary table. e.g @table({name: 'temp.MYTEMPTABLE'

NOTE: you can map the same table to different model classes, e.g for using a partial model class

Database Connection

import {SqlDatabase} from 'sqlite3orm';

(async () => {
  let sqldb = new SqlDatabase();
  await sqldb.open(':memory:');
})();

SqlDatabase is a thin promised-based wrapper around sqlite3.Database: node-sqlite3

Schema Creation

import {schema} from 'sqlite3orm';

(async() => {
  // get the user_version from the database:
  let userVersion = await sqldb.getUserVersion();

  // create all the tables if they do not exist:
  await schema().createTable(sqldb, 'USERS');
  await schema().createTable(sqldb, 'CONTACTS');
  await schema().createIndex(sqldb, 'CONTACTS', 'idx_contacts_user');

  if (userVersion >= 1 && userVersion < 10) {
    // the 'CONTACTS' table has been introduced in user_version 1
    // a column 'contact_mobile' has been added to the 'CONTACTS'
    // table in user_version 10
    await schema().alterTableAddColumn(
        sqldb, 'CONTACTS', 'contact_mobile');
  }
  await sqldb.setUserVersion(10);
})();

Select/Insert/Update/Delete using DAOs

In order to read from or write to the database, you can use the `BaseDAO' class

(async () => {

  let userDAO = new BaseDAO(User, sqldb);
  let contactDAO = new BaseDAO(Contact, sqldb);

  // insert a user:
  let user = new User();
  user.userId = 1;
  user.userLoginName = 'donald';
  user.userJsonData = { lastScores: [10, 42, 31]};
  user = await userDAO.insert(user);

  // insert a contact:
  let contact = new Contact();
  contact.userId = 1;
  contact.emailAddress = '[email protected]';
  contact = await contactDAO.insert(contact);

  // update a contact:
  contact.mobile = '+49 123 456';
  contact = await contactDAO.update(contact);

  // read a user:
  let userDonald = await userDAO.select(user);

  // update a user partially:
  await userDAO.updatePartial({userId: userDonald.userId, deleted: true});

  // read all contacts (child) for a given user (parent):
  let contactsDonald1 = await contactDAO.selectAllOf('fk_user_contacts', User, userDonald);
  //   or
  let contactsDonald2 = await userDAO.selectAllChildsOf('fk_user_contacts', Contact, userDonald);

  // read all users:
  let allUsers = await userDAO.selectAll();

  // read all users having login-name starting with 'd':
  // (see section 'typesafe queries')
  let selectedUsers = await userDAO.selectAll({userLoginName: {isLike: 'd%'});

  // read all users having a contact:
  let allUsersHavingContacts = await userDAO.selectAll(
      'WHERE EXISTS(SELECT 1 FROM CONTACTS C WHERE C.user_id = T.user_id)');

  // read all contacts from 'duck.com':
  let allContactsFromDuckDotCom = await contactDAO.selectAll(
      'WHERE contact_email like $contact_email',
      {$contact_email: '%@duck.com'});

  // read user (parent) for a given contact (child)
  let userDonald1 = await userDAO.selectByChild('fk_user_contacts', Contact, contactsDonald1[0]);
  // or
  let userDonald2 = await contactDAO.selectParentOf('fk_user_contacts', User, contactsDonald2[0]);

})();

Typesafe query syntax

Filter

interface Filter<ModelType> {
  select?: Columns<ModelType>;      // the columns which should be returned by the select
  where?: Where<ModelType>;         // the conditions for the WHERE-clause
  order?: OrderColumns<ModelType>;  // the columns to use for 'ORDER BY'-clause
  limit?: number;                   // the limit for the 'LIMIT'-clause
  offset?: number;                  // the offset for the 'LIMIT'-clause
  tableAlias?: string;              // a table alias to use for the query
}

select-object

Only columns mapped to properties that evaluate to true participate in the result set. Therefore, this select-object is only useful for methods that return an array of partials and is otherwise ignored

const filter = {
  select: {userId: true, userLoginName: true}
}

where-object

The where-object consists of predicates and may be grouped by (nested conditions: 'and', 'or', 'not')

A simple predicate is defined for a specific property of the model, the comparison operator and the value:

{userLoginName: {eq: 'donald'}}   // transforms to: WHERE user_loginname = 'donald'

For the 'eq' operator a shorthand form exist:

{userLoginName: 'donald'}         // transforms to: WHERE user_loginname = 'donald'

All the given values are not inserted directly into the SQL, but passed via parameters: eg: 'WHERE user_loginname = :userLoginName'

We can define multiple predicates on one property as well as on multiple properties:

{
  userLoginName: {gte: 'A', lt: 'B' },
  userJsonData: {isNotNull: true}
}   // transforms to: WHERE user_loginname >= 'A' AND user_loginname < 'B' AND user_json IS NOT NULL

All predicates are combined using logical 'AND' operator, so if we have the need for a logical 'OR' we would do something like that:

{
  or: [{deleted: true}, {deleted: {isNull: true}}]
}   // transforms to: WHERE user_deleted = 1 OR user_deleted IS NULL

'and' and 'or' operators are expecing an array, the 'not' operator requires a single child-condition/predicates only

{
  not: {
    or: [{deleted: true}, {deleted: {isNull: true}}]
  }
}   // transforms to: WHERE NOT ( user_deleted = 1 OR user_deleted IS NULL )

Furthermore, it is also possible to define parts of the query as sql expression, or replace the complete where-object with a sql where-clause:

{
  and: [{deleted: true}, {sql: `
EXISTS (select 1 from CONTACTS C where C.user_id = T.user_id)
  `}]
}   // transforms to: WHERE NOT ( user_deleted = 1 OR user_deleted IS NULL )

NOTE: If you want to use user input as part of the sql expression, it is highly recommendet to use host variables instead. The value for the host variables can be defined using an additional and optional 'params' object

additional filter properties and other things worth mentioning

limit and offset speak for themselves. By defining the tableAlias property, the default alias 'T' for the main table (see above) can be overwritten

For all 'update*' and 'delete*' methods, only the where-object part is needed, not the complete filter definition:

userDAO.deleteAll({deleted: true});

Supported data types

All primitive JavaScript data types ('String', 'Number', 'Boolean') and properties of type 'Date' are supported. Type safety is guaranteed, when reading properties of these types from the database (NULL values are treated as 'undefined').

Date properties can be mapped to either the 'TEXT' or to the 'INTEGER' storage class (defaults to 'INTEGER') and their values will be stored as UTC. Using 'INTEGER' converts to Unix-Time, so fractions of seconds are lost. This can be changed by using the field option 'dateInMilliSeconds' or by setting as default using schema().dateInMilliSeconds = true.

These are the corresponding defaults for a 'current timestamp':

default for 'TEXT':

 dbtype: 'TEXT    DEFAULT(datetime(\'now\') || \'Z\')'

default for 'INTEGER' (in seconds):

 dbtype: 'INTEGER DEFAULT(CAST(strftime(\'%s\',\'now\') as INT))' // unix epoch in seconds

Boolean properties can either be mapped to 'TEXT' or to 'INTEGER' (default). On storing a boolean value false will be converted to '0' and true will be converted to '1', on reading '0' or 'false' will be converted to false and '1' or 'true' will be converted to true. All other values will result in undefined

Other data types can be serialized to a database field of type TEXT in JSON format, by setting the option 'isJson' to true (see sample above).

additional you have the possibility to apply your own serialze/deserialize functions by setting the 'transform' option.

Connection pool

NOTE: For each database transaction, the involved database connection (SqlDatabase instance) should be used exclusively!

One possibility to achieve this could be to use a connection pool and to perform all database transactions with their own database connection.

NOTE: instances of BaseDAO are lightweight objects and can be created on the fly and exclusively for one database transaction

(async () => {
  let pool = new SqlConnectionPool();

  // open the database connection pool with 1 to 2 database connections:
  await pool.open('/path/to/mydata.db', SQL_OPEN_DEFAULT, 1, 2);

  let con1 = await pool.get();
  let con2 = await pool.get();
  await Promise.all([doSomeThing(con1), doAnotherThing(con2)]);

  // free all connections to the pool:
  con1.close();
  pool.release(con2);

})();

Autoupgrade

automatically create or upgrade tables and indexes in the database based on your table definitions

const autoUpgrader = new AutoUpgrader(sqldb);

// run autoupgrade for all registered tables:
autoUpgrader.upgradeAllTables();

// test if table definitions are up-to-date
autoUpgrader.isActual([userDAO.table, contactDAO.table]);

// run autoupgrade for specific table(s):
autoUpgrader.upgradeTables([userDAO.table]);

NOTE: autoupgrade should be carefully tested before running it on a production database! A backup should be self-evident!

NOTE: renaming of columns can not be detected! autoupgrade would normally add a new column with the new name and the data in the old column would be lost, but there is an option 'keepOldColumns' for preventing old columns from beeing dropped. Recycling the old column name for other purpose is asking for trouble

NOTE: changing autoIncrement cannot be detected! You can use the optional parameter force to force a recreation

NOTE: if you have changed the column type, the table definition will be updated accordingly, but the content of the column will be still the same. You need an additional action if you want to convert the content of the column

NOTE: please always add a DEFAULT-clause for newly added columns which are not nullable

Install

npm install sqlite3orm

When using TypeScript, the compiler options experimentalDecorators and emitDecoratorMetadata must be enabled.

tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    ...
  },
  ...
}

NOTE: SQLite's SQLCipher extension is also supported, see SQLCipher

NOTE: for custom builds and Electron, see Custom builds and Electron

Release Notes

CHANGELOG

License

node-sqlite3-orm is licensed under the MIT License: LICENSE

Wiki

further documentation can be found in our Wiki

node-sqlite3-orm's People

Contributors

gms1 avatar greenkeeper[bot] avatar peterennis avatar greenkeeperio-bot avatar dependabot[bot] avatar

Watchers

James Cloos avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.