MongoDB query plugin and repository API for in-memory object collections
Getting Started
Installation
Usage
Built With
Contributing
MongoDB query plugin and repository API for in-memory object collections.
- run aggregation pipelines
- execute searches (with query criteria and URL queries)
- parse and convert URL query objects and strings
- perform CRUD operations on repositories
- validate collection objects
-
Create or edit an
.npmrc
file with the following information:@flex-development:registry=https://npm.pkg.github.com/
-
Add project to
dependencies
yarn add @flex-development/mango # or npm i @flex-development/mango
Configuration
Mango Finder
Mango Repository
Mango Validator
The MangoFinder
and MangoFinderAsync
plugins integrate with mingo, a
MongoDB query language for in-memory objects, to support aggregation pipelines
and executing searches.
Operators loaded by Mango can be viewed in the config file. If additional operators are needed, load them before creating a new plugin.
For shorter import paths, TypeScript users can add the following aliases:
{
"compilerOptions": {
"paths": {
"@mango": ["node_modules/@flex-development/mango/index"],
"@mango/*": ["node_modules/@flex-development/mango/*"]
}
}
}
These aliases will be used in following code examples.
The Mango Finder plugins allow users to run aggregation pipelines and execute searches against in-memory object collections. Query documents using a URL query, or search for them using a query criteria and options object.
/**
* `AbstractMangoFinder` plugin interface.
*
* Used to define class contract of `MangoFinder`, `MangoFinderAsync`, and
* possible derivatives.
*
* See:
*
* - https://github.com/kofrasa/mingo
* - https://github.com/fox1t/qs-to-mongo
*
* @template D - Document (collection object)
* @template U - Name of document uid field
* @template P - Search parameters (query criteria and options)
* @template Q - Parsed URL query object
*
* @extends IAbstractMangoFinderBase
*/
export interface IAbstractMangoFinder<
D extends ObjectPlain = ObjectUnknown,
U extends string = DUID,
P extends MangoSearchParams<D> = MangoSearchParams<D>,
Q extends MangoParsedUrlQuery<D> = MangoParsedUrlQuery<D>
> extends IAbstractMangoFinderBase<D, U> {
aggregate(
pipeline?: OneOrMany<AggregationStages<D>>
): OrPromise<AggregationPipelineResult<D>>
find(params?: P): OrPromise<DocumentPartial<D, U>[]>
findByIds(uids?: UID[], params?: P): OrPromise<DocumentPartial<D, U>[]>
findOne(uid: UID, params?: P): OrPromise<DocumentPartial<D, U> | null>
findOneOrFail(uid: UID, params?: P): OrPromise<DocumentPartial<D, U>>
query(query?: Q | string): OrPromise<DocumentPartial<D, U>[]>
queryByIds(
uids?: UID[],
query?: Q | string
): OrPromise<DocumentPartial<D, U>[]>
queryOne(
uid: UID,
query?: Q | string
): OrPromise<DocumentPartial<D, U> | null>
queryOneOrFail(uid: UID, query?: Q | string): OrPromise<DocumentPartial<D, U>>
setCache(collection?: D[]): OrPromise<MangoCacheFinder<D>>
uid(): string
}
/**
* Base `AbstractMangoFinder` plugin interface.
*
* Used to define properties of `MangoFinder`, `MangoFinderAsync`, and
* possible derivatives.
*
* @template D - Document (collection object)
* @template U - Name of document uid field
*/
export interface IAbstractMangoFinderBase<
D extends ObjectPlain = ObjectUnknown,
U extends string = DUID
> {
readonly cache: Readonly<MangoCacheFinder<D>>
readonly logger: Debugger
readonly mingo: typeof mingo
readonly mparser: IMangoParser<D>
readonly options: MangoFinderOptions<D, U>
}
A document is an object from an in-memory collection. Each document should have a unique identifier (uid).
By default, this value is assumed to map to the id
field of each document, but
can be changed via the plugin settings.
import type { MangoParsedUrlQuery, MangoSearchParams } from '@mango/types'
export interface IPerson {
email: string
first_name: string
last_name: string
}
export type PersonUID = 'email'
export type PersonParams = MangoSearchParams<IPerson>
export type PersonQuery = MangoParsedUrlQuery<IPerson>
Both the MangoFinder
and MangoFinderAsync
plugins accept an options
object
thats gets passed down to the mingo and qs-to-mongo modules.
Via the options dto, you can:
- set initial collection cache
- set uid field for each document
- set date fields and fields searchable by text
import { MangoFinder, MangoFinderAsync } from '@mango'
import type { MangoFinderOptionsDTO } from '@mango/dto'
const options: MangoFinderOptionsDTO<IPerson, PersonUID> = {
cache: {
collection: [
{
email: '[email protected]',
first_name: 'Nate',
last_name: 'Maxstead'
},
{
email: '[email protected]',
first_name: 'Roland',
last_name: 'Brisseau'
},
{
email: '[email protected]',
first_name: 'Kippar',
last_name: 'Smidmoor'
},
{
email: '[email protected]',
first_name: 'Godfree',
last_name: 'Durnford'
},
{
email: '[email protected]',
first_name: 'Madelle',
last_name: 'Fauguel'
}
]
},
mingo: { idKey: 'email' },
parser: {
fullTextFields: ['first_name', 'last_name']
}
}
export const PeopleFinder = new MangoFinder<IPerson, PersonUID>(options)
export const PeopleFinderA = new MangoFinderAsync<IPerson, PersonUID>(options)
Note: All properties are optional.
To learn more about qs-to-mongo options, see Options from the package
documentation. Note that the objectIdFields
and parameters
options are not
accepted by the MangoParser
.
The Mango Repositories extend the Mango Finder plugins and allow users to perform write operations on an object collection.
/**
* `AbstractMangoRepository` class interface.
*
* Used to define class contract of `MangoRepository`, `MangoRepositoryAsync`,
* and possible derivatives.
*
* @template E - Entity
* @template U - Name of entity uid field
* @template P - Repository search parameters (query criteria and options)
* @template Q - Parsed URL query object
*
* @extends IAbstractMangoFinder
* @extends IAbstractMangoRepositoryBase
*/
export interface IAbstractMangoRepository<
E extends ObjectPlain = ObjectUnknown,
U extends string = DUID,
P extends MangoSearchParams<E> = MangoSearchParams<E>,
Q extends MangoParsedUrlQuery<E> = MangoParsedUrlQuery<E>
> extends Omit<IAbstractMangoFinder<E, U, P, Q>, 'cache' | 'options'>,
IAbstractMangoRepositoryBase<E, U> {
clear(): OrPromise<boolean>
create(dto: CreateEntityDTO<E>): OrPromise<E>
delete(uid?: OneOrMany<UID>, should_exist?: boolean): OrPromise<UID[]>
patch(uid: UID, dto?: PatchEntityDTO<E>, rfields?: string[]): OrPromise<E>
setCache(collection?: E[]): OrPromise<MangoCacheRepo<E>>
save(dto?: OneOrMany<EntityDTO<E>>): OrPromise<E[]>
}
/**
* Base `AbstractMangoRepository` class interface.
*
* Used to define properties of `MangoRepository`, `MangoRepositoryAsync`,
* and possible derivatives.
*
* @template E - Entity
* @template U - Name of entity uid field
*
* @extends IAbstractMangoFinderBase
*/
export interface IAbstractMangoRepositoryBase<
E extends ObjectPlain = ObjectUnknown,
U extends string = DUID
> extends IAbstractMangoFinderBase<E, U> {
readonly cache: MangoCacheRepo<E>
readonly options: MangoRepoOptions<E, U>
readonly validator: IMangoValidator<E>
}
Before creating a new repository, a model needs to be created.
For the next set of examples, the model User
will be used.
import { IsStrongPassword, IsUnixTimestamp } from '@mango/decorators'
import type { MangoParsedUrlQuery, MangoSearchParams } from '@mango/types'
import {
IsEmail,
IsNotEmpty,
IsOptional,
IsPhoneNumber,
IsString
} from 'class-validator'
import type { IPerson, PersonUID } from './people'
export interface IUser extends IPerson {
created_at: number
password: string
phone?: string
updated_at?: number
}
export type UserParams = MangoSearchParams<IUser>
export type UserQuery = MangoParsedUrlQuery<IUser>
export class User implements IUser {
@IsUnixTimestamp()
created_at: IUser['created_at']
@IsEmail()
email: IUser['email']
@IsString()
@IsNotEmpty()
first_name: IUser['first_name']
@IsString()
@IsNotEmpty()
last_name: IUser['last_name']
@IsStrongPassword()
password: IUser['password']
@IsOptional()
@IsPhoneNumber()
phone?: IUser['phone']
@IsOptional()
@IsUnixTimestamp()
updated_at: IUser['updated_at']
}
For more information about validation decorators, see the class-validator package.
Mango also exposes a set of custom decorators.
The MangoRepository
class accepts an options
object that gets passed down to
the MangoFinder
and MangoValidator
.
import { MangoRepository, MangoRepositoryAsync } from '@mango'
import type { MangoRepoOptionsDTO } from '@mango/dtos'
const options: MangoRepoOptionsDTO<IUser, PersonUID> = {
cache: { collection: [] },
mingo: { idKey: 'email' },
parser: {
fullTextFields: ['first_name', 'last_name']
},
validation: {
enabled: true,
transformer: {},
validator: {}
}
}
export const UsersRepo = new MangoRepository<IUser, PersonUID>(User, options)
export const UsersRepoA = new MangoRepositoryAsync<IUser, PersonUID>(
User,
options
)
See Mango Validator for more information about validation
options.
The MangoValidator
mixin allows for decorator-based model validation.
Under the hood, it uses class-transformer-validator.
/**
* `MangoValidator` mixin interface.
*
* @template E - Entity
*/
export interface IMangoValidator<E extends ObjectPlain = ObjectUnknown> {
readonly enabled: boolean
readonly model: ClassType<E>
readonly model_name: string
readonly tvo: Omit<MangoValidatorOptions, 'enabled'>
readonly validator: typeof transformAndValidate
readonly validatorSync: typeof transformAndValidateSync
check<V extends unknown = ObjectPlain>(value?: V): Promise<E | V>
checkSync<V extends unknown = ObjectPlain>(value?: V): E | V
handleError(error: Error | ValidationError[]): Exception
}
Each repository has it owns validator, but the validator can be used standalone as well.
import { MangoValidator } from '@mango'
import type { MangoValidatorOptions } from '@mango/types'
const options: MangoValidatorOptions = {
transformer: {},
validator: {}
}
export const UsersValidator = new MangoValidator<IUser>(User, options)
Validation options will be merged with the following object:
import type { TVODefaults } from '@mango/types'
/**
* @property {TVODefaults} TVO_DEFAULTS - `class-transformer-validator` options
* @see https://github.com/MichalLytek/class-transformer-validator
*/
export const TVO_DEFAULTS: TVODefaults = Object.freeze({
transformer: {},
validator: {
enableDebugMessages: true,
forbidNonWhitelisted: true,
stopAtFirstError: false,
validationError: { target: false, value: true },
whitelist: true
}
})
- class-transformer-validator - Plugin for class-transformer and class-validator
- debug - Debugging utility
- mingo - MongoDB query language for in-memory objects
- qs-to-mongo - Parse and convert URL queries into MongoDB query criteria and options
- uuid - Generate RFC-compliant UUIDs