@intuitionrobotics/db-api-generator
Version:
535 lines • 24.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.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