UNPKG

@intuitionrobotics/db-api-generator

Version:
535 lines 24.5 kB
"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.BaseDB_ApiGenerator = exports.validator_InternationalPhoneNumber = exports.validator_LowerUpperStringWithDashesAndUnderscore = exports.validator_LowerUpperStringWithSpaces = exports.validator_LowercaseStringWithDashes = exports.validateNameWithDashesAndDots = exports.validator_JavaObjectMemberName = exports.validateStringAndNumbersWithDashes = exports.validateStringWithDashes = exports.validateOptionalId = exports.validateUniqueId = exports.validateGeneralUrl = exports.validateBucketUrl = exports.validateEmail = exports.validateId = void 0; const ts_common_1 = require("@intuitionrobotics/ts-common"); const apis_1 = require("./apis"); const backend_1 = require("@intuitionrobotics/thunderstorm/backend"); const backend_2 = require("@intuitionrobotics/firebase/backend"); const types_1 = require("../shared/types"); const idLength = 32; const validateId = (length, mandatory = true) => (0, ts_common_1.validateRegexp)(new RegExp(`^[0-9a-f]{${length}}$`), mandatory); exports.validateId = validateId; exports.validateEmail = ts_common_1.validateEmail; const validateBucketUrl = (mandatory) => (0, ts_common_1.validateRegexp)(/gs?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/, mandatory); exports.validateBucketUrl = validateBucketUrl; const validateGeneralUrl = (mandatory) => (0, ts_common_1.validateRegexp)(/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/, mandatory); exports.validateGeneralUrl = validateGeneralUrl; exports.validateUniqueId = (0, exports.validateId)(idLength); exports.validateOptionalId = (0, exports.validateId)(idLength, false); exports.validateStringWithDashes = (0, ts_common_1.validateRegexp)(/^[A-Za-z-]+$/); exports.validateStringAndNumbersWithDashes = (0, ts_common_1.validateRegexp)(/^[0-9A-Za-z-]+$/); exports.validator_JavaObjectMemberName = (0, ts_common_1.validateRegexp)(/^[a-z][a-zA-Z0-9]+$/); exports.validateNameWithDashesAndDots = (0, ts_common_1.validateRegexp)(/^[a-z-.]+$/); exports.validator_LowercaseStringWithDashes = (0, ts_common_1.validateRegexp)(/^[a-z-.]+$/); exports.validator_LowerUpperStringWithSpaces = (0, ts_common_1.validateRegexp)(/^[A-Za-z ]+$/); exports.validator_LowerUpperStringWithDashesAndUnderscore = (0, ts_common_1.validateRegexp)(/^[A-Za-z-_]+$/); exports.validator_InternationalPhoneNumber = (0, ts_common_1.validateRegexp)(/^\+(?:[0-9] ?){6,14}[0-9]$/); /** * 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 BaseDB_ApiGenerator extends ts_common_1.Module { constructor(collectionName, validator, itemName, moduleName) { super(moduleName); // @ts-ignore this.setDefaultConfig({ itemName, collectionName, externalFilterKeys: ["_id"], lockKeys: ["_id"] }); this.validator = validator; } setValidator(validator) { this.validator = validator; } __onFirestoreBackupSchedulerAct() { return [{ backupQuery: this.resolveBackupQuery(), collection: this.collection, keepInterval: 7 * ts_common_1.Day, interval: ts_common_1.Day, moduleKey: this.config.collectionName }]; } resolveBackupQuery() { return { where: {} }; } // this.setExternalUniqueKeys(["accessLevelIds"]); /** * Sets the external unique keys. External keys are the attributes of a document that must be unique inside the * collection. Default is `_id`. * * @remarks * You can only update the external unique keys before the module is initialized, preferably from its constructor. * * @param keys - The external unique keys. * * @returns * The external filter keys. */ setExternalUniqueKeys(keys) { if (this.initiated) throw new ts_common_1.BadImplementationException("You can only update the 'externalUniqueKeys' before the module was initialized.. preferably from its constructor"); return this.config.externalFilterKeys = keys; } /** * Sets the lock keys. Lock keys are the attributes of a document that must not be changed during a patch. * Thr property `_id` is always part of the lock keys. * * @remarks * You can only update the lock keys before the module is initialized, preferably from its constructor. * * @param keys - The lock keys. * * @returns * The lock keys. */ setLockKeys(keys) { if (this.initiated) throw new ts_common_1.BadImplementationException("You can only update the 'lockKeys' before the module was initialized.. preferably from its constructor"); return this.config.lockKeys = (0, ts_common_1.filterDuplicates)([...keys, "_id"]); } getCollectionName() { return this.config.collectionName; } getItemName() { return this.config.itemName; } /** * Executed during the initialization of the module. * The collection reference is set in this method. */ init() { var _a; const firestore = backend_2.FirebaseModule.createAdminSession((_a = this.config) === null || _a === void 0 ? void 0 : _a.projectId).getFirestore(); // @ts-ignore this.collection = firestore.getCollection(this.config.collectionName, this.config.externalFilterKeys); } assertExternalQueryUnique(instance, transaction) { return __awaiter(this, void 0, void 0, function* () { const dbInstance = yield transaction.queryItem(this.collection, instance); if (!dbInstance) { const uniqueQuery = backend_2.FirestoreInterface.buildUniqueQuery(this.collection, instance); throw new backend_1.ApiException(404, `Could not find ${this.config.itemName} with unique query '${(0, ts_common_1.__stringify)(uniqueQuery)}'`); } return dbInstance; }); } /** * Asserts the uniqueness of an instance in two steps: * - Executes `this.preUpsertProcessing`. * - Asserts uniqueness based on the internal filters. * * @param transaction - The transaction object. * @param instance - The document for which the uniqueness assertion will occur. */ assertUniqueness(transaction, instance, request) { return __awaiter(this, void 0, void 0, function* () { yield this.preUpsertProcessing(transaction, instance, request); const uniqueQueries = this.internalFilter(instance); if (uniqueQueries.length === 0) return; const dbInstances = yield Promise.all(uniqueQueries.map(uniqueQuery => { return transaction.queryUnique(this.collection, { where: uniqueQuery }); })); for (const idx in dbInstances) { const dbInstance = dbInstances[idx]; if (!dbInstance || dbInstance._id === instance._id) continue; const query = uniqueQueries[idx]; const message = (0, ts_common_1._keys)(query).reduce((carry, key) => { return carry + "\n" + `${String(key)}: ${query[key]}`; }, `${this.config.itemName} uniqueness violation. There is already a document with`); this.logWarning(message); throw new backend_1.ApiException(422, message); } }); } /** * Runs the module's validator for the instance. * * @param instance - The object to be validated. * * @throws `ApiException` for bad implementation or invalid input. */ validateImpl(instance) { try { (0, ts_common_1.validate)(instance, this.validator); } catch (e) { const badImplementation = (0, ts_common_1.isErrorOfType)(e, ts_common_1.BadImplementationException); if (badImplementation) throw new backend_1.ApiException(500, badImplementation.message); const error = (0, ts_common_1.isErrorOfType)(e, ts_common_1.ValidationException); if (error) { const errorBody = { type: types_1.ErrorKey_BadInput, body: { path: error.path, input: error.input } }; throw new backend_1.ApiException(400, error.message).setErrorBody(errorBody); } throw e; } } /** * Override this method to return a list of "where" queries that dictate uniqueness inside the collection. * Example return value: [{attribute1: item.attribute1, attribute2: item.attribute2}]. * * @param item - The DB entry that will be used. */ internalFilter(item) { return []; } /** * Override this method to customize the assertions that should be done before the insertion of the document to the DB. * * @param transaction - The transaction object. * @param dbInstance - The DB entry for which the uniqueness is being asserted. */ preUpsertProcessing(transaction, dbInstance, request) { return __awaiter(this, void 0, void 0, function* () { }); } /** * Override this method to provide actions or assertions to be executed before the deletion happens. * * Currently executed only before `deleteUnique()`. * * @param transaction - The transaction object * @param dbInstance - The DB entry that is going to be deleted. */ assertDeletion(transaction, dbInstance, request) { return __awaiter(this, void 0, void 0, function* () { return (yield this.assertDeletion_Read(transaction, dbInstance, request))(); }); } assertDeletion_Read(transaction, dbInstance, request) { return __awaiter(this, void 0, void 0, function* () { return () => __awaiter(this, void 0, void 0, function* () { }); }); } /** * A wrapper of the collections's `runInTransaction`. * * @param processor - The transaction's processor. * * @returns * A promise of the result of the `processor`. */ runInTransaction(processor) { return __awaiter(this, void 0, void 0, function* () { return this.collection.runInTransaction(processor); }); } // @ts-ignore deleteCollection() { return __awaiter(this, void 0, void 0, function* () { yield this.collection.deleteAll(); }); } /** * Inserts the `instance` using the `transaction` object. * * @param transaction - The transaction object. * @param instance - The object to be inserted. * @param request - The request in order to possibly obtain more info. * * @returns * A promise of the document that was inserted. */ // private async createImpl(transaction: FirestoreTransaction, instance: DBType, request?: ExpressRequest): Promise<DBType> { // return (await this.createImpl_Read(transaction, instance, request))() // }; createImpl_Read(transaction, instance, request) { return __awaiter(this, void 0, void 0, function* () { yield this.validateImpl(instance); yield this.assertUniqueness(transaction, instance, request); return () => transaction.insert(this.collection, instance); }); } ; /** * Upserts the `instance` using a transaction, after validating it and asserting uniqueness. * * @param instance - The object to be upserted. * @param transaction - OPTIONAL transaction to perform the upsert operation on * @param request - The request in order to possibly obtain more info. * * @returns * A promise of the document that was upserted. */ upsert(instance, transaction, request) { return __awaiter(this, void 0, void 0, function* () { const processor = (_transaction) => __awaiter(this, void 0, void 0, function* () { return (yield this.upsert_Read(instance, _transaction, request))(); }); if (transaction) return processor(transaction); return this.collection.runInTransaction(processor); }); } upsert_Read(instance, transaction, request) { return __awaiter(this, void 0, void 0, function* () { if (instance._id === undefined) return this.createImpl_Read(transaction, Object.assign(Object.assign({}, instance), { _id: (0, ts_common_1.generateHex)(idLength) }), request); return this.upsertImpl_Read(transaction, instance, request); }); } /** * Upserts a set of objects. Batching is used to circumvent firestore limitations on the number of objects. * * @param instances - The objects to be upserted. * @param request - The request in order to possibly obtain more info. * * @returns * A promise of an array of documents that were upserted. */ upsertAll_Batched(instances, request) { return __awaiter(this, void 0, void 0, function* () { return (0, ts_common_1.batchAction)(instances, 500, (chunked) => __awaiter(this, void 0, void 0, function* () { return this.upsertAll(chunked, undefined, request); })); }); } /** * Upserts the `dbInstances` using the `transaction` object. * * @param transaction - The transaction object. * @param instances - The instances to update. * @param request - The request in order to possibly obtain more info. * * @throws `BadImplementationException` when the instances are more than 500. * * @returns * A promise of the array of documents that were upserted. */ upsertAll(instances, transaction, request) { return __awaiter(this, void 0, void 0, function* () { if (instances.length > 500) { if (transaction) throw new ts_common_1.BadImplementationException('Firestore transaction supports maximum 500 at a time'); return this.upsertAll_Batched(instances, request); } const processor = (_transaction) => __awaiter(this, void 0, void 0, function* () { const writes = yield Promise.all(yield this.upsertAllImpl_Read(instances, _transaction, request)); return writes.map(write => write()); }); if (transaction) return processor(transaction); return this.collection.runInTransaction(processor); }); } upsertAllImpl_Read(instances, transaction, request) { return __awaiter(this, void 0, void 0, function* () { const actions = []; instances.reduce((carry, instance) => { (0, ts_common_1.addItemToArray)(carry, this.upsert_Read(instance, transaction, request)); return carry; }, actions); return Promise.all(actions); }); } /** * Upserts the `dbInstance` using the `transaction` transaction object. * * @param transaction - The transaction object. * @param dbInstance - The object to be upserted. * @param request - The request in order to possibly obtain more info. * * @returns * A promise of the document that was upserted. */ upsertImpl(transaction, dbInstance, request) { return __awaiter(this, void 0, void 0, function* () { return (yield this.upsertImpl_Read(transaction, dbInstance, request))(); }); } ; upsertImpl_Read(transaction, dbInstance, request) { return __awaiter(this, void 0, void 0, function* () { yield this.validateImpl(dbInstance); yield this.assertUniqueness(transaction, dbInstance, request); return transaction.upsert_Read(this.collection, dbInstance); }); } ; /** * Deletes a unique document based on its `_id`. Uses a transaction, after deletion assertions occur. * * @param _id - The _id of the object to be deleted. * @param transaction * @param request - The request in order to possibly obtain more info. * * @throws `ApiException` when the document doesn't exist in the collection. * * @returns * A promise of the document that was deleted. */ deleteUnique(_id, transaction, request) { return __awaiter(this, void 0, void 0, function* () { if (!_id) throw new ts_common_1.BadImplementationException(`No _id for deletion provided.`); const processor = (_transaction) => __awaiter(this, void 0, void 0, function* () { const write = yield this.deleteUnique_Read(_id, _transaction, request); if (!write) throw new backend_1.ApiException(404, `Could not find ${this.config.itemName} with unique id: ${_id}`); return write(); }); if (transaction) return processor(transaction); return this.collection.runInTransaction(processor); }); } deleteUnique_Read(_id, transaction, request) { return __awaiter(this, void 0, void 0, function* () { if (!_id) throw new ts_common_1.BadImplementationException(`No _id for deletion provided.`); const ourQuery = { where: { _id } }; const dbInstance = yield transaction.queryUnique(this.collection, ourQuery); if (!dbInstance) throw new backend_1.ApiException(404, `Could not find ${this.config.itemName} with unique id: ${_id}`); const write = yield this.deleteImpl_Read(transaction, ourQuery, request); return () => __awaiter(this, void 0, void 0, function* () { if (!write) return dbInstance; // Here can do both read an write! yield this.assertDeletion(transaction, dbInstance, request); return write(); }); }); } /** * Uses the `transaction` to delete a unique document, querying with the `ourQuery`. * * @param transaction - The transaction object. * @param ourQuery - The query to be used for the deletion. * @param request - The request in order to possibly obtain more info. * * @returns * A promise of the document that was deleted. */ deleteImpl_Read(transaction, ourQuery, request) { return __awaiter(this, void 0, void 0, function* () { const write = yield transaction.deleteUnique_Read(this.collection, ourQuery); if (!write) throw new ts_common_1.ThisShouldNotHappenException(`I just checked that I had an instance for query: ${(0, ts_common_1.__stringify)(ourQuery)}`); return write; }); } /** * Calls the `delete` method of the module's collection. * * @param query - The query to be executed for the deletion. * @param request - The request in order to possibly obtain more info. */ delete(query, request) { return __awaiter(this, void 0, void 0, function* () { return this.collection.delete(query); }); } /** * Queries the database for a specific document in the module's collection. * * @param where - The where clause to be used for querying. * @param request - The request in order to possibly obtain more info. * * @throws `ApiException` if the document is not found. * * @returns * The DB document that was found. */ queryUnique(where, request) { return __awaiter(this, void 0, void 0, function* () { const dbItem = yield this.collection.queryUnique({ where }); if (!dbItem) throw new backend_1.ApiException(404, `Could not find ${this.config.itemName} with unique query: ${JSON.stringify(where)}`); return dbItem; }); } /** * Executes the specified query on the module's collection. * * @param query - The query to be executed. * @param request - The request in order to possibly obtain more info. * * @returns * A promise of an array of documents. */ query(query, request) { return __awaiter(this, void 0, void 0, function* () { return yield this.collection.query(query); }); } /** * If propsToPatch is not set, we remove the lock keys from the caller's instance * before merging with the original dbInstance. * If propsToPatch is set, we also remove all of the instance's keys that * are not specified in propsToPatch. * * @param instance - The instance to be upserted. * @param propsToPatch - Properties to patch. * @param request - The request in order to possibly obtain more info. * * @returns * A promise of the patched document. */ patch(instance, propsToPatch, request) { return __awaiter(this, void 0, void 0, function* () { return this.collection.runInTransaction((transaction) => __awaiter(this, void 0, void 0, function* () { const dbInstance = yield this.assertExternalQueryUnique(instance, transaction); // If the caller has specified props to be changed, make sure the don't conflict with the lockKeys. const wrongKey = propsToPatch === null || propsToPatch === void 0 ? void 0 : propsToPatch.find(prop => this.config.lockKeys.includes(prop)); if (wrongKey) throw new ts_common_1.BadImplementationException(`Key ${String(wrongKey)} is part of the 'lockKeys' and cannot be updated.`); // If the caller has not specified props, we remove the keys from the caller's instance // before merging with the original dbInstance. (0, ts_common_1._keys)(instance).forEach(key => { if (this.config.lockKeys.includes(key) || (propsToPatch && !propsToPatch.includes(key))) { delete instance[key]; } }); const mergedObject = (0, ts_common_1.merge)(dbInstance, instance); yield (0, ts_common_1.validate)(mergedObject, this.validator); yield this.assertUniqueness(transaction, mergedObject, request); return this.upsertImpl(transaction, mergedObject, request); })); }); } apiCreate(pathPart) { return new apis_1.ServerApi_Create(this, pathPart); } apiQuery(pathPart) { return new apis_1.ServerApi_Query(this, pathPart); } apiQueryUnique(pathPart) { return new apis_1.ServerApi_Unique(this, pathPart); } apiUpdate(pathPart) { return new apis_1.ServerApi_Update(this, pathPart); } apiDelete(pathPart) { return new apis_1.ServerApi_Delete(this, pathPart); } /** * Override this method, to control which server api endpoints are created automatically. * * @param pathPart - The path part. * * @returns * An array of api endpoints. */ apis(pathPart) { return (0, ts_common_1.filterInstances)([ this.apiCreate(pathPart), this.apiQuery(pathPart), this.apiQueryUnique(pathPart), this.apiUpdate(pathPart), this.apiDelete(pathPart), ]); } } exports.BaseDB_ApiGenerator = BaseDB_ApiGenerator; //# sourceMappingURL=BaseDB_ApiGenerator.js.map