@cloud-cli/store
Version:
Data store abstraction
158 lines (157 loc) • 6.13 kB
JavaScript
import SQLite from 'better-sqlite3';
import { Resource, ResourceDriver } from './resource.js';
import { Logger } from './logger.js';
export class SQLiteDriver extends ResourceDriver {
/* istanbul ignore next */
constructor(path = process.env.SQLITE_DB_PATH) {
super();
this.db = new SQLite(path);
this.db.pragma('journal_mode = WAL');
}
static parse(type, value) {
if (type === Number) {
return Number(value);
}
if (type === Boolean) {
return Number(value) === 1;
}
if (type === Object) {
return JSON.parse(String(value));
}
return String(value);
}
static serialize(type, value) {
if (type === Number) {
return Number(value);
}
if (type === Object) {
return JSON.stringify(value);
}
if (type === Boolean) {
return Number(value) === 1 ? 1 : 0;
}
return String(value);
}
async save(model) {
const desc = Resource.describe(model);
const fields = desc.fields.filter(field => !field.primary);
const columns = fields.map(f => f.name);
const row = fields.map(field => {
return SQLiteDriver.serialize(field.type, model[field.name] || field.defaultValue || '');
});
const primary = desc.fields.find(field => field.primary);
const isUpdate = model[primary.name] !== undefined;
if (isUpdate) {
columns.unshift(primary.name);
row.unshift(model[primary.name]);
}
const values = Array(columns.length).fill('?').join(',');
return new Promise((resolve, reject) => {
const sql = `REPLACE INTO ${desc.name} (${columns}) VALUES (${values})`;
Logger.debug(sql, row);
try {
const statement = this.db.prepare(sql);
const { lastInsertRowid } = statement.run(row);
resolve(String(lastInsertRowid));
}
catch (error) {
reject(new Error('Cannot store item: ' + error.message));
}
});
}
async remove(model) {
const resource = Object.getPrototypeOf(model).constructor;
const desc = Resource.describe(resource);
return new Promise((resolve, reject) => {
const primary = desc.fields.find(field => field.primary);
const query = `DELETE FROM ${desc.name} WHERE ${primary.name} = ?`;
Logger.debug(query, model[primary.name]);
try {
const statement = this.db.prepare(query);
statement.run([model[primary.name]]);
resolve();
}
catch (error) {
reject(new Error('Unable to remove: ' + error.message));
}
});
}
async find(model) {
const Model = Object.getPrototypeOf(model).constructor;
const desc = Resource.describe(Model);
const columns = desc.fields.map(f => f.name);
const primary = desc.fields.find(field => field.primary);
const id = model[primary.name];
const query = `SELECT ${columns} FROM ${desc.name} WHERE ${primary.name} = ? LIMIT 1`;
Logger.debug(query, [id]);
return new Promise((resolve, reject) => {
try {
const statement = this.db.prepare(query);
const result = statement.get([id]);
if (!result) {
reject(new Error('Not found'));
return;
}
resolve(this.createModel(Model, result));
}
catch (error) {
reject(error);
}
});
}
async findAll(resource, query) {
const desc = Resource.describe(resource);
const conditions = query.toJSON();
const where = conditions.map(([field, operator]) => `${String(field)} ${operator} ?`);
const args = conditions.map(c => c[2]);
const queryStr = `SELECT * FROM ${desc.name}${where.length ? ' WHERE ' + where.join(' AND ') : ''}`;
Logger.debug(queryStr, args);
return new Promise((resolve, reject) => {
try {
const statement = this.db.prepare(queryStr);
const value = statement.all(args);
resolve(value.map(data => this.createModel(resource, data)));
}
catch (error) {
reject(error);
}
});
}
createModel(model, data) {
const desc = Resource.describe(model);
const modelData = {};
desc.fields.forEach(field => {
modelData[field.name] = SQLiteDriver.parse(field.type, data[field.name] || field.defaultValue || '');
});
return new model(modelData);
}
async create(resource) {
const desc = Resource.describe(resource);
const { name } = desc;
const fields = desc.fields;
const names = fields.map(f => `${f.name} ${f.type === Number || f.type === Boolean ? 'INTEGER' : 'TEXT'}` + (f.notNull && ' NOT NULL' || ''));
const unique = fields.filter((field) => field.unique).map(f => f.name);
const primary = fields.filter(field => field.primary && field.type === Number);
return new Promise((resolve, reject) => {
if (!primary.length) {
return reject(new Error('Invalid primary key. It has to be of type Number'));
}
const sql = [
`CREATE TABLE IF NOT EXISTS ${name} (`,
names.join(', '),
(unique.length ? ', UNIQUE(' + unique.join(',') + ')' : ''),
', PRIMARY KEY(' + primary[0].name + ')',
')'
].join('');
Logger.debug(sql);
try {
const s = this.db.prepare(sql);
s.run([]);
resolve(undefined);
}
catch (error) {
return reject(new Error(`Cannot create table "${name}"`));
}
});
}
}