dynamic-record
Version:
A bare minimum Javascript implementation of the Active Record pattern
314 lines (313 loc) • 14.5 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DynamicRecord = void 0;
const _ = require("lodash");
const DynamicRecord_1 = require("../DynamicRecord");
const DynamicCollection_1 = require("./DynamicCollection");
const DynamicSchema_1 = require("./DynamicSchema");
const schemaValidation_1 = require("./schemaValidation");
class DynamicRecord extends DynamicRecord_1.DynamicRecord {
constructor(options) {
super(options);
let _db, _client;
const connect = this._databaseConnection = options.connection;
const _schemaValidator = this._schemaValidator = (0, schemaValidation_1.default)(this._databaseConnection);
const tableSlug = options.tableSlug;
const _schema = this.schema = connect.interface.then(({ db }) => {
return new DynamicSchema_1.DynamicSchema({ connection: this._databaseConnection });
}).then((schema) => {
return schema.read(tableSlug);
}).then((schema) => {
if (schema.tableSlug === null)
return Promise.reject(`Table with name ${tableSlug} does not exist`);
return schema;
});
// Initialize database connection and populate schema instance
// Collection must already exist in database
const _ready = this._ready = connect.interface.then(({ db, client }) => {
_db = this._db = db;
_client = this._client = client;
return this.schema;
}).then((schema) => {
const col = _db.collection(tableSlug);
if (col) {
return Promise.resolve(col);
}
else {
return Promise.reject(`Table with name ${tableSlug} does not exist`);
}
});
const Model = this.Model = class Model extends DynamicRecord_1.Model {
constructor(data, _preserveOriginal) {
let id = null;
// Preserve mongodb _id if exist for faster saving
if (_.has(data, "_id")) {
id = data._id;
delete data._id;
}
super(data, _preserveOriginal);
this._savePromise = null;
this._id = id;
}
save() {
return __awaiter(this, void 0, void 0, function* () {
yield _ready;
const saveData = () => __awaiter(this, void 0, void 0, function* () {
const col = yield _ready;
if (this._id) {
// Update existing entry
yield validateData(this.data);
yield col.updateOne({ _id: this._id }, { $set: this.data });
this._original = _.cloneDeep(this.data);
return this;
}
else {
// Create new entry
// Check if collection contains index that needs auto incrementing
return _db.collection("_counters").findOne({ _$id: tableSlug }).then((res) => __awaiter(this, void 0, void 0, function* () {
const promises = [];
if (res !== null) {
// Auto incrementing index exist
_.each(res.sequences, (el, columnLabel) => {
promises.push(_schema.then((schema) => {
return schema._incrementCounter(tableSlug, columnLabel).then((newSequence) => {
this.data[columnLabel] = newSequence;
return Promise.resolve(newSequence);
});
}));
});
yield Promise.all(promises);
}
yield validateData(this.data);
// Save data into the database
yield col.insertOne(this.data);
if (_.has(this.data, "_id")) {
this._id = this.data._id;
delete this.data._id;
}
this._original = _.cloneDeep(this.data);
return this;
})).catch((err) => __awaiter(this, void 0, void 0, function* () {
// Reverse database actions
try {
yield Promise.all([
// 1. Decrement autoincrement counter
_db.collection("_counters").findOne({ _$id: tableSlug }).then((res) => {
const promises = [];
if (res) {
_.each(res.sequences, (el, columnLabel) => {
promises.push(_schema.then((schema) => {
return schema._decrementCounter(tableSlug, columnLabel);
}));
});
}
return Promise.all(promises);
})
]);
return Promise.reject(err);
}
catch (e) {
return Promise.reject(e);
}
}));
}
});
// Wait for current queue of saves before continuing
if (this._savePromise) {
yield this._savePromise;
this._savePromise = saveData();
}
else {
this._savePromise = saveData();
}
return this._savePromise;
function validateData(data) {
return __awaiter(this, void 0, void 0, function* () {
const schema = yield _schema;
const validate = yield _schemaValidator.compileAsync({ $ref: schema.tableSlug });
if (validate(data)) {
return Promise.resolve();
}
else {
return Promise.reject(new Error(JSON.stringify(validate.errors, null, 2)));
}
});
}
});
}
destroy() {
return __awaiter(this, void 0, void 0, function* () {
const col = yield _ready;
const destroyData = () => __awaiter(this, void 0, void 0, function* () {
if (this._original) {
yield col.deleteOne(this._original);
this._original = null;
this.data = null;
return this;
}
else {
throw new Error("Model not saved in database yet.");
}
});
// Wait for current queue of saves before continuing
if (this._savePromise) {
yield this._savePromise;
this._savePromise = destroyData();
}
else {
this._savePromise = destroyData();
}
return this._savePromise;
});
}
validate(schema) {
let result = false;
_.each(this.data, (el, key) => {
const field = _.find(schema, (column) => {
return column.label == key;
});
if (field.type == "string") {
result = _.isString(el);
}
else if (field.type == "int") {
result = Number.isInteger(el);
}
});
return result;
}
};
}
closeConnection() {
return __awaiter(this, void 0, void 0, function* () {
// Should only ever be called to terminate the node process
try {
yield this._ready;
this._client.close();
}
catch (e) {
// BY ANY MEANS NECESSARY
this._client.close();
}
});
}
findBy(query) {
return __awaiter(this, void 0, void 0, function* () {
const col = yield this._ready;
const model = yield col.findOne(query);
if (model !== null) {
return new this.Model(model, true);
}
else {
return null;
}
});
}
where(query, options) {
return __awaiter(this, void 0, void 0, function* () {
const col = yield this._ready;
let models = yield col.find(query)
.limit((options === null || options === void 0 ? void 0 : options.limit) || 0)
.skip((options === null || options === void 0 ? void 0 : options.offset) || 0)
.sort(_.mapValues(options === null || options === void 0 ? void 0 : options.sort, (val) => {
if (val === "ASC") {
return 1;
}
else if (val === "DESC") {
return -1;
}
else {
return 0;
}
}))
.toArray();
const results = new DynamicCollection_1.DynamicCollection(this.Model, ...models);
results.forEach((result) => {
result._original = _.cloneDeep(result.data);
});
return results;
});
}
all() {
return __awaiter(this, void 0, void 0, function* () {
const col = yield this._ready;
let models = yield col.find().toArray();
const results = new DynamicCollection_1.DynamicCollection(this.Model, ...models);
results.forEach((result) => {
result._original = _.cloneDeep(result.data);
});
return results;
});
}
first(options) {
return __awaiter(this, void 0, void 0, function* () {
const col = yield this._ready;
const models = yield col.find({})
.limit((options === null || options === void 0 ? void 0 : options.limit) || 1)
.skip((options === null || options === void 0 ? void 0 : options.offset) || 0)
.sort(_.assign(_.mapValues(options === null || options === void 0 ? void 0 : options.sort, (val) => {
if (val === "ASC") {
return 1;
}
else if (val === "DESC") {
return -1;
}
else {
return 0;
}
}), {
_id: 1
}))
.toArray();
if (models.length === 0 && _.isUndefined(options === null || options === void 0 ? void 0 : options.limit)) {
return null;
}
else if (models.length === 1 && _.isUndefined(options === null || options === void 0 ? void 0 : options.limit)) {
return new this.Model(models[0], true);
}
else {
return new DynamicCollection_1.DynamicCollection(this.Model, ...models);
}
});
}
last(options) {
return __awaiter(this, void 0, void 0, function* () {
const col = yield this._ready;
const models = yield col.find({})
.limit((options === null || options === void 0 ? void 0 : options.limit) || 1)
.skip((options === null || options === void 0 ? void 0 : options.offset) || 0)
.sort(_.assign(_.mapValues(options === null || options === void 0 ? void 0 : options.sort, (val) => {
if (val === "ASC") {
return 1;
}
else if (val === "DESC") {
return -1;
}
else {
return 0;
}
}), {
_id: -1
}))
.toArray();
if (models.length === 0 && _.isUndefined(options === null || options === void 0 ? void 0 : options.limit)) {
return null;
}
else if (models.length === 1 && _.isUndefined(options === null || options === void 0 ? void 0 : options.limit)) {
return new this.Model(models[0], true);
}
else {
return new DynamicCollection_1.DynamicCollection(this.Model, ...models);
}
});
}
}
exports.DynamicRecord = DynamicRecord;