UNPKG

@chevre/domain

Version:

Chevre Domain Library for Node.js

678 lines (677 loc) 31.7 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.PendingReservationRepo = void 0; const createDebug = require("debug"); const errorHandler_1 = require("../errorHandler"); const factory = require("../factory"); const settings_1 = require("../settings"); const pendingReservation_1 = require("./mongoose/schemas/pendingReservation"); const debug = createDebug('chevre-domain:repo:pendingReservation'); /** * 保留予約リポジトリ */ class PendingReservationRepo { constructor(connection) { // this.aggregateReservationModel = connection.model(modelName, createSchema()); this.pendingReservationModel = connection.model(pendingReservation_1.modelName, (0, pendingReservation_1.createSchema)()); } static CREATE_FILTER_QUERY(params) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; const filterQueries = [ // { numSeats: { $gte: 1 } } ]; const projectIdEq = (_b = (_a = params.project) === null || _a === void 0 ? void 0 : _a.id) === null || _b === void 0 ? void 0 : _b.$eq; if (typeof projectIdEq === 'string') { filterQueries.push({ 'project.id': { $eq: projectIdEq } }); } const providerIdEq = (_d = (_c = params.provider) === null || _c === void 0 ? void 0 : _c.id) === null || _d === void 0 ? void 0 : _d.$eq; if (typeof providerIdEq === 'string') { filterQueries.push({ 'provider.id': { $eq: providerIdEq } }); } const reservationForIdEq = (_f = (_e = params.reservationFor) === null || _e === void 0 ? void 0 : _e.id) === null || _f === void 0 ? void 0 : _f.$eq; if (typeof reservationForIdEq === 'string') { filterQueries.push({ 'reservationFor.id': { $eq: reservationForIdEq } }); } const reservationNumberEq = (_g = params.reservationNumber) === null || _g === void 0 ? void 0 : _g.$eq; if (typeof reservationNumberEq === 'string') { filterQueries.push({ reservationNumber: { $eq: reservationNumberEq } }); } const subReservationIdentifierEq = (_j = (_h = params.subReservation) === null || _h === void 0 ? void 0 : _h.identifier) === null || _j === void 0 ? void 0 : _j.$eq; if (typeof subReservationIdentifierEq === 'string') { filterQueries.push({ 'subReservation.identifier': { $eq: subReservationIdentifierEq } }); } return filterQueries; } static offer2identifier(params, hasTicketedSeat) { var _a, _b; if (hasTicketedSeat) { return `${params.seatSection}:${params.seatNumber}`; } else { // 予約IDをfieldにする場合 const serviceOutputId = (_b = (_a = params.itemOffered) === null || _a === void 0 ? void 0 : _a.serviceOutput) === null || _b === void 0 ? void 0 : _b.id; if (typeof serviceOutputId === 'string' && serviceOutputId !== '') { return serviceOutputId; } else { throw new factory.errors.Internal('offer2identifier requires itemOffered.serviceOutput.id'); } } } // private static lockKey2aggregateReservation(lockKey: ILockKey): IAggregateReservation { // const { eventId, startDate, holder, offers, expires, hasTicketedSeat } = lockKey; // if (!(startDate instanceof Date)) { // throw new factory.errors.Argument('startDate', 'must be Date'); // } // const reservations = offers.map((offer) => { // const reservationIdentifier = PendingReservationRepo.offer2identifier(offer, hasTicketedSeat); // const reservationId = `${holder}:${reservationIdentifier}`; // return { // identifier: reservationIdentifier, // reservationId // }; // }); // // check uniqueness // const reservationIdentifiers = reservations.map(({ identifier }) => identifier); // const uniqueIdentifiers = [...new Set(reservationIdentifiers)]; // if (uniqueIdentifiers.length !== reservationIdentifiers.length) { // throw new factory.errors.Argument('offers', 'offers must be unique'); // } // return { // project: { id: lockKey.project.id, typeOf: factory.organizationType.Project }, // typeOf: 'AggregateReservation', // expires, // reservationCount: 0, // reservationFor: { id: eventId, startDate }, // reservationIds: reservations.map(({ reservationId }) => reservationId) // }; // } static lockKey2reservationPackage(lockKey) { const { eventId, startDate, holder, offers, expires, hasTicketedSeat, bookingTime } = lockKey; if (!(startDate instanceof Date)) { throw new factory.errors.Argument('startDate', 'must be Date'); } const subReservation = offers.map((offer) => { return { identifier: PendingReservationRepo.offer2identifier(offer, hasTicketedSeat) }; }); return { project: { id: lockKey.project.id, typeOf: factory.organizationType.Project }, provider: { id: lockKey.provider.id, typeOf: factory.organizationType.Corporation }, typeOf: factory.reservationType.ReservationPackage, bookingTime, expires, numSeats: offers.length, reservationFor: { id: eventId, startDate }, reservationNumber: holder, subReservation }; } // public async lockIfNotLimitExceeded(lockKey: ILockKey, maximumReservationCount: number): Promise<void> { // const aggregateReservation = PendingReservationRepo.lockKey2aggregateReservation(lockKey); // const reservationPackage = PendingReservationRepo.lockKey2reservationPackage(lockKey); // // まずcreate document // await this.createIfNotExist(aggregateReservation); // await this.increaseReservationCount(aggregateReservation, { maximumReservationCount }); // try { // await this.createReservationPackageIfPossible(reservationPackage); // } catch (error) { // await this.decreaseReservationCountByLockKey(lockKey); // throw error; // } // } // public async lock(lockKey: ILockKey): Promise<void> { // const aggregateReservation = PendingReservationRepo.lockKey2aggregateReservation(lockKey); // const reservationPackage = PendingReservationRepo.lockKey2reservationPackage(lockKey); // // まずcreate document // await this.createIfNotExist(aggregateReservation); // await this.increaseReservationCount(aggregateReservation, {}); // try { // await this.createReservationPackageIfPossible(reservationPackage); // } catch (error) { // await this.decreaseReservationCountByLockKey(lockKey); // throw error; // } // } lockIfNotLimitExceeded(lockKey, maximumReservationCount) { return __awaiter(this, void 0, void 0, function* () { const { eventId } = lockKey; const reservationPackage = PendingReservationRepo.lockKey2reservationPackage(lockKey); // lock前でもcapacity確認 // dateCreated < bookingTimeの集計 const currentReservationCountBeforeLock = yield this.aggregateNumSeats({ bookingTime: { $lte: reservationPackage.bookingTime }, dateCreated: { $lt: reservationPackage.bookingTime }, reservationFor: { id: eventId }, limit: maximumReservationCount }); const remainingAttendeeCapacity = maximumReservationCount - currentReservationCountBeforeLock; if (remainingAttendeeCapacity <= 0) { throw new factory.errors.Argument('Event', 'maximumAttendeeCapacity exceeded'); } yield this.createReservationPackageIfPossible(reservationPackage); try { // dateCreated >= bookingTimeの集計 const reservationCountAfterAggregate = yield this.aggregateNumSeats({ bookingTime: { $lte: reservationPackage.bookingTime }, dateCreated: { $gte: reservationPackage.bookingTime }, reservationFor: { id: eventId }, limit: remainingAttendeeCapacity }); // console.log('reservationCountAfterAggregate:', reservationCountAfterAggregate, remainingAttendeeCapacity); if (reservationCountAfterAggregate > remainingAttendeeCapacity) { throw new factory.errors.Argument('Event', 'maximumAttendeeCapacity exceeded'); } } catch (error) { // 最大数超過であればreservationPackageを削除 yield this.deleteReservationPackage(reservationPackage); throw error; } }); } lock(lockKey) { return __awaiter(this, void 0, void 0, function* () { const reservationPackage = PendingReservationRepo.lockKey2reservationPackage(lockKey); yield this.createReservationPackageIfPossible(reservationPackage); }); } // public async unlock(params: IUnlockKey): Promise<void> { // await this.decreaseReservationCount(params); // await this.deleteReservationIfExists(params); // } unlock(params) { return __awaiter(this, void 0, void 0, function* () { yield this.deleteReservationIfExists(params); }); } // public async countUnavailableOffers(params: { // event: { // id: string; // startDate: Date; // hasTicketedSeat: boolean; // }; // }): Promise<number> { // const { event } = params; // const doc = await this.aggregateReservationModel.findOne( // { 'reservationFor.id': { $eq: event.id } }, // { // _id: 0, // reservationCount: 1 // } // ) // .setOptions({ maxTimeMS: MONGO_MAX_TIME_MS }) // .lean<Pick<IAggregateReservation, 'reservationCount'>>() // .exec(); // return (doc !== null) ? doc.reservationCount : 0; // } /** * 現時点での保留予約数を集計する */ countUnavailableOffers(params) { return __awaiter(this, void 0, void 0, function* () { const { event } = params; return this.aggregateNumSeats({ bookingTime: { $lte: new Date() }, // 指定有無での性能は対して変わらないか(2025-04-25) reservationFor: { id: event.id } }); }); } getHolder(params) { return __awaiter(this, void 0, void 0, function* () { const { eventId, offer, hasTicketedSeat } = params; const reservationIdentifier = PendingReservationRepo.offer2identifier(offer, hasTicketedSeat); const doc = yield this.pendingReservationModel.findOne({ 'reservationFor.id': { $eq: eventId }, 'subReservation.identifier': { $exists: true, $eq: reservationIdentifier } }, { _id: 0, reservationNumber: 1 }) .setOptions({ maxTimeMS: settings_1.MONGO_MAX_TIME_MS }) .lean() .exec(); return (doc !== null) ? doc.reservationNumber : undefined; }); } searchHolders(params) { return __awaiter(this, void 0, void 0, function* () { const { eventId, offers, hasTicketedSeat } = params; const reservationIdentifiers = offers.map((offer) => PendingReservationRepo.offer2identifier(offer, hasTicketedSeat)); const aggregate = this.pendingReservationModel.aggregate([ // unwind,matchの順序 // unwind->matchでは遅い? // match->unwind->matchにする { $match: { 'reservationFor.id': { $eq: eventId }, 'subReservation.identifier': { $exists: true, $in: reservationIdentifiers } } }, { $unwind: { path: '$subReservation' } }, { $match: { // 'subReservation.identifier': { $exists: true, $in: reservationIdentifiers } 'subReservation.identifier': { $in: reservationIdentifiers } // $exists不要か? } }, // unwind後にもmatchがないと、reservationIdentifiers以外のsubReservationもprojectされる { $project: { _id: 0, // reservationNumber: '$reservationNumber', identifier: '$subReservation.identifier' } } ]); const docs = yield aggregate .option({ maxTimeMS: settings_1.MONGO_MAX_TIME_MS }) .exec(); debug('searchHolders: aggregated.', docs, docs.length, 'docs'); // reservationNumberを正確に返す必要はなく、存在しているかどうかだけ分かればよい(stringを返せばよい) return reservationIdentifiers.map((reservationIdentifier) => { const doc = docs.find(({ identifier }) => identifier === reservationIdentifier); // tslint:disable-next-line:no-null-keyword // return (doc !== undefined) ? doc.reservationNumber : null; // tslint:disable-next-line:no-null-keyword return (doc !== undefined) ? doc.identifier : null; }); }); } // public async searchHolders2(params: { // project: { id: string }; // eventId: string; // startDate: Date; // hasTicketedSeat: boolean; // offers: IOffer[]; // }): Promise<IGetHolderResult[]> { // const { eventId, offers, hasTicketedSeat } = params; // const reservationIdentifiers = offers.map((offer) => PendingReservationRepo.offer2identifier(offer, hasTicketedSeat)); // const aggregate = this.pendingReservationModel.aggregate<{ // // reservationNumber: string; // identifier: string; // }>([ // // unwind,matchの順序 // // unwind->matchでは遅い? // // match->limit->unwind->matchにする // { // $unwind: { // path: '$subReservation' // } // }, // { // $match: { // 'reservationFor.id': { $eq: eventId }, // 'subReservation.identifier': { $exists: true, $in: reservationIdentifiers } // } // }, // { $limit: reservationIdentifiers.length }, // { // $project: { // _id: 0, // // reservationNumber: '$reservationNumber', // identifier: '$subReservation.identifier' // } // } // ]); // const docs = await aggregate // .option({ maxTimeMS: MONGO_MAX_TIME_MS }) // .exec(); // debug('searchHolders: aggregated.', docs, docs.length, 'docs'); // // reservationNumberを正確に返す必要はなく、存在しているかどうかだけ分かればよい(stringを返せばよい) // return reservationIdentifiers.map((reservationIdentifier) => { // const doc = docs.find(({ identifier }) => identifier === reservationIdentifier); // // tslint:disable-next-line:no-null-keyword // // return (doc !== undefined) ? doc.reservationNumber : null; // // tslint:disable-next-line:no-null-keyword // return (doc !== undefined) ? doc.identifier : null; // }); // } // public async searchHoldersByDistinct(params: { // project: { id: string }; // eventId: string; // startDate: Date; // hasTicketedSeat: boolean; // offers: IOffer[]; // }) { // const { eventId, offers, hasTicketedSeat } = params; // const reservationIdentifiers = offers.map((offer) => PendingReservationRepo.offer2identifier(offer, hasTicketedSeat)); // const doc = await this.pendingReservationModel.distinct( // 'subReservation.identifier', // { // 'reservationFor.id': { $eq: eventId }, // 'subReservation.identifier': { $exists: true, $in: reservationIdentifiers } // } // ) // .setOptions({ maxTimeMS: MONGO_MAX_TIME_MS }) // .exec(); // debug('searchHolders: distinct.', doc); // return doc; // } // public async getSize(params: Omit<IUnlockKey, 'holder' | 'offer'>) { // const { eventId } = params; // const aggregate = this.aggregateReservationModel.aggregate([ // { // $match: { // 'reservationFor.id': { $eq: eventId } // } // }, // { // $project: { // typeOf: 1, // objectSize: { $bsonSize: '$$ROOT' } // } // } // ]); // return aggregate // .option({ maxTimeMS: MONGO_MAX_TIME_MS }) // .exec(); // } // public getCursor(conditions: FilterQuery<any>, projection: any) { // return this.aggregateReservationModel.find(conditions, projection) // .sort({ bookingTime: factory.sortType.Ascending }) // .cursor(); // } docExists(params) { return __awaiter(this, void 0, void 0, function* () { const { eventId } = params; const doc = yield this.pendingReservationModel.findOne({ 'reservationFor.id': { $eq: eventId } }, { _id: 0, typeOf: 1 }) .setOptions({ maxTimeMS: settings_1.MONGO_MAX_TIME_MS }) .lean() .exec(); return doc !== null; }); } deleteExpiredMany(params) { return __awaiter(this, void 0, void 0, function* () { const { expires } = params; return this.pendingReservationModel.deleteMany({ expires: { $lte: expires.$lte } }) .exec(); }); } projectFields(params) { return __awaiter(this, void 0, void 0, function* () { const { limit, page } = params; const filterQueries = PendingReservationRepo.CREATE_FILTER_QUERY(params); const matchStage = (filterQueries.length > 0) ? { $match: { $and: filterQueries } } : undefined; let limitStage; let skipStage; if (typeof limit === 'number' && limit > 0) { const pageMustBePositive = (typeof page === 'number' && page > 0) ? page : 1; skipStage = { $skip: limit * (pageMustBePositive - 1) }; limitStage = { $limit: limit }; } const projectStage = { $project: { _id: 0, typeOf: '$typeOf', bookingTime: '$bookingTime', dateCreated: '$dateCreated', dateModified: '$dateModified', expires: '$expires', numSeats: '$numSeats', reservationFor: '$reservationFor', reservationNumber: '$reservationNumber', subReservationCount: { $size: '$subReservation' } } }; const pipeline = [ ...(matchStage !== undefined) ? [matchStage] : [], { $sort: { bookingTime: factory.sortType.Descending } }, ...(skipStage !== undefined) ? [skipStage] : [], ...(limitStage !== undefined) ? [limitStage] : [], projectStage ]; return this.pendingReservationModel.aggregate(pipeline) .option({ maxTimeMS: settings_1.MONGO_MAX_TIME_MS }) .exec(); }); } projectSubReservationByReservationNumber(params) { return __awaiter(this, void 0, void 0, function* () { const matchStage = { $match: { 'project.id': { $eq: params.project.id.$eq }, reservationNumber: { $eq: params.reservationNumber.$eq } } }; const projectStage = { $project: { _id: 0, identifier: '$subReservation.identifier', index: `$reservationIndex` } }; const unwindStage = { $unwind: { path: '$subReservation', includeArrayIndex: 'reservationIndex' } }; const pipeline = [ matchStage, unwindStage, projectStage ]; return this.pendingReservationModel.aggregate(pipeline) .option({ maxTimeMS: settings_1.MONGO_MAX_TIME_MS }) .exec(); }); } /** * expiresを最新の情報に同期する */ syncEvent2expires(params) { return __awaiter(this, void 0, void 0, function* () { const { expires, reservationFor } = params; if (!(expires instanceof Date)) { throw new factory.errors.Argument('expires', 'must be Date'); } return this.pendingReservationModel.updateMany({ 'reservationFor.id': { $eq: reservationFor.id }, numSeats: { $gte: 1 } // numSeats:0についてはもはや不要なドキュメントなので除外 }, { $set: { expires } }) .exec(); }); } aggregateNumSeats(params) { return __awaiter(this, void 0, void 0, function* () { const { bookingTime, dateCreated, reservationFor, limit } = params; const bookingTimeLte = bookingTime === null || bookingTime === void 0 ? void 0 : bookingTime.$lte; const dateCreatedGte = dateCreated === null || dateCreated === void 0 ? void 0 : dateCreated.$gte; const dateCreatedLt = dateCreated === null || dateCreated === void 0 ? void 0 : dateCreated.$lt; const matchStage = { $match: Object.assign(Object.assign({ 'reservationFor.id': { $eq: reservationFor.id }, // 'subReservation.identifier': { $exists: true }, numSeats: { $gte: 1 } }, (bookingTimeLte instanceof Date) ? { bookingTime: { $lte: bookingTimeLte } } : undefined), (dateCreatedGte instanceof Date || dateCreatedLt instanceof Date) ? { dateCreated: Object.assign(Object.assign({}, (dateCreatedGte instanceof Date) ? { $gte: dateCreatedGte } : undefined), (dateCreatedLt instanceof Date) ? { $lt: dateCreatedLt } : undefined) } : undefined) }; let limitStage; if (typeof limit === 'number' && limit > 0) { limitStage = { $limit: limit }; } const aggregate = this.pendingReservationModel.aggregate([ matchStage, ...(limitStage !== undefined) ? [limitStage] : [], { $project: { numSeats: '$numSeats' } }, { $group: { // tslint:disable-next-line:no-null-keyword _id: null, totalNumSeats: { $sum: '$numSeats' } } }, { $project: { _id: 0, numSeats: '$totalNumSeats' } } ]); const aggregations = yield aggregate.option({ maxTimeMS: settings_1.MONGO_MAX_TIME_MS }) .exec(); debug('countUnavailableOffers:', aggregations); if (aggregations.length === 0) { return 0; } return aggregations[0].numSeats; }); } // private async createIfNotExist(aggregateReservation: IAggregateReservation): Promise<void> { // try { // const { expires, project, reservationFor, typeOf } = aggregateReservation; // await this.aggregateReservationModel.updateOne( // { // 'reservationFor.id': { $eq: aggregateReservation.reservationFor.id } // }, // { // $setOnInsert: { // expires, project, reservationFor, typeOf, // reservationCount: 0, reservationIds: [] // } // }, // { // upsert: true // } // ) // .exec(); // } catch (error) { // let throwsError = true; // if (await isMongoError(error)) { // // すでに存在する場合ok // if (error.code === MongoErrorCode.DuplicateKey) { // throwsError = false; // } // } // if (throwsError) { // throw error; // } // } // } createReservationPackageIfPossible(reservationPackage) { return __awaiter(this, void 0, void 0, function* () { var _a, _b; try { const result = yield this.pendingReservationModel.insertMany(Object.assign(Object.assign({}, reservationPackage), { dateCreated: new Date() }), { rawResult: true }); const id = (_b = (_a = result.insertedIds) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.toHexString(); if (typeof id !== 'string') { throw new factory.errors.Internal('pending reservationPackage not saved'); } } catch (error) { if (yield (0, errorHandler_1.isMongoError)(error)) { if (error.code === errorHandler_1.MongoErrorCode.DuplicateKey) { throw new factory.errors.AlreadyInUse(factory.reservationType.EventReservation, ['ticketedSeat'], 'Already hold'); } } } }); } deleteReservationPackage(reservationPackage) { return __awaiter(this, void 0, void 0, function* () { const { reservationNumber } = reservationPackage; yield this.pendingReservationModel.deleteOne({ reservationNumber: { $eq: reservationNumber } }) .exec(); }); } // private async decreaseReservationCountByLockKey( // lockKey: ILockKey // ): Promise<void> { // const { project, eventId, startDate, offers, hasTicketedSeat, holder } = lockKey; // for (const offer of offers) { // await this.decreaseReservationCount({ // project: { id: project.id }, // eventId, // startDate, // hasTicketedSeat, // offer, // holder // }); // } // } // private async increaseReservationCount( // aggregateReservation: IAggregateReservation, // options: { // maximumReservationCount?: number; // } // ): Promise<void> { // const { maximumReservationCount } = options; // const newReservationCount = aggregateReservation.reservationIds.length; // let reservationCountLte: number | undefined; // if (typeof maximumReservationCount === 'number') { // reservationCountLte = maximumReservationCount - newReservationCount; // if (reservationCountLte < 0) { // throw new factory.errors.Argument('Event', 'maximumAttendeeCapacity exceeded'); // } // } // const doc = await this.aggregateReservationModel.findOneAndUpdate( // { // 'reservationFor.id': { $eq: aggregateReservation.reservationFor.id }, // // 'reservations.identifier': { $nin: reservationIdentifiers }, // pendingReservationsで重複回避済の前提で実行される // ...(typeof reservationCountLte === 'number') // ? { reservationCount: { $lte: reservationCountLte } } // : undefined // // : { reservationCount: { $exists: true } } // 必ず条件内になるように // }, // { // $inc: { reservationCount: newReservationCount }, // $push: { // reservationIds: { $each: aggregateReservation.reservationIds } // }, // $set: { expires: aggregateReservation.expires } // }, // { // new: false, // trueである必要はない // projection: { typeOf: 1 } // } // ) // .setOptions({ maxTimeMS: MONGO_MAX_TIME_MS }) // .lean<Pick<IAggregateReservation, 'typeOf'>>() // .exec(); // if (doc === null) { // throw new factory.errors.Argument('Event', 'maximumAttendeeCapacity exceeded'); // } // } deleteReservationIfExists(params) { return __awaiter(this, void 0, void 0, function* () { const { eventId, offer, hasTicketedSeat, holder } = params; const reservationIdentifier = PendingReservationRepo.offer2identifier(offer, hasTicketedSeat); yield this.pendingReservationModel.updateOne({ 'reservationFor.id': { $eq: eventId }, reservationNumber: { $eq: holder }, 'subReservation.identifier': { $exists: true, $eq: reservationIdentifier } }, { $inc: { numSeats: -1 }, $pull: { subReservation: { identifier: reservationIdentifier } }, $set: { dateModified: new Date() } }) .setOptions({ maxTimeMS: settings_1.MONGO_MAX_TIME_MS }) .exec(); // debug('unlocked', pendingReservationDoc); }); } } exports.PendingReservationRepo = PendingReservationRepo;