diff --git a/package.json b/package.json index 1309e15..da9aab6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@charge-tech/chargejs", - "version": "0.1.0", + "version": "2.0.0-beta.19", "description": "The Charge Core JS implementation.", "main": "src/index.js", "engines": { @@ -55,11 +55,13 @@ "http-status": "^1.5.0", "https": "^1.0.0", "lodash": "^4.17.20", + "paginate-info": "^1.0.4", "node-device-detector": "^1.3.11", "structure": "^2.0.1", "winston": "^3.2.1" }, "peerDependencies": { + "awilix": "^5.0.1", "awilix-express": "^5.0.0" }, "directories": { diff --git a/src/constants/index.js b/src/constants/index.js index 35bf275..5b6709c 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -150,7 +150,8 @@ export const ERROR_TYPES = { CACHE_DISABLED: 'CacheDisabledError', INVALID_FILTER: 'InvalidFilterError', INVALID_FILTER_VALUE: 'InvalidFilterValueError', - INVALID_FILTER_TYPE: 'InvalidFIlterTypeError', + INVALID_FILTER_TYPE: 'InvalidFilterTypeError', + EMPTY_UPDATE_FIELDS: 'EmptyUpdateFields', INVALID_PAGINATE_PARAMS: 'InvalidPaginateParamsError' } diff --git a/src/errors/index.js b/src/errors/index.js index e9d5c34..dfa312d 100644 --- a/src/errors/index.js +++ b/src/errors/index.js @@ -17,6 +17,11 @@ export const VALIDATION_ERROR = { code: ERROR_TYPES.VALIDATION_ERROR } +export const CONFLICT_ERROR = { + message: 'Conflict error', + code: ERROR_TYPES.CONFLICT +} + export const INVALID_ENV = { message: `Invalid environment. Allowed values ${ALLOWED_ENVIRONMENTS}`, code: CODES.INVALID_ENV @@ -48,6 +53,11 @@ export const INVALID_FILTER_TYPE = { code: ERROR_TYPES.INVALID_FILTER_TYPE } +export const EMPTY_UPDATE_FIELDS = { + message: 'No fileds to update', + code: ERROR_TYPES.EMPTY_UPDATE_FIELDS +} + export const INVALID_CURRENT_PAGE = { message: 'currentPage parameter should be a valid number', code: ERROR_TYPES.INVALID_PAGINATE_PARAMS diff --git a/src/infra/BaseRepository.js b/src/infra/BaseRepository.js index c438672..f76c619 100644 --- a/src/infra/BaseRepository.js +++ b/src/infra/BaseRepository.js @@ -7,7 +7,8 @@ import { SEQUELIZE_NOT_FOUNT_ERROR, SEQUELIZE_VALIDATION_ERROR, INVALID_PAGE_SIZE, - INVALID_CURRENT_PAGE + INVALID_CURRENT_PAGE, + EMPTY_UPDATE_FIELDS } from '../errors' /** @@ -32,14 +33,15 @@ class BaseRepository { redisRepository, calculateLimitAndOffset, paginate, - config: { logmsg }, + config, standardError }) { this.transactionProvider = transactionProvider this.redisRepository = redisRepository this.calculateLimitAndOffset = calculateLimitAndOffset this.paginate = paginate - this.logmsg = logmsg + this.config = config + this.logmsg = config.logmsg this.standardError = standardError } @@ -47,7 +49,6 @@ class BaseRepository { * Provides configuration for BaseRepository. * * @param {Object} input The input object as injected in src/container.js - * @param {string} input.modelName The model name * @param {Object} input.model The db model * @param {Object} input.mapper The entity mapper * @param {string[]} [input.patchAllowedFields] The patch allowed fields array @@ -55,22 +56,25 @@ class BaseRepository { * @param {boolean} [input.cacheDisabled=false] Disable cahing */ init ({ - modelName, model, mapper, patchAllowedFields = undefined, include = undefined, cacheDisabled = false }) { - this.modelName = modelName + this.modelName = this._getModelName(model) this.model = model this.mapper = mapper this.patchAllowedFields = patchAllowedFields this.include = include this.cacheDisabled = cacheDisabled - const indexes = !cacheDisabled ? model._indexes : [] - this.redisRepository.init(modelName, indexes, include) + this.redisRepository.init({ + modelName: this.modelName, + indexes: this._getIndexes(model), + include: this._normalizeInclude(include), + references: this._getReferences(model) + }) } /** @@ -117,25 +121,64 @@ class BaseRepository { } } + /** + * Сount entities by filter + * + * @param {Object} where filter + * @param {Object} [options] additional sequelize options + * @returns {Promise} number of entities + */ + count (where, options) { + return this.model.count({ + ...options, + where, + include: this.include, + transaction: this._getTransaction() + }) + } + + /** + * Returns latest entry + * + * @param {Object} [where] filter + * @param {Object} [options] additional sequelize options + * @param {String} [options.orderBy] order field + * @returns {Promise} entity + * @throws {module:interface.standardError} + */ + async findLatest (where = {}, { rejectOnEmpty = true, ...opts } = {}) { + const [result] = await this.findAll(where, { + limit: 1, + ...opts + }) + if (result) { + return result + } + if (rejectOnEmpty) { + throw this._getNotFoundError() + } + } + /** * Finds entities by filter * * @param {Object} where filter * @param {Object} [options] additional sequelize options + * @param {String} [options.orderBy] order field * @returns {Promise} entities * @throws {module:interface.standardError} */ - async findAll (where, options = {}) { + async findAll (where, { orderBy = 'createdAt', ...opts } = {}) { this._validateFilter(where) - const results = await this.model.findAll({ - order: [['createdAt', 'DESC']], - ...options, + const rows = await this.model.findAll({ + order: [[orderBy, 'DESC']], + ...opts, where, include: this.include, transaction: this._getTransaction() }) - return results.map((entry) => entry.toJSON()) + return rows.map(row => this.mapper.toEntity(row.toJSON())) } /** @@ -150,18 +193,19 @@ class BaseRepository { * @param {number} currentPage current page number * @param {number} pageSize page size * @param {Object} [options] additional sequelize options + * @param {String} [options.orderBy] order field * @returns {Promise<{ data: Object[], meta: Meta>} entities * @throws {module:interface.standardError} */ - async findAndCountAll (where, currentPage, pageSize, options = {}) { + async findAndCountAll (where, currentPage, pageSize, { orderBy = 'createdAt', ...opts } = {}) { this._validateFilter(where) this._validatePaginateParams(currentPage, pageSize) const { limit, offset } = this.calculateLimitAndOffset(currentPage, pageSize) const { rows, count } = await this.model.findAndCountAll({ - order: [['createdAt', 'DESC']], - ...options, + order: [[orderBy, 'DESC']], + ...opts, limit, offset, distinct: true, @@ -170,7 +214,10 @@ class BaseRepository { transaction: this._getTransaction() }) const meta = this.paginate(currentPage, count, rows, pageSize) - return { data: rows.map((row) => row.toJSON()), meta } + return { + data: rows.map(row => this.mapper.toEntity(row.toJSON())), + meta + } } /** @@ -185,10 +232,13 @@ class BaseRepository { if (!valid) { throw this._getValidationError(errors) } - const dbEntry = await this.model.create(this.mapper.toDatabase(entity), { - transaction: this._getTransaction() - }) - return this.mapper.toEntity(dbEntry.toJSON()) + const dbEntry = await this.model.create( + this.mapper.toDatabase(entity), + { transaction: this._getTransaction() } + ) + const json = dbEntry.toJSON() + await this.redisRepository.clearReferenced(json) + return this.mapper.toEntity(json) } /** @@ -196,11 +246,12 @@ class BaseRepository { * * @param {string} id entity uuid * @param {Object} updateFields the fields to be updated + * @param {boolean} [filterFields=true] should filter updateFields * @returns {Promise} updated domain entity * @throws {module:interface.standardError} */ - async patchById (id, updateFields) { - return this.patch({ id }, updateFields) + async patchById (id, updateFields, filterFields = true) { + return this.patch({ id }, updateFields, filterFields) } /** @@ -208,13 +259,20 @@ class BaseRepository { * * @param {Object} where filter * @param {Object} updateFields the fields to be updated + * @param {boolean} [filterFields=true] should filter updateFields * @returns {Promise} updated domain entity * @throws {module:interface.standardError} */ - async patch (where, updateFields) { + async patch (where, updateFields, filterFields = true) { this._validateFilter(where) - const filteredFields = this._filterPatchFields(updateFields) + const filteredFields = filterFields ? this._filterPatchFields(updateFields) : updateFields + if (!Object.keys(filteredFields).length) { + throw this._getValidationError([{ + message: EMPTY_UPDATE_FIELDS.message, + path: 'updateFields' + }]) + } try { const filter = await this._getPatchFilter(where) const [, result] = await this.model.update(filteredFields, { @@ -227,7 +285,7 @@ class BaseRepository { throw this._getNotFoundError() } const json = result.toJSON() - await this._clearCache(json.id) + await this._clearCache(json) return this.mapper.toEntity(json) } catch (error) { switch (error.name) { @@ -263,16 +321,20 @@ class BaseRepository { this._validateFilter(where) try { - const filter = await this._getPatchFilter(where) - const [, result] = await this.model.destroy({ - where: filter, + const result = await this.model.findOne({ + where, + distinct: true, transaction: this._getTransaction() }) if (!result) { throw this._getNotFoundError() } + await this.model.destroy({ + where: { id: result.id }, + transaction: this._getTransaction() + }) const json = result.toJSON() - await this._clearCache(json.id) + await this._clearCache(json) return this.mapper.toEntity(json) } catch (error) { switch (error.name) { @@ -287,6 +349,47 @@ class BaseRepository { } // Private + _getModelName (model) { + const modelName = model.options.name.singular + return modelName.charAt(0).toLowerCase() + modelName.slice(1) + } + + _getReferences (model) { + const models = Object.values(model.sequelize.models) + return Object.values(model.rawAttributes) + .filter(({ references }) => references && references.key === 'id') + .map(attr => { + const { model } = attr.references + const m = models.find(m => m.tableName === model) + return { + modelName: this._getModelName(m), + fieldName: attr.fieldName + } + }) + } + + _normalizeInclude (include) { + if (!include) { + return [] + } + return include.map(({ model, as, include }) => ({ + modelName: this._getModelName(model), + as, + include: include && this._normalizeInclude(include) + })) + } + + _getIndexes (model) { + const models = Object.values(model.sequelize.models) + const entries = models.map(model => { + const modelName = this._getModelName(model) + const indexes = (model.options.indexes || []) + .filter(index => index.unique) + .map(index => index.fields) + return [modelName, indexes] + }) + return Object.fromEntries(entries) + } async _dbFindOne (where) { const result = await this.model.findOne({ @@ -300,11 +403,8 @@ class BaseRepository { } } - async _clearCache (id) { - if (!this.cacheDisabled) { - await this.redisRepository.delete(id) - } - await this.redisRepository.clearRelated(id) + _clearCache (entity) { + return this.redisRepository.clear(entity, this.cacheDisabled) } async _getPatchFilter (where) { @@ -342,7 +442,6 @@ class BaseRepository { error.type = INVALID_CURRENT_PAGE.code throw error } - if (Number.isNaN(Number(pageSize))) { // Error for developers only, can only occur if the pageSize is invalid const error = new Error(INVALID_PAGE_SIZE.message) @@ -398,7 +497,7 @@ class BaseRepository { } _getCapitalizedModelName () { - return this.modelName.charAt(0).toUpperCase() + this.modelName.slice(1) + return this.model.options.name.singular } _notFoundErrorMessage () { diff --git a/src/infra/index.js b/src/infra/index.js index d46ab60..902b7d2 100644 --- a/src/infra/index.js +++ b/src/infra/index.js @@ -1,5 +1,35 @@ +import { asClass, asFunction, asValue } from 'awilix' +import { calculateLimitAndOffset, paginate } from 'paginate-info' +import { Transaction, TransactionProvider } from './transaction' +import { RedisRepository, RedisStorage, redis } from './redis' + +function register (container, repositories) { + container.register({ + calculateLimitAndOffset: asValue(calculateLimitAndOffset), + paginate: asValue(paginate), + redis: asFunction(redis).singleton(), + redisStorage: asClass(RedisStorage).singleton(), + redisRepository: asClass(RedisRepository), + Transaction: asValue(Transaction), + transactionProvider: asClass(TransactionProvider).scoped() + }) + container.register( + Object.fromEntries( + Object.entries(repositories).map(([name, repository]) => [ + lowerСaseName(name), + asClass(repository).scoped() + ]) + ) + ) +} + +function lowerСaseName (name) { + return name.charAt(0).toLowerCase() + name.slice(1) +} + +export { register } +export { Transaction, TransactionProvider } +export { RedisRepository, RedisStorage, redis } +export { default as BaseRepository } from './BaseRepository' export { default as LoggerStreamAdapter } from './LoggerStreamAdapter' export { default as ModelLoader } from './ModelLoader' -export { default as BaseRepository } from './BaseRepository' -export * from './transaction' -export * from './redis' diff --git a/src/infra/redis/RedisRepository.js b/src/infra/redis/RedisRepository.js index f90af0e..8cb5708 100644 --- a/src/infra/redis/RedisRepository.js +++ b/src/infra/redis/RedisRepository.js @@ -13,7 +13,12 @@ class RedisRepository { this.transactionProvider = transactionProvider } - init (modelName, indexes, include) { + init ({ + modelName, + indexes, + include, + references + }) { if (this._initialized) { // Error for developers only, can only occur if init was called more than once const error = new Error(REDIS_REPOSITORY_INITIALIZED.message) @@ -24,6 +29,7 @@ class RedisRepository { this.modelName = modelName this.include = include this.indexes = this._normalizeIndexes(indexes) + this.references = references } async findOneOrCreate (where, getObject) { @@ -56,63 +62,87 @@ class RedisRepository { async create (object) { const idKey = this._buildIdKey(this.modelName, object.id) - await this._saveObject(idKey, object) - const cacheKeys = this._getObjectCacheKeys(this.modelName, this.indexes, object) - await Promise.all(cacheKeys.map( - key => this._saveObject(key, object.id) - )) - await this._storeRelated(object) + await this._saveRow(idKey, object) + const cacheKeys = this._getObjectCacheKeys(this.modelName, object) + await Promise.all([ + this._storeIncluded(object), + ...cacheKeys.map(key => this._saveRow(key, object.id)) + ]) return object } - delete (id) { - return this._delete(this.modelName, this.indexes, id) + async clearReferenced (object) { + const references = this.references.map(({ modelName, fieldName }) => ({ + modelName, + id: object[fieldName] + })).filter(r => r.id) + if (references.length) { + await Promise.all( + references.map(({ modelName, id }) => this._clear(modelName, id)) + ) + } } - async clearRelated (id) { - const key = this._buildRelationsKey(this.modelName, id) - const relations = await this.redisStorage.getList(key) - if (relations.length) { - await Promise.all( - relations.map(({ modelName, indexes, id }) => { - return this._delete(modelName, indexes, id) + clear ({ id }, relationsOnly = false) { + return this._clear(this.modelName, id, relationsOnly) + } + + _clear (modelName, id, relationsOnly = false) { + const promises = [ + this._clearRelated(modelName, id) + ] + if (!relationsOnly) { + promises.unshift(this._clearObject(modelName, id)) + } + return Promise.all(promises) + } + + async _clearRelated (modelName, id) { + const key = this._buildRelationsKey(modelName, id) + const related = await this.redisStorage.getList(key) + if (related.length) { + await Promise.all([ + this._clearList(key, related), + ...related.map(({ modelName, id }) => { + return this._clearObject(modelName, id) }) - ) - return this._clearList(key, relations) + ]) } } _normalizeIndexes (indexes) { - return indexes.filter(index => index.unique) - .filter(index => !index.fields.includes('id')) - .map(index => index.fields.sort()) + return Object.fromEntries( + Object.entries(indexes).map(([modelName, index]) => [ + modelName, + index.filter(fields => !fields.includes('id')).map(fields => fields.sort()) + ]) + ) } - async _delete (modelName, indexes, id) { + async _clearObject (modelName, id) { const object = await this._getById(modelName, id) if (object) { const key = this._buildIdKey(modelName, id) - await this._deleteObject(key, object) - const cacheKeys = this._getObjectCacheKeys(modelName, indexes, object) + await this._deleteRow(key, object) + const cacheKeys = this._getObjectCacheKeys(modelName, object) if (cacheKeys.length) { await Promise.all(cacheKeys.map( - key => this._deleteObject(key, id) + key => this._deleteRow(key, id) )) } } } - async _storeRelated (entity) { - if (this.include && this.include.length) { + async _storeIncluded (entity) { + if (this.include.length) { const entityInfo = { id: entity.id, - modelName: this.modelName, - indexes: this.indexes + modelName: this.modelName } - const related = this._getRelated(entity, this.include) - if (related.length) { + const included = this._getIncluded(entity, this.include) + if (included.length) { await Promise.all( - related + included .map(({ modelName, id }) => this._buildRelationsKey(modelName, id)) .map(key => this._pushToList(key, entityInfo)) ) @@ -120,7 +150,7 @@ class RedisRepository { } } - _getRelated (entity, include) { + _getIncluded (entity, include) { return include.reduce((result, { modelName, as, include }) => { const related = entity[as] if (related) { @@ -131,7 +161,7 @@ class RedisRepository { id: object.id }) if (include) { - result.push(...this._getRelated(object, include)) + result.push(...this._getIncluded(object, include)) } }) } @@ -154,11 +184,11 @@ class RedisRepository { async _pushToList (key, object) { await this.redisStorage.listPush(key, object) this.transactionProvider.addRedisRollback( - async () => this.redisStorage.listRemove(key, object) + () => this.redisStorage.listRemove(key, object) ) } - async _saveObject (key, object) { + async _saveRow (key, object) { await this.redisStorage.saveObject(key, object) this.transactionProvider.addRedisRollback( () => this.redisStorage.deleteObject(key) @@ -168,11 +198,11 @@ class RedisRepository { async _clearList (key, values) { await this.redisStorage.listClear(key) this.transactionProvider.addRedisRollback( - () => this.redisStorage.pushToList(key, ...values) + () => this.redisStorage.listPush(key, ...values) ) } - async _deleteObject (key, object) { + async _deleteRow (key, object) { await this.redisStorage.deleteObject(key) this.transactionProvider.addRedisRollback( () => this.redisStorage.saveObject(key, object) @@ -191,12 +221,13 @@ class RedisRepository { value, type: typeof value })) - const error = new Error(`${INVALID_FILTER_VALUE.message}: Invalid fields: ${fields}`) + const error = new Error(`${INVALID_FILTER_VALUE.message}: Invalid fields: ${JSON.stringify(fields)}`) error.type = INVALID_FILTER_VALUE.code throw error } const sortedKeys = Object.keys(normalizedFilter).sort() - const valid = sortedKeys.includes('id') || this.indexes.find(key => isEqual(key, sortedKeys)) + const indexes = this.indexes[this.modelName] + const valid = sortedKeys.includes('id') || indexes.find(key => isEqual(key, sortedKeys)) if (!valid) { // Error for developers only, can only occur if the indexes configuration is incorrect const error = new Error(`${INVALID_FILTER.message}: "${JSON.stringify(sortedKeys)}"`) @@ -243,8 +274,8 @@ class RedisRepository { } } - _getObjectCacheKeys (modelName, indexes, object) { - return indexes.map(index => { + _getObjectCacheKeys (modelName, object) { + return this.indexes[modelName].map(index => { const filter = index.reduce((res, field) => ({ ...res, [field]: get(object, field) diff --git a/src/infra/transaction/TransactionProvider.js b/src/infra/transaction/TransactionProvider.js index 67291b8..9d9bdf3 100644 --- a/src/infra/transaction/TransactionProvider.js +++ b/src/infra/transaction/TransactionProvider.js @@ -10,18 +10,23 @@ class TransactionProvider { } async useTransaction (action) { + // Check for cases when useTransaction is called inside useTransaction, + // so only outer transaction wrapping should commit changes + if (this.current) { + return action() + } try { - await this.transaction() + await this._transaction() const response = await action() - await this.commit() + await this._commit() return response } catch (error) { - await this.rollback() + await this._rollback() throw error } } - async transaction () { + async _transaction () { if (!this.current) { const transaction = new this.Transaction(this.database) this.current = await transaction.begin() @@ -29,12 +34,12 @@ class TransactionProvider { return this.current } - async commit () { + async _commit () { await this.current.commit() this.current = undefined } - async rollback () { + async _rollback () { await this.current.rollback() this.current = undefined } diff --git a/src/interfaces/ServiceCommunicator.js b/src/interfaces/ServiceCommunicator.js index fde8b98..b3cb3b0 100644 --- a/src/interfaces/ServiceCommunicator.js +++ b/src/interfaces/ServiceCommunicator.js @@ -1,5 +1,6 @@ import axios from 'axios' import Status from 'http-status' +import { NOT_FOUND, VALIDATION_ERROR, CONFLICT_ERROR } from '../errors' export class ServiceCommunicator { constructor ({ @@ -31,7 +32,7 @@ export class ServiceCommunicator { } _getStandardError (error) { - const type = this._getErrorType(error.type) + const type = this._getErrorCode(error.type) const { message, errors = [] } = error return this.standardError({ type, @@ -44,12 +45,14 @@ export class ServiceCommunicator { }) } - _getErrorType (type) { + _getErrorCode (type) { switch (type) { case Status['400_NAME']: - return this.logmsg.errors.validationError + return VALIDATION_ERROR.code case Status['404_NAME']: - return this.logmsg.errors.notFoundError + return NOT_FOUND.code + case Status['409_NAME']: + return CONFLICT_ERROR.code default: return type } diff --git a/src/interfaces/index.js b/src/interfaces/index.js index a7f7127..ebd467b 100644 --- a/src/interfaces/index.js +++ b/src/interfaces/index.js @@ -153,8 +153,7 @@ export const deviceMiddleware = (req, res, next) => { export const errorHandler = (err, req, res, next) => { res.status(Status.INTERNAL_SERVER_ERROR).json({ type: 'InternalServerError', - message: 'The server failed to handle this request', - errors: err.errors + message: 'The server failed to handle this request' }) } /** diff --git a/tests/domain.test.js b/tests/domain.test.js index fcde8eb..cbbcff9 100644 --- a/tests/domain.test.js +++ b/tests/domain.test.js @@ -14,7 +14,6 @@ describe('domain', function () { it('it set meta correctly', function () { const meta = 'test' base.setMeta(meta) - console.log(base.meta) assert.deepStrictEqual(base.meta, meta) }) it('it should set masked correctly', function () { diff --git a/tests/fixtures/functions/infra/SequelizeModelStub.js b/tests/fixtures/functions/infra/SequelizeModelStub.js index a261ca2..33d3595 100644 --- a/tests/fixtures/functions/infra/SequelizeModelStub.js +++ b/tests/fixtures/functions/infra/SequelizeModelStub.js @@ -1,4 +1,68 @@ export default { + tableName: 'ModelName', + options: { + name: { + singular: 'ModelName' + } + }, + rawAttributes: { + id: { + allowNull: false, + primaryKey: true, + Model: { + options: { + name: { + singular: 'ModelName' + } + } + }, + fieldName: 'id', + _modelAttribute: true, + field: 'id' + }, + otherId: { + allowNull: false, + Model: { + options: { + name: { + singular: 'ModelName' + } + } + }, + fieldName: 'otherId', + references: { model: 'Other', key: 'id' }, + _modelAttribute: true, + field: 'otherId' + } + }, + sequelize: { + models: { + ModelName: { + tableName: 'ModelName', + options: { + name: { + singular: 'ModelName' + }, + indexes: [ + { unique: false }, + { unique: true, fields: ['a', 'b'] }, + { unique: true, fields: ['c', 'd'] } + ] + } + }, + Other: { + tableName: 'Other', + options: { + name: { + singular: 'Other' + }, + indexes: [ + { unique: false } + ] + } + } + } + }, findOne () { return true }, diff --git a/tests/fixtures/functions/infra/redis/RedisRepositoryStub.js b/tests/fixtures/functions/infra/redis/RedisRepositoryStub.js index 44ff446..7803720 100644 --- a/tests/fixtures/functions/infra/redis/RedisRepositoryStub.js +++ b/tests/fixtures/functions/infra/redis/RedisRepositoryStub.js @@ -14,10 +14,10 @@ export default { create (obj) { return obj }, - delete () { + clear () { return true }, - clearRelated () { + clearReferenced () { return true } } diff --git a/tests/infra/BaseRepository.test.js b/tests/infra/BaseRepository.test.js index 7ab91e4..b3cb8ee 100644 --- a/tests/infra/BaseRepository.test.js +++ b/tests/infra/BaseRepository.test.js @@ -50,7 +50,6 @@ describe('BaseRepository', function () { describe('init', function () { const opts = { - modelName: 'modelName', model: SequelizeModelStub, mapper: EntityMapperStub, patchAllowedFields: ['field', 'field2'], @@ -64,10 +63,33 @@ describe('BaseRepository', function () { cacheDisabled: false } it('should init BaseRepository', async function () { + sinon.spy(RedisRepositoryStub, 'init') baseRepository.init(opts) Object.entries(opts).forEach(([key, value]) => { expect(baseRepository[key]).to.deep.equal(value) }) + expect(baseRepository.modelName).to.deep.equal('modelName') + RedisRepositoryStub.init.should.have.been.calledWith({ + modelName: 'modelName', + indexes: { + modelName: [ + ['a', 'b'], + ['c', 'd'] + ], + other: [] + }, + references: [ + { modelName: 'other', fieldName: 'otherId' } + ], + include: [ + { + modelName: 'modelName', + as: 'realted', + include: undefined + } + ] + }) + RedisRepositoryStub.init.restore() }) }) @@ -359,6 +381,7 @@ describe('BaseRepository', function () { sinon.spy(entity, 'validate') sinon.spy(baseRepository, '_getValidationError') + sinon.spy(RedisRepositoryStub, 'clearReferenced') sinon.stub(SequelizeModelStub, 'create').callsFake(() => dbEntry) sinon.stub(EntityMapperStub, 'toDatabase').callsFake(() => obj) sinon.stub(EntityMapperStub, 'toEntity').callsFake(() => entity) @@ -376,6 +399,8 @@ describe('BaseRepository', function () { EntityMapperStub.toDatabase.should.have.been.calledOnce EntityMapperStub.toDatabase.should.have.been.calledWith(entity) expect(SequelizeModelStub.create.getCall(0).args[0]).to.deep.equal(obj) + RedisRepositoryStub.clearReferenced.should.have.been.calledOnce + RedisRepositoryStub.clearReferenced.should.have.been.calledWith(obj) EntityMapperStub.toEntity.should.have.been.calledOnce EntityMapperStub.toEntity.should.have.been.calledWith(obj) @@ -383,6 +408,7 @@ describe('BaseRepository', function () { entity.validate.restore() baseRepository._getValidationError.restore() + RedisRepositoryStub.clearReferenced.restore() SequelizeModelStub.create.restore() EntityMapperStub.toDatabase.restore() EntityMapperStub.toEntity.restore() @@ -484,7 +510,7 @@ describe('BaseRepository', function () { sinon.stub(SequelizeModelStub, 'update').callsFake(() => [1, dbEntry]) sinon.spy(baseRepository, '_getNotFoundError') sinon.stub(EntityMapperStub, 'toEntity').callsFake(() => domainEntity) - sinon.spy(RedisRepositoryStub, 'delete') + sinon.spy(RedisRepositoryStub, 'clear') baseRepository.init({ modelName: 'modelName', @@ -502,8 +528,8 @@ describe('BaseRepository', function () { baseRepository._getNotFoundError.should.have.not.been.called EntityMapperStub.toEntity.should.have.been.calledOnce EntityMapperStub.toEntity.should.have.been.calledWith(obj) - RedisRepositoryStub.delete.should.have.been.calledOnce - expect(RedisRepositoryStub.delete.getCall(0).args[0]).to.deep.equal(obj.id) + RedisRepositoryStub.clear.should.have.been.calledOnce + expect(RedisRepositoryStub.clear.getCall(0).args[0]).to.deep.equal(obj) expect(res).to.deep.equal(domainEntity) baseRepository._filterPatchFields.restore() @@ -511,7 +537,7 @@ describe('BaseRepository', function () { SequelizeModelStub.update.restore() baseRepository._getNotFoundError.restore() EntityMapperStub.toEntity.restore() - RedisRepositoryStub.delete.restore() + RedisRepositoryStub.clear.restore() }) it('should patch entity (cacheDisabled=true)', async function () { @@ -520,7 +546,7 @@ describe('BaseRepository', function () { sinon.stub(SequelizeModelStub, 'update').callsFake(() => [1, dbEntry]) sinon.spy(baseRepository, '_getNotFoundError') sinon.stub(EntityMapperStub, 'toEntity').callsFake(() => domainEntity) - sinon.spy(RedisRepositoryStub, 'delete') + sinon.spy(RedisRepositoryStub, 'clear') baseRepository.init({ modelName: 'modelName', @@ -546,7 +572,7 @@ describe('BaseRepository', function () { SequelizeModelStub.update.restore() baseRepository._getNotFoundError.restore() EntityMapperStub.toEntity.restore() - RedisRepositoryStub.delete.restore() + RedisRepositoryStub.clear.restore() }) it('should throw NotFound on patch entity', async function () { @@ -555,7 +581,7 @@ describe('BaseRepository', function () { sinon.stub(SequelizeModelStub, 'update').callsFake(() => [0]) sinon.spy(baseRepository, '_getNotFoundError') sinon.spy(EntityMapperStub, 'toEntity') - sinon.spy(RedisRepositoryStub, 'delete') + sinon.spy(RedisRepositoryStub, 'clear') baseRepository.init({ modelName: 'modelName', @@ -578,7 +604,7 @@ describe('BaseRepository', function () { expect(SequelizeModelStub.update.getCall(0).args[1].where).to.deep.equal(where) baseRepository._getNotFoundError.should.have.been.calledOnce EntityMapperStub.toEntity.should.have.not.been.called - RedisRepositoryStub.delete.should.have.not.been.called + RedisRepositoryStub.clear.should.have.not.been.called expect(error.message).to.equal('NotFoundError') baseRepository._filterPatchFields.restore() @@ -586,7 +612,7 @@ describe('BaseRepository', function () { SequelizeModelStub.update.restore() baseRepository._getNotFoundError.restore() EntityMapperStub.toEntity.restore() - RedisRepositoryStub.delete.restore() + RedisRepositoryStub.clear.restore() }) }) @@ -625,11 +651,11 @@ describe('BaseRepository', function () { const domainEntity = EntityStub(obj) it('should delete entity', async function () { - sinon.stub(baseRepository, '_getPatchFilter').callsFake(() => where) - sinon.stub(SequelizeModelStub, 'destroy').callsFake(() => [1, dbEntry]) + sinon.stub(SequelizeModelStub, 'findOne').callsFake(() => dbEntry) + sinon.stub(SequelizeModelStub, 'destroy').callsFake(() => true) sinon.spy(baseRepository, '_getNotFoundError') sinon.stub(EntityMapperStub, 'toEntity').callsFake(() => domainEntity) - sinon.spy(RedisRepositoryStub, 'delete') + sinon.spy(RedisRepositoryStub, 'clear') baseRepository.init({ modelName: 'modelName', @@ -639,30 +665,28 @@ describe('BaseRepository', function () { const res = await baseRepository.delete(where) - baseRepository._getPatchFilter.should.have.been.calledOnce - baseRepository._getPatchFilter.should.have.been.calledWith(where) SequelizeModelStub.destroy.should.have.been.calledOnce - expect(SequelizeModelStub.destroy.getCall(0).args[0].where).to.deep.equal(where) + expect(SequelizeModelStub.destroy.getCall(0).args[0].where).to.deep.equal({ id: dbEntry.id }) baseRepository._getNotFoundError.should.have.not.been.called EntityMapperStub.toEntity.should.have.been.calledOnce EntityMapperStub.toEntity.should.have.been.calledWith(obj) - RedisRepositoryStub.delete.should.have.been.calledOnce - RedisRepositoryStub.delete.should.have.been.calledWith(obj.id) + RedisRepositoryStub.clear.should.have.been.calledOnce + RedisRepositoryStub.clear.should.have.been.calledWith(obj) expect(res).to.deep.equal(domainEntity) - baseRepository._getPatchFilter.restore() SequelizeModelStub.destroy.restore() + SequelizeModelStub.findOne.restore() baseRepository._getNotFoundError.restore() EntityMapperStub.toEntity.restore() - RedisRepositoryStub.delete.restore() + RedisRepositoryStub.clear.restore() }) it('should delete entity (cacheDisabled=true)', async function () { - sinon.stub(baseRepository, '_getPatchFilter').callsFake(() => where) + sinon.stub(SequelizeModelStub, 'findOne').callsFake(() => dbEntry) sinon.stub(SequelizeModelStub, 'destroy').callsFake(() => [1, dbEntry]) sinon.spy(baseRepository, '_getNotFoundError') sinon.stub(EntityMapperStub, 'toEntity').callsFake(() => domainEntity) - sinon.spy(RedisRepositoryStub, 'delete') + sinon.spy(RedisRepositoryStub, 'clear') baseRepository.init({ modelName: 'modelName', @@ -673,28 +697,27 @@ describe('BaseRepository', function () { const res = await baseRepository.delete(where) - baseRepository._getPatchFilter.should.have.been.calledOnce - baseRepository._getPatchFilter.should.have.been.calledWith(where) SequelizeModelStub.destroy.should.have.been.calledOnce - expect(SequelizeModelStub.destroy.getCall(0).args[0].where).to.deep.equal(where) + expect(SequelizeModelStub.destroy.getCall(0).args[0].where).to.deep.equal({ id: dbEntry.id }) + RedisRepositoryStub.clear.should.have.been.calledWith(obj, true) baseRepository._getNotFoundError.should.have.not.been.called EntityMapperStub.toEntity.should.have.been.calledOnce EntityMapperStub.toEntity.should.have.been.calledWith(obj) expect(res).to.deep.equal(domainEntity) - baseRepository._getPatchFilter.restore() + SequelizeModelStub.findOne.restore() SequelizeModelStub.destroy.restore() baseRepository._getNotFoundError.restore() EntityMapperStub.toEntity.restore() - RedisRepositoryStub.delete.restore() + RedisRepositoryStub.clear.restore() }) - it('should throw NotFound on patch entity', async function () { - sinon.stub(baseRepository, '_getPatchFilter').callsFake(() => where) + it('should throw NotFound on delete entity', async function () { + sinon.stub(SequelizeModelStub, 'findOne').callsFake(() => false) sinon.stub(SequelizeModelStub, 'destroy').callsFake(() => [0]) sinon.spy(baseRepository, '_getNotFoundError') sinon.spy(EntityMapperStub, 'toEntity') - sinon.spy(RedisRepositoryStub, 'delete') + sinon.spy(RedisRepositoryStub, 'clear') baseRepository.init({ modelName: 'modelName', @@ -709,20 +732,19 @@ describe('BaseRepository', function () { error = e } - baseRepository._getPatchFilter.should.have.been.calledOnce - baseRepository._getPatchFilter.should.have.been.calledWith(where) - SequelizeModelStub.destroy.should.have.been.calledOnce - expect(SequelizeModelStub.destroy.getCall(0).args[0].where).to.deep.equal(where) + SequelizeModelStub.findOne.should.have.been.calledOnce + SequelizeModelStub.destroy.should.have.not.been.calledOnce + expect(SequelizeModelStub.findOne.getCall(0).args[0].where).to.deep.equal(where) baseRepository._getNotFoundError.should.have.been.calledOnce EntityMapperStub.toEntity.should.have.not.been.called - RedisRepositoryStub.delete.should.have.not.been.called + RedisRepositoryStub.clear.should.have.not.been.called expect(error.message).to.deep.equal('NotFoundError') - baseRepository._getPatchFilter.restore() + SequelizeModelStub.findOne.restore() SequelizeModelStub.destroy.restore() baseRepository._getNotFoundError.restore() EntityMapperStub.toEntity.restore() - RedisRepositoryStub.delete.restore() + RedisRepositoryStub.clear.restore() }) })