@nu-art/db-api-generator
Version:
db-api-generator
247 lines (246 loc) • 12.2 kB
JavaScript
"use strict";
/*
* Database API Generator is a utility library for Thunderstorm.
*
* Given proper configurations it will dynamically generate APIs to your Firestore
* collections, will assert uniqueness and restrict deletion... and more
*
* Copyright (C) 2020 Adam van der Kruk aka TacB0sS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModuleBE_BaseDBV2 = void 0;
const ts_common_1 = require("@nu-art/ts-common");
const backend_1 = require("@nu-art/firebase/backend");
const db_def_1 = require("./db-def");
const shared_1 = require("../shared");
const consts_1 = require("@nu-art/firebase/backend/firestore-v2/consts");
const ModuleBE_v2_SyncManager_1 = require("./ModuleBE_v2_SyncManager");
/**
* An abstract base class used for implementing CRUD operations on a specific collection.
*
* By default, it exposes API endpoints for creating, deleting, updating, querying and querying for unique document.
*/
class ModuleBE_BaseDBV2 extends ts_common_1.Module {
constructor(dbDef, appConfig) {
super();
// @ts-ignore
this.ModuleBE_BaseDBV2 = true;
this.querySync = async (syncQuery) => {
const items = await this.collection.query.custom(syncQuery);
const deletedItems = await ModuleBE_v2_SyncManager_1.ModuleBE_v2_SyncManager.queryDeleted(this.config.collectionName, syncQuery);
await this.upgradeInstances(items);
return { toUpdate: items, toDelete: deletedItems };
};
this._preWriteProcessing = async (dbItem, transaction) => {
await this.upgradeInstances([dbItem]);
await this.preWriteProcessing(dbItem, transaction);
};
this._postWriteProcessing = async (data) => {
const now = (0, ts_common_1.currentTimeMillis)();
if (data.updated && !(Array.isArray(data.updated) && data.updated.length === 0)) {
const latestUpdated = Array.isArray(data.updated) ?
data.updated.reduce((toRet, current) => Math.max(toRet, current.__updated), data.updated[0].__updated) :
data.updated.__updated;
await ModuleBE_v2_SyncManager_1.ModuleBE_v2_SyncManager.setLastUpdated(this.config.collectionName, latestUpdated);
}
if (data.deleted && !(Array.isArray(data.updated) && data.updated.length === 0)) {
await ModuleBE_v2_SyncManager_1.ModuleBE_v2_SyncManager.onItemsDeleted(this.config.collectionName, (0, ts_common_1.asArray)(data.deleted), this.config.uniqueKeys);
await ModuleBE_v2_SyncManager_1.ModuleBE_v2_SyncManager.setLastUpdated(this.config.collectionName, now);
}
else if (data.deleted === null)
// this means the whole collection has been deleted - setting the oldestDeleted to now will trigger a clean sync
await ModuleBE_v2_SyncManager_1.ModuleBE_v2_SyncManager.setOldestDeleted(this.config.collectionName, now);
await this.postWriteProcessing(data);
};
this.upgradeInstances = async (dbInstances) => {
await Promise.all(dbInstances.map(async (dbInstance) => {
var _a;
const instanceVersion = (_a = dbInstance._v) !== null && _a !== void 0 ? _a : (dbInstance._v = ts_common_1.DefaultDBVersion);
const currentVersion = this.config.versions[0];
if (instanceVersion !== undefined && instanceVersion !== currentVersion)
try {
await this.upgradeItem(dbInstance, currentVersion);
}
catch (e) {
this.logError(e);
throw new ts_common_1.ApiException(500, `Error while upgrading db item "${this.config.itemName}"(${dbInstance._id}): ${instanceVersion} => ${currentVersion}`, e);
}
dbInstance._v = currentVersion;
}));
};
const config = (0, db_def_1.getModuleBEConfig)(dbDef);
const preConfig = Object.assign(Object.assign({}, config), appConfig);
// @ts-ignore
this.setDefaultConfig(preConfig);
this.dbDef = dbDef;
this.canDeleteItems.bind(this);
this._preWriteProcessing.bind(this);
this._postWriteProcessing.bind(this);
this.manipulateQuery.bind(this);
this.collectDependencies.bind(this);
}
/**
* Executed during the initialization of the module.
* The collection reference is set in this method.
*/
init() {
var _a;
const firestore = backend_1.ModuleBE_Firebase.createAdminSession((_a = this.config) === null || _a === void 0 ? void 0 : _a.projectId).getFirestoreV2();
this.collection = firestore.getCollection(this.dbDef, {
canDeleteItems: this.canDeleteItems.bind(this),
preWriteProcessing: this._preWriteProcessing.bind(this),
postWriteProcessing: this._postWriteProcessing.bind(this),
manipulateQuery: this.manipulateQuery.bind(this)
});
// ############################## API ##############################
this.runTransaction = this.collection.runTransaction;
const wrapInTryCatch = (input, path) => (0, ts_common_1._keys)(input).reduce((acc, key) => {
const value = input[key];
const newPath = path ? `${path}.${String(key)}` : String(key);
if (typeof value === 'function') {
acc[key] = (async (...args) => {
try {
return await value(...args);
}
catch (e) {
this.logError(`Error while calling "${newPath}"`);
throw e;
}
});
return acc;
}
if (typeof value === 'object' && value !== null) {
acc[key] = wrapInTryCatch(value, newPath);
return acc;
}
acc[key] = value;
return acc;
}, {});
this.query = wrapInTryCatch(this.collection.query, 'query');
this.create = wrapInTryCatch(this.collection.create, 'create');
this.set = wrapInTryCatch(this.collection.set, 'set');
this.update = wrapInTryCatch(this.collection.update, 'update');
this.delete = wrapInTryCatch(this.collection.delete, 'delete');
this.doc = wrapInTryCatch(this.collection.doc, 'doc');
}
getCollectionName() {
return this.config.collectionName;
}
getItemName() {
return this.config.itemName;
}
__onFirestoreBackupSchedulerActV2() {
return [{
query: this.resolveBackupQuery(),
queryFunction: this.collection.query.custom,
moduleKey: this.config.collectionName,
version: this.config.versions[0]
}];
}
resolveBackupQuery() {
return shared_1._EmptyQuery;
}
/**
* Override this method to customize the processing that should be done before create, set or update.
*
* @param transaction - The transaction object.
* @param dbInstance - The DB entry for which the uniqueness is being asserted.
* @param request
*/
async preWriteProcessing(dbInstance, transaction) {
}
/**
* Override this method to customize processing that should be done after create, set, update or delete.
* @param data: a map of updated and deleted dbItems - deleted === null means the whole collection has been deleted
*/
async postWriteProcessing(data) {
}
manipulateQuery(query) {
return query;
}
async upgradeItem(dbItem, toVersion) {
}
async promoteCollection() {
var _a, _b;
// read chunks of ${maxChunkSize} documents that are not of the latest collection version..
// run them via upsert, which should convert/upgrade them to the latest version
// if timeout should kick in.. run the api again and this will continue the promotion on the rest of the documents
// TODO validate
this.logDebug(`Promoting '${this.config.collectionName}' to version: ${this.config.versions[0]}`);
let page = 0;
const itemsCount = this.config.maxChunkSize || 100;
let iteration = 0;
while (iteration < 5) {
try {
const itemsToSyncQuery = {
where: {
_v: { $neq: this.config.versions[0] },
},
limit: { page, itemsCount }
};
const items = await this.collection.query.custom(itemsToSyncQuery);
this.logInfo(`Page: ${page} Found: ${items.length} - first: ${(_a = items === null || items === void 0 ? void 0 : items[0]) === null || _a === void 0 ? void 0 : _a.__updated} last: ${(_b = items === null || items === void 0 ? void 0 : items[items.length - 1]) === null || _b === void 0 ? void 0 : _b.__updated}`);
await this.collection.set.all(items);
if (items.length < itemsCount)
break;
page++;
}
catch (e) {
break;
}
iteration++;
}
}
/**
* Override this method to provide actions or assertions to be executed before the deletion happens.
* @param transaction - The transaction object
* @param dbItems - The DB entry that is going to be deleted.
*/
async canDeleteItems(dbItems, transaction) {
const dependencies = await this.collectDependencies(dbItems, transaction);
if (dependencies)
throw new ts_common_1.ApiException(422, 'entity has dependencies').setErrorBody({
type: 'has-dependencies',
body: dependencies
});
}
async collectDependencies(dbInstances, transaction) {
const potentialErrors = await consts_1.canDeleteDispatcherV2.dispatchModuleAsync(this.dbDef.entityName, dbInstances, transaction);
const dependencies = (0, ts_common_1.filterInstances)(potentialErrors.map(item => ((item === null || item === void 0 ? void 0 : item.conflictingIds.length) || 0) === 0 ? undefined : item));
return dependencies.length > 0 ? dependencies : undefined;
}
async upgradeCollection(forceUpgrade) {
const docs = await this.collection.doc.query(shared_1._EmptyQuery);
const toDelete = docs.filter(doc => {
return doc.ref.id !== doc.data._id;
});
let items = (0, ts_common_1.filterDuplicates)(docs.map(d => d.data), ts_common_1.dbObjectToId);
// this should be paginated
if (!forceUpgrade)
items = items.filter(item => item._v !== this.dbDef.versions[0]);
this.logWarning(`Upgrading instances: ${items.length} ${this.dbDef.entityName}s ....`);
await (0, ts_common_1.batchAction)(items, this.dbDef.upgradeChunksSize || 200, async (chunk) => {
this.logWarning(`Upgrading instance: ${chunk[0]._id}`);
await this.upgradeInstances(chunk);
this.logWarning(`setting multi instances: ${chunk.length} ${this.dbDef.entityName}s ....`);
await this.set.multi(chunk);
});
if (toDelete.length > 0) {
this.logWarning(`Need to delete docs: ${toDelete.length} ${this.dbDef.entityName}s ....`);
await this.collection.delete.multi.allDocs(toDelete);
}
}
}
exports.ModuleBE_BaseDBV2 = ModuleBE_BaseDBV2;