@wepublish/api-db-mongodb
Version:
We.publish Database adapter for mongoDB
227 lines (190 loc) • 6.01 kB
text/typescript
import {
DBImageAdapter,
CreateImageArgs,
OptionalImage,
UpdateImageArgs,
DeleteImageArgs,
Image,
ConnectionResult,
GetImagesArgs,
InputCursorType,
LimitType,
SortOrder,
ImageSort
} from '@wepublish/api'
import {Collection, Db, FilterQuery, MongoCountPreferences} from 'mongodb'
import {CollectionName, DBImage} from './schema'
import {MaxResultsPerPage} from './defaults'
import {Cursor} from './cursor'
import {escapeRegExp} from '../utility'
export class MongoDBImageAdapter implements DBImageAdapter {
private images: Collection<DBImage>
private locale: string
constructor(db: Db, locale: string) {
this.images = db.collection(CollectionName.Images)
this.locale = locale
}
async createImage({id, input}: CreateImageArgs): Promise<OptionalImage> {
const {ops} = await this.images.insertOne({
_id: id,
createdAt: new Date(),
modifiedAt: new Date(),
fileSize: input.fileSize,
extension: input.extension,
mimeType: input.mimeType,
format: input.format,
width: input.width,
height: input.height,
filename: input.filename,
title: input.title,
description: input.description,
tags: input.tags,
source: input.source,
link: input.link,
license: input.license,
focalPoint: input.focalPoint
})
const {_id: outID, ...image} = ops[0]
return {id: outID, ...image}
}
async updateImage({id, input}: UpdateImageArgs): Promise<OptionalImage> {
const {value} = await this.images.findOneAndUpdate(
{_id: id},
[
{
$set: {
modifiedAt: new Date(),
filename: input.filename,
title: input.title,
description: input.description,
tags: input.tags,
source: input.source,
link: input.link,
license: input.license,
focalPoint: input.focalPoint
}
}
] as any,
{returnOriginal: false}
)
if (!value) return null
const {_id: outID, ...image} = value
return {id: outID, ...image}
}
async deleteImage({id}: DeleteImageArgs): Promise<boolean | null> {
const {deletedCount} = await this.images.deleteOne({_id: id})
return deletedCount !== 0 ? true : null
}
async getImagesByID(ids: readonly string[]): Promise<OptionalImage[]> {
const images = await this.images.find({_id: {$in: ids}}).toArray()
const imageMap = Object.fromEntries(
images.map(({_id: id, ...article}) => [id, {id, ...article}])
)
return ids.map(id => imageMap[id] ?? null)
}
async getImages({
filter,
sort,
order,
cursor,
limit
}: GetImagesArgs): Promise<ConnectionResult<Image>> {
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 = imageSortFieldForSort(sort)
const cursorFilter = cursorData
? {
$or: [
{[sortField]: {[expr]: cursorData.date}},
{_id: {[expr]: cursorData.id}, [sortField]: cursorData.date}
]
}
: {}
let textFilter: FilterQuery<any> = {}
let metaFilters: FilterQuery<any> = []
// TODO: Rename to search
if (filter?.title != undefined) {
textFilter['$or'] = [
{title: {$regex: escapeRegExp(filter.title), $options: 'i'}},
{filename: {$regex: escapeRegExp(filter.title), $options: 'i'}}
]
}
if (filter?.tags) {
metaFilters.push({tags: {$in: filter.tags}})
}
const [totalCount, images] = await Promise.all([
this.images.countDocuments(
{$and: [metaFilters.length ? {$and: metaFilters} : {}, textFilter]} as any,
{collation: {locale: this.locale, strength: 2}} as MongoCountPreferences
), // MongoCountPreferences doesn't include collation
this.images
.aggregate([], {collation: {locale: this.locale, strength: 2}})
.match(metaFilters.length ? {$and: metaFilters} : {})
.match(textFilter)
.match(cursorFilter)
.sort({[sortField]: sortDirection, _id: sortDirection})
.skip(limit.skip ?? 0)
.limit(limitCount + 1)
.toArray()
])
const nodes = images.slice(0, limitCount)
if (limit.type === LimitType.Last) {
nodes.reverse()
}
const hasNextPage =
limit.type === LimitType.First
? images.length > limitCount
: cursor.type === InputCursorType.Before
? true
: false
const hasPreviousPage =
limit.type === LimitType.Last
? images.length > limitCount
: cursor.type === InputCursorType.After
? true
: false
const firstImage = nodes[0]
const lastImage = nodes[nodes.length - 1]
const startCursor = firstImage
? new Cursor(firstImage._id, imageDateForSort(firstImage, sort)).toString()
: null
const endCursor = lastImage
? new Cursor(lastImage._id, imageDateForSort(lastImage, sort)).toString()
: null
return {
nodes: nodes.map<Image>(({_id: id, ...image}) => ({id, ...image})),
pageInfo: {
startCursor,
endCursor,
hasNextPage,
hasPreviousPage
},
totalCount
}
}
}
function imageSortFieldForSort(sort: ImageSort) {
switch (sort) {
case ImageSort.CreatedAt:
return 'createdAt'
case ImageSort.ModifiedAt:
return 'modifiedAt'
}
}
function imageDateForSort(image: DBImage, sort: ImageSort): Date {
switch (sort) {
case ImageSort.CreatedAt:
return image.createdAt
case ImageSort.ModifiedAt:
return image.modifiedAt
}
}