@wepublish/api-db-mongodb
Version:
We.publish Database adapter for mongoDB
250 lines (205 loc) • 7.06 kB
text/typescript
import {
ConnectionResult,
CreateInvoiceArgs,
DBInvoiceAdapter,
DeleteInvoiceArgs,
GetInvoicesArgs,
InputCursorType,
Invoice,
InvoiceSort,
LimitType,
OptionalInvoice,
SortOrder,
UpdateInvoiceArgs
} from '@wepublish/api'
import {Collection, Db, FilterQuery, MongoCountPreferences} from 'mongodb'
import {CollectionName, DBInvoice} from './schema'
import {Cursor} from './cursor'
import {MaxResultsPerPage} from './defaults'
import {mapDateFilterComparisonToMongoQueryOperatior} from './utility'
import {escapeRegExp} from '../utility'
export class MongoDBInvoiceAdapter implements DBInvoiceAdapter {
private invoices: Collection<DBInvoice>
private locale: string
constructor(db: Db, locale: string) {
this.invoices = db.collection(CollectionName.Invoices)
this.locale = locale
}
async createInvoice({input}: CreateInvoiceArgs): Promise<Invoice> {
const {ops} = await this.invoices.insertOne({
createdAt: new Date(),
modifiedAt: new Date(),
mail: input.mail,
dueAt: input.dueAt,
subscriptionID: input.subscriptionID,
description: input.description,
paidAt: input.paidAt,
canceledAt: input.canceledAt,
sentReminderAt: input.sentReminderAt,
items: input.items
})
const {_id: id, ...invoice} = ops[0]
return {id, ...invoice}
}
async updateInvoice({id, input}: UpdateInvoiceArgs): Promise<OptionalInvoice> {
const {value} = await this.invoices.findOneAndUpdate(
{_id: id},
{
$set: {
modifiedAt: new Date(),
mail: input.mail,
dueAt: input.dueAt,
subscriptionID: input.subscriptionID,
description: input.description,
paidAt: input.paidAt,
canceledAt: input.canceledAt,
sentReminderAt: input.sentReminderAt,
items: input.items,
manuallySetAsPaidByUserId: input.manuallySetAsPaidByUserId
}
},
{returnOriginal: false}
)
if (!value) return null
const {_id: outID, ...invoice} = value
return {id: outID, ...invoice}
}
async deleteInvoice({id}: DeleteInvoiceArgs): Promise<string | null> {
const {deletedCount} = await this.invoices.deleteOne({_id: id})
return deletedCount !== 0 ? id : null
}
async getInvoiceByID(id: string): Promise<OptionalInvoice> {
const invoice = await this.invoices.findOne({_id: id})
return invoice ? {id: invoice._id, ...invoice} : null
}
async getInvoicesByID(ids: readonly string[]): Promise<OptionalInvoice[]> {
const invoices = await this.invoices.find({_id: {$in: ids}}).toArray()
const invoiceMap = Object.fromEntries(
invoices.map(({_id: id, ...invoice}) => [id, {id, ...invoice}])
)
return ids.map(id => invoiceMap[id] ?? null)
}
async getInvoicesBySubscriptionID(subscriptionID: string): Promise<OptionalInvoice[]> {
const invoices = await this.invoices.find({subscriptionID}).toArray()
return invoices.map(({_id: id, ...invoice}) => ({id, ...invoice}))
}
async getInvoices({
filter,
sort,
order,
cursor,
limit
}: GetInvoicesArgs): Promise<ConnectionResult<Invoice>> {
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 = invoiceSortFieldForSort(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 = []
}
// TODO: Rename to search
if (filter?.mail !== undefined) {
textFilter.$and?.push({mail: {$regex: escapeRegExp(filter.mail), $options: 'i'}})
}
if (filter?.paidAt !== undefined) {
const {comparison, date} = filter.paidAt
textFilter.$and?.push({
paidAt: {[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date}
})
}
if (filter?.canceledAt !== undefined) {
const {comparison, date} = filter.canceledAt
textFilter.$and?.push({
canceledAt: {[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date}
})
}
if (filter?.userID !== undefined) {
textFilter.$and?.push({userID: {$eq: filter.userID}})
}
if (filter?.subscriptionID !== undefined) {
textFilter.$and?.push({subscriptionID: {$eq: filter.subscriptionID}})
}
const [totalCount, invoices] = await Promise.all([
this.invoices.countDocuments(textFilter, {
collation: {locale: this.locale, strength: 2}
} as MongoCountPreferences), // MongoCountPreferences doesn't include collation
this.invoices
.aggregate([], {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 = invoices.slice(0, limitCount)
if (limit.type === LimitType.Last) {
nodes.reverse()
}
const hasNextPage =
limit.type === LimitType.First
? invoices.length > limitCount
: cursor.type === InputCursorType.Before
const hasPreviousPage =
limit.type === LimitType.Last
? invoices.length > limitCount
: cursor.type === InputCursorType.After
const firstInvoice = nodes[0]
const lastInvoice = nodes[nodes.length - 1]
const startCursor = firstInvoice
? new Cursor(firstInvoice._id, invoiceDateForSort(firstInvoice, sort)).toString()
: null
const endCursor = lastInvoice
? new Cursor(lastInvoice._id, invoiceDateForSort(lastInvoice, sort)).toString()
: null
return {
nodes: nodes.map<Invoice>(({_id: id, ...invoice}) => ({id, ...invoice})),
pageInfo: {
startCursor,
endCursor,
hasNextPage,
hasPreviousPage
},
totalCount
}
}
}
function invoiceSortFieldForSort(sort: InvoiceSort) {
switch (sort) {
case InvoiceSort.CreatedAt:
return 'createdAt'
case InvoiceSort.ModifiedAt:
return 'modifiedAt'
case InvoiceSort.PaidAt:
return 'paidAt'
}
}
function invoiceDateForSort(invoice: DBInvoice, sort: InvoiceSort): Date {
switch (sort) {
case InvoiceSort.CreatedAt:
return invoice.createdAt
case InvoiceSort.ModifiedAt:
return invoice.modifiedAt
case InvoiceSort.PaidAt:
return invoice.paidAt ? invoice.paidAt : invoice.createdAt
default:
return invoice.createdAt
}
}