lib | ||
src | ||
.gitignore | ||
package-lock.json | ||
package.json | ||
README.md | ||
webpack.config.js |
PawSQLite
PawSQLite is a library for interacting with SQLite databases. Through the use of multiple adapters for various environments, it provides a simple API that abstracts out implementation specific quirks.
Installation
You can install using npm:
npm install -s git+https://git.n0m.org/n0m/PawSQLite.git
In order to actually use PawSQLite, you must also install an adapter for your specific environment. Currently, adapters are available for the following environments:
WebSQL:
PawSQLite-WebSQL-Adapter: https://git.n0m.org/n0m/PawSQLite-WebSQL-Adapter
Apache Cordova:
PawSQLite-Cordova-Adapter: https://git.n0m.org/n0m/PawSQLite-Cordova-Adapter
Node.js via Sqlite3 package:
PawSQLite-Node-Adapter: https://git.n0m.org/n0m/PawSQLite-Node-Adapter
Usage
Registering an adapter:
You must register at least one adapter before you can use PawSQLite
const PawSQLite = require("pawsqlite");
const PawSQLiteNodeAdapter = require("pawsqlite-node-adapter");
PawSQLite.registerAdapter(PawSQLiteNodeAdapter)
Opening a database:
PawSQLite.open
accepts two arguments. The first argument is the name of the database. This name is handled differently depending on the adapter in use, but in general it corresponds to the full filename of the database. The second argument is an object for various configuration options.
const db = await PawSQLite.open("test", {
adapter: 'PawSQLiteNodeAdapter'
});
Querying the database:
You can query the database by using the sql
method on a transaction object. For convenience, database objects also contain an sql
method which is shorthand for db.autoTransaction((tx) => tx.sql(sql, ...args));
. The sql
method also allows you to bind parameters to the satatement. Bound parameters are escaped by whichever native SQLite implementation your chosen adapter uses.
await db.sql("SELECT * FROM contacts WHERE name=?", "Paul");
The above code will return an array of objects representing the matched rows in the contacts table. It might look like the following:
[
{ name: 'Paul', number: '+1 333 333 3333' },
{ name: 'Paul', number: '+1 444 444 4444' },
{ name: 'Paul', number: '+1 555 555 5555' }
]
PawSQLite also allows you to bind multiple parameters according to the number of parameters in an array using ???
. Although this might reduce readability.
await db.sql("INSERT INTO contacts VALUES (???)", ["Pat", 23])
The result of successful INSERT
statements will also include an insertId
property containing the id of the last inserted row. The result of UPDATE
and DELETE
statements will include a rowsAffected
property containing a count of the number of rows that were affected.
Transactions:
Transactions can be handled manually, or automatically. To use transactions manually, first create a transaction by calling the transaction method on your database. The actual transaction won't be started until the first SQL statement is executed on it.
const tx = db.transaction();
const result = await tx.sql("SELECT 1");
await tx.sql("INSERT INTO contacts (name) VALUES (?);", "Bob");
await tx.commit();
Only one transaction can be active at any one time. PawSQLite will automatically queue additional transactions until the active transaction is complete.
const tx1 = db.transaction();
const tx2 = db.transaction();
tx1.sql(`SELECT "tx1 - first select"`);
tx2.sql(`SELECT "tx2 - first select"`);
tx1.sql(`SELECT "tx1 - second select"`);
tx2.sql(`SELECT "tx2 - second select"`);
await tx1.commit();
await tx2.commit();
With debugging enabled, console output will show that the above statements were correctly sequenced and tx2
was forced to wait until tx1
had completed:
BEGIN
SELECT "tx1 - first select"
SELECT "tx1 - second select"
COMMIT
BEGIN
SELECT "tx2 - first select"
SELECT "tx2 - second select"
COMMIT
Transactions can also be handled automatically. autoTransaction
accepts two arguments, the first is a callback which will receive the transaction object. The callback is expected to return a promise whose completion signifies the end any work that needed to be wrapped in the transaction. The second argument is optional and can contain another transaction object to use instead of starting a new transaction. This functionality is very useful for chaining database operations, and will be demonstrated below.
await db.autoTransaction(async (tx) => {
await tx.sql(`SELECT "Some Value"`);
await someAsyncOperation();
await tx.sql(`INSERT INTO contacts (name) VALUES (?)`, "Ringo Starr");
});
Recommended Approach to Model Classes
Supposing we have two tables in our database. The first table contacts
contains rows of contact names. The second table numbers
contains rows of numbers and foreign keys for contact ids. We could model that database like this:
class NumberModel {
constructor(db) {
this.db = db;
this.id = null;
this.contactId = null;
this.number = null;
}
load(numberId, inheritTx) {
return this.db.autoTransaction(async (tx) => {
const result = await tx.sql(
"SELECT (number) FROM numbers WHERE id=?",
numberId
);
if (!result.length) {
throw new Error(`Unknown number: ${ numberId }`);
}
this.id = numberId;
this.contactId = result[0].contactId;
this.number = result[0].number;
}, inheritTx);
}
save(inheritTx) {
return this.db.autoTransaction(async (tx) => {
const isInsert = !this.id;
if (isInsert) {
const result = await tx.sql(
"INSERT INTO numbers (contact_id, number) VALUES (?, ?)",
this.contactId,
this.number
);
this.id = result.insertId;
} else {
await tx.sql(
"UPDATE numbers SET contact_id=? number=? WHERE id=?",
this.contactId,
this.number,
this.id
);
}
}, inheritTx);
}
}
class ContactModel {
constructor(db) {
this.db = db;
this.id = null;
this.name = null;
this.numbers = [];
}
load(contactId, inheritTx) {
return this.db.autoTransaction(async (tx) => {
const result = await tx.sql(
"SELECT (name) FROM contacts WHERE id=?",
contactId
);
if (!result.length) {
throw new Error(`Unknown contact: ${ contactId }`);
}
this.id = contactId;
this.name = result[0].name;
const numbers = await tx.sql(
"SELECT (id) FROM numbers WHERE contact_id=?",
this.id
);
this.numbers = await Promise.all(
numbers.map(async (number) => {
const numberModel = new NumberModel(this.db);
await numberModel.load(number.id, tx);
return numberModel;
})
);
}, inheritTx);
}
save(inheritTx) {
return this.db.autoTransaction(async (tx) => {
const isInsert = !this.id;
if (isInsert) {
const result = await tx.sql(
"INSERT INTO contacts (name) VALUES (?)",
this.name
);
this.id = result.insertId;
} else {
await tx.sql(
"UPDATE contacts SET name=? WHERE id=?",
this.name,
this.id
);
}
// Set contactId and save all numbers
await Promise.all(
this.numbers.map((numberModel) => {
numberModel.contactId = this.id;
return numberModel.save(tx);
})
);
}, inheritTx);
}
}
You can then create a contact like this:
const c1 = new ContactModel(db);
c1.name = "Bob Dylan";
const n1 = new NumberModel(db);
n1.number = "+1 555 555 5555";
c1.numbers.push(n1);
const n2 = new NumberModel(db);
n2.number = "+1 999 999 9999";
c1.numbers.push(n2);
await c1.save();
You can retrieve a contact like this:
const c2 = new ContactModel(db);
await c2.load(contactId);
Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.