@wepublish/api-db-mongodb
Version:
We.publish Database adapter for mongoDB
452 lines (388 loc) • 12.8 kB
text/typescript
import {
ConnectionResult,
CreateSubscriptionArgs,
CreateSubscriptionPeriodArgs,
DBSubscriptionAdapter,
DeleteSubscriptionArgs,
DeleteSubscriptionPeriodArgs,
GetSubscriptionArgs,
InputCursorType,
LimitType,
OptionalSubscription,
SortOrder,
Subscription,
SubscriptionSort,
UpdateSubscriptionArgs
} from '@wepublish/api'
import {Collection, Db, FilterQuery, MongoCountPreferences} from 'mongodb'
import {CollectionName, DBSubscription} from './schema'
import {MaxResultsPerPage} from './defaults'
import {Cursor} from './cursor'
import nanoid from 'nanoid'
import {mapDateFilterComparisonToMongoQueryOperatior} from './utility'
export class MongoDBSubscriptionAdapter implements DBSubscriptionAdapter {
private subscriptions: Collection<DBSubscription>
private locale: string
constructor(db: Db, locale: string) {
this.subscriptions = db.collection(CollectionName.Subscriptions)
this.locale = locale
}
async createSubscription({input}: CreateSubscriptionArgs): Promise<OptionalSubscription> {
const {ops} = await this.subscriptions.insertOne({
createdAt: new Date(),
modifiedAt: new Date(),
userID: input.userID,
memberPlanID: input.memberPlanID,
paymentMethodID: input.paymentMethodID,
monthlyAmount: input.monthlyAmount,
autoRenew: input.autoRenew,
startsAt: input.startsAt,
paymentPeriodicity: input.paymentPeriodicity,
properties: input.properties,
deactivation: input.deactivation,
paidUntil: input.paidUntil,
periods: []
})
const {_id: id, ...data} = ops[0]
return {id, ...data}
}
async updateSubscription({id, input}: UpdateSubscriptionArgs): Promise<OptionalSubscription> {
const {value} = await this.subscriptions.findOneAndUpdate(
{_id: id},
{
$set: {
modifiedAt: new Date(),
userID: input.userID,
memberPlanID: input.memberPlanID,
paymentMethodID: input.paymentMethodID,
monthlyAmount: input.monthlyAmount,
autoRenew: input.autoRenew,
startsAt: input.startsAt,
paymentPeriodicity: input.paymentPeriodicity,
properties: input.properties,
deactivation: input.deactivation,
paidUntil: input.paidUntil
}
},
{returnOriginal: false}
)
if (!value) return null
const {_id: outID, ...data} = value
return {id: outID, ...data}
}
async updateUserID(subscriptionID: string, userID: string): Promise<OptionalSubscription> {
const {value} = await this.subscriptions.findOneAndUpdate(
{_id: subscriptionID},
{
$set: {
modifiedAt: new Date(),
userID
}
}
)
if (!value) return null
return await this.subscriptions.findOne({_id: subscriptionID})
}
async deleteSubscription({id}: DeleteSubscriptionArgs): Promise<string | null> {
const {deletedCount} = await this.subscriptions.deleteOne({_id: id})
return deletedCount !== 0 ? id : null
}
async addSubscriptionPeriod({
subscriptionID,
input
}: CreateSubscriptionPeriodArgs): Promise<OptionalSubscription> {
const subscription = await this.subscriptions.findOne({_id: subscriptionID})
if (!subscription) return null
const {periods = []} = subscription
periods.push({
id: nanoid(),
createdAt: new Date(),
amount: input.amount,
paymentPeriodicity: input.paymentPeriodicity,
startsAt: input.startsAt,
endsAt: input.endsAt,
invoiceID: input.invoiceID
})
const {value} = await this.subscriptions.findOneAndUpdate(
{_id: subscriptionID},
{
$set: {
modifiedAt: new Date(),
periods: periods
}
},
{returnOriginal: false}
)
if (!value) return null
const {_id: id, ...data} = value
return {id, ...data}
}
async deleteSubscriptionPeriod({
subscriptionID,
periodID
}: DeleteSubscriptionPeriodArgs): Promise<OptionalSubscription> {
const subscription = await this.subscriptions.findOne({_id: subscriptionID})
if (!subscription) return null
const {periods = []} = subscription
const updatedPeriods = periods.filter(period => period.id !== periodID)
const {value} = await this.subscriptions.findOneAndUpdate(
{_id: subscriptionID},
{
$set: {
modifiedAt: new Date(),
periods: updatedPeriods
}
},
{returnOriginal: false}
)
if (!value) return null
const {_id: id, ...data} = value
return {id, ...data}
}
async getSubscriptionByID(id: string): Promise<OptionalSubscription> {
const subscription = await this.subscriptions.findOne({_id: id})
return subscription ? {id: subscription._id, ...subscription} : null
}
async getSubscriptionsByID(ids: readonly string[]): Promise<OptionalSubscription[]> {
const subscriptions = await this.subscriptions.find({_id: {$in: ids}}).toArray()
const subscriptionMap = Object.fromEntries(
subscriptions.map(({_id: id, ...data}) => [id, {id, ...data}])
)
return ids.map(id => subscriptionMap[id] ?? null)
}
async getSubscriptionsByUserID(userID: string): Promise<OptionalSubscription[]> {
const subscriptions = await this.subscriptions.find({userID: {$eq: userID}}).toArray()
return subscriptions.map(({_id: id, ...data}) => ({id, ...data}))
}
async getSubscriptions({
filter,
joins,
sort,
order,
cursor,
limit
}: GetSubscriptionArgs): Promise<ConnectionResult<Subscription>> {
const limitCount = Math.min(limit.count, MaxResultsPerPage)
const sortDirection = limit.type === LimitType.First ? order : -order
const cursorData = cursor.type !== InputCursorType.None ? Cursor.from(cursor.data) : undefined
const expr =
order === SortOrder.Ascending
? cursor.type === InputCursorType.After
? '$gt'
: '$lt'
: cursor.type === InputCursorType.After
? '$lt'
: '$gt'
const sortField = subscriptionSortFieldForSort(sort)
const cursorFilter = cursorData
? {
$or: [
{[sortField]: {[expr]: cursorData.date}},
{_id: {[expr]: cursorData.id}, [sortField]: cursorData.date}
]
}
: {}
const textFilter: FilterQuery<any> = {}
if (filter && JSON.stringify(filter) !== '{}') {
textFilter.$and = []
}
// support for old filters https://github.com/wepublish/wepublish/issues/601 -->
if (filter?.startsAt !== undefined) {
const {comparison, date} = filter.startsAt
textFilter.$and?.push({
startsAt: {[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date}
})
}
if (filter?.paidUntil !== undefined) {
const {comparison, date} = filter.paidUntil
textFilter.$and?.push({
paidUntil: {
[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
}
})
}
// <-- support for old filters
if (filter?.startsAtFrom) {
const {comparison, date} = filter.startsAtFrom
textFilter.$and?.push({
startsAt: {[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date}
})
}
if (filter?.startsAtTo) {
const {comparison, date} = filter.startsAtTo
textFilter.$and?.push({
startsAt: {[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date}
})
}
if (filter?.paidUntilFrom) {
const {comparison, date} = filter.paidUntilFrom
textFilter.$and?.push({
paidUntil: {
[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
}
})
}
if (filter?.paidUntilTo) {
const {comparison, date} = filter.paidUntilTo
textFilter.$and?.push({
paidUntil: {
[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
}
})
}
if (filter?.deactivationDate !== undefined) {
const {comparison, date} = filter.deactivationDate
if (date === null) {
textFilter.$and?.push({deactivation: {$eq: null}})
} else {
textFilter.$and?.push({
'deactivation.date': {
[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
}
})
}
}
if (filter?.deactivationDateFrom !== undefined) {
const {comparison, date} = filter.deactivationDateFrom
textFilter.$and?.push({
'deactivation.date': {
[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
}
})
}
if (filter?.deactivationDateTo !== undefined) {
const {comparison, date} = filter.deactivationDateTo
textFilter.$and?.push({
'deactivation.date': {
[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
}
})
}
if (filter?.deactivationReason !== undefined) {
const reason = filter.deactivationReason
textFilter.$and?.push({
'deactivation.reason': reason
})
}
if (filter?.autoRenew !== undefined) {
textFilter.$and?.push({autoRenew: {$eq: filter.autoRenew}})
}
if (filter?.paymentPeriodicity) {
textFilter.$and?.push({paymentPeriodicity: {$eq: filter.paymentPeriodicity}})
}
if (filter?.paymentMethodID) {
textFilter.$and?.push({paymentMethodID: {$eq: filter.paymentMethodID}})
}
if (filter?.memberPlanID) {
textFilter.$and?.push({memberPlanID: {$eq: filter.memberPlanID}})
}
if (filter?.userID) {
textFilter.$and?.push({userID: {$eq: filter.userID}})
}
// join related collections
let preparedJoins: any = []
// member plan join
if (joins?.joinMemberPlan) {
preparedJoins = [
{
$lookup: {
from: CollectionName.MemberPlans,
localField: 'memberPlanID',
foreignField: '_id',
as: 'memberPlan'
}
},
{$unwind: '$memberPlan'}
]
}
// payment method join
if (joins?.joinPaymentMethod) {
preparedJoins = [
...preparedJoins,
{
$lookup: {
from: CollectionName.PaymentMethods,
localField: 'paymentMethodID',
foreignField: '_id',
as: 'paymentMethod'
}
},
{$unwind: '$paymentMethod'}
]
}
// user join
if (joins?.joinUser) {
preparedJoins = [
...preparedJoins,
{
$lookup: {
from: CollectionName.Users,
localField: 'userID',
foreignField: '_id',
as: 'user'
}
},
{$unwind: '$user'}
]
}
const [totalCount, subscriptions] = await Promise.all([
this.subscriptions.countDocuments(textFilter, {
collation: {locale: this.locale, strength: 2}
} as MongoCountPreferences), // MongoCountPreferences doesn't include collation
this.subscriptions
.aggregate([...preparedJoins], {collation: {locale: this.locale, strength: 2}})
.match(textFilter)
.match(cursorFilter)
.sort({[sortField]: sortDirection, _id: sortDirection})
.skip(limit.skip ?? 0)
.limit(limitCount + 1)
.toArray()
])
const nodes = subscriptions.slice(0, limitCount)
if (limit.type === LimitType.Last) {
nodes.reverse()
}
const hasNextPage =
limit.type === LimitType.First
? subscriptions.length > limitCount
: cursor.type === InputCursorType.Before
const hasPreviousPage =
limit.type === LimitType.Last
? subscriptions.length > limitCount
: cursor.type === InputCursorType.After
const firstUser = nodes[0]
const lastUser = nodes[nodes.length - 1]
const startCursor = firstUser
? new Cursor(firstUser._id, subscriptionDateForSort(firstUser, sort)).toString()
: null
const endCursor = lastUser
? new Cursor(lastUser._id, subscriptionDateForSort(lastUser, sort)).toString()
: null
return {
nodes: nodes.map<Subscription>(({_id: id, ...data}) => ({id, ...data})),
pageInfo: {
startCursor,
endCursor,
hasNextPage,
hasPreviousPage
},
totalCount
}
}
}
function subscriptionSortFieldForSort(sort: SubscriptionSort) {
switch (sort) {
case SubscriptionSort.CreatedAt:
return 'createdAt'
case SubscriptionSort.ModifiedAt:
return 'modifiedAt'
}
}
function subscriptionDateForSort(subscription: DBSubscription, sort: SubscriptionSort): Date {
switch (sort) {
case SubscriptionSort.CreatedAt:
return subscription.createdAt
case SubscriptionSort.ModifiedAt:
return subscription.modifiedAt
}
}