@joktec/mongo
v0.2.38
Published
JokTec - Mongo Service
Readme
@joktec/mongo
MongoDB database package for JokTec applications.
@joktec/mongo wraps Mongoose/Typegoose with JokTec config, lifecycle, decorators, repository helpers, and shared CRUD pagination contracts from @joktec/core.
Install
yarn add @joktec/mongoPublic Surface
- module and service:
MongoModuleMongoServiceMongoRepo
- config and client:
MongoConfigMongoClient
- model contracts:
MongoSchemaIMongoRequest
IMongoPaginationResponseMongoCoverageMongoChangeStreamMongoStreamOptions- decorators and helpers:
@Schema@PropIMongoSchemaOptionsIMongoPropOptionsRefIdPopulatedRefObjectIdMongoHelperMongoPipeline- Mongo plugins
- selected Mongoose and Typegoose exports.
Module Registration
Register application models through MongoModule.forRoot(...):
import { Module } from '@joktec/core';
import { MongoModule } from '@joktec/mongo';
import { Article } from './models/article.schema';
import { User } from './models/user.schema';
@Module({
imports: [
MongoModule.forRoot({
conId: 'default',
models: [Article, User],
}),
],
})
export class RepositoryModule {}Use conId when the application config contains multiple Mongo connections.
MongoService keeps model resolution connection-aware. Repository instances receive a conId and resolve models through that connection instead of relying on the global mongoose registry.
Repository Usage
Extend MongoRepo for each application schema:
import { Injectable } from '@joktec/core';
import { MongoRepo, MongoService } from '@joktec/mongo';
import { Article } from '../models/article.schema';
@Injectable()
export class ArticleRepo extends MongoRepo<Article, string> {
constructor(mongoService: MongoService) {
super(mongoService, Article);
}
}Services can then use the shared BaseService contract:
import { BaseService, Injectable } from '@joktec/core';
import { IMongoRequest } from '@joktec/mongo';
import { Article } from '../models/article.schema';
import { ArticleRepo } from '../repositories/article.repo';
@Injectable()
export class ArticleService extends BaseService<Article, string, IMongoRequest<Article>> {
constructor(protected articleRepo: ArticleRepo) {
super(articleRepo);
}
}Config Shape
The application config reads the mongo section and maps it to MongoConfig.
Common fields:
mongo:
conId: default
host: localhost
port: 27017
username: example_user
password: example_password
database: example_db
srvMode: false
strictQuery: true
autoIndex: false
params: replicaSet=rs0&directConnection=true
options:
serverSelectionTimeoutMS: 10000uri can be used instead of host/port fields when an application needs a complete MongoDB connection string.
Connection options are merged as base defaults, then options, then query-style params. When the same key appears in both params and options, the value from params wins:
mongo:
params: authSource=app_db&replicaSet=rs0&directConnection=true&connectTimeoutMS=20000
options:
authSource: admin
connectTimeoutMS: 30000
serverSelectionTimeoutMS: 5000In this example, the final connection options use authSource=app_db and connectTimeoutMS=20000, while keeping serverSelectionTimeoutMS=5000.
For multi-process deployments, prefer enabling autoIndex in one owner process and disabling it in request-facing processes. When autoIndex is enabled, MongoService checks index drift with diffIndexes() and runs syncIndexes({ continueOnError: true }) only when Mongo reports indexes to create or drop. Sync errors are caught and logged with connection/schema context so bootstrap diagnostics identify the affected schema.
Coverage and Change Streams
MongoService.getCoverage(conId?) reports runtime capability for a Mongo connection:
- MongoDB server version.
- Mongoose package version.
- Typegoose package version.
- topology:
standalone,replica-set,sharded, orunknown. canUseSession.canUseTransaction.canUseStream.reasonswhen a capability is unavailable.
MongoService.startTransaction(...), MongoService.watch(...), and MongoRepo.watch(...) use coverage checks before opening driver sessions or MongoDB Change Streams. Unsupported topology fails immediately with framework errors such as MONGO_TRANSACTION_NOT_SUPPORTED or MONGO_STREAM_NOT_SUPPORTED.
Use watch(...) for realtime MongoDB Change Streams:
const coverage = await mongoService.getCoverage();
if (coverage.canUseStream) {
const stream = await articleRepo.watch([{ $match: { operationType: 'insert' } }]);
stream.on('change', change => {
// handle insert/update/delete events
});
}Change Streams require MongoDB replica set or sharded topology. For standalone local MongoDB, use a polling fallback in the application layer. Query cursors remain separate: MongoRepo.cursor(...) iterates large query result sets and is not realtime listening.
Query Contract
IMongoRequest<T> extends IBaseRequest<T> and adds aggregation support:
{
select?: string | Array<keyof T>;
keyword?: string;
condition?: ICondition<T>;
page?: number;
offset?: number;
cursor?: string;
cursorKey?: keyof T | Array<keyof T> | string;
limit?: number;
sort?: ISort<T>;
populate?: IPopulate<T>;
aggregations?: PipelineStage[];
}Supported repository operations include paginate, find, count, findOne, create, update, delete, restore, upsert, and bulkUpsert.
Query parsing is intentionally conservative:
idis treated as an API alias for root_idin query conditions.- ObjectId casting is schema-aware and limited to
_id, schema ObjectId paths, or explicitly configured ObjectId paths. - Repository id conditions accept string ids, JokTec
ObjectId, and native Mongoose/BSON ObjectId values. Native ObjectId values are normalized before simple conditions are converted to_idfilters. - String fields that happen to contain 24 hex characters are not cast to ObjectId unless the schema path requires it.
$like,$begin, and$endescape regex input by default to avoid accidental raw regex behavior.- Legacy casting and regex behavior are available only through explicit parser options for migration compatibility.
Pagination
MongoRepo.paginate supports page, offset, and cursor pagination through the shared @joktec/core response contracts.
Runtime priority:
- cursor when
cursororcursorKeyexists - offset when
offsetexists - page as the default fallback
Cursor pagination behavior:
- default cursor key:
_id - custom
cursorKey: supported - custom cursor keys automatically append
_idas a tie-breaker - cursor conditions are built as lexicographic Mongo
$orclauses - fetches
limit + 1documents to computehasNextPage - returns
nextCursoras an opaque token
Example first cursor request:
const firstPage = await articleRepo.paginate({
cursorKey: 'createdAt',
limit: 20,
sort: { createdAt: 'desc' },
});Example next cursor request:
const nextPage = await articleRepo.paginate({
cursor: firstPage.nextCursor,
limit: 20,
});Schema Notes
Use package decorators and base schema contracts for Mongo models. Keep app-specific query behavior inside app repositories or services, not inside @joktec/mongo.
The schema decorators wrap Typegoose, class-validator, class-transformer, and Swagger metadata so one schema class can be reused by mapped DTOs where appropriate.
import { MongoSchema, Prop, Schema } from '@joktec/mongo';
@Schema({ collection: 'users', index: ['username'] })
export class User extends MongoSchema {
@Prop({ required: true, unique: true })
username!: string;
@Prop({ type: () => [String], default: [] })
profileBadgeIds?: string[];
}Use Schema({ kind: 'embedded' }) for value objects. Embedded schemas default to _id: false and timestamps: false, and they do not register collection-level plugins or indexes:
@Schema({ kind: 'embedded' })
export class Preference {
@Prop({ required: true })
theme!: string;
}Use Schema({ kind: 'subdocument' }) when the nested document still needs its own _id and timestamps but should not become a top-level collection:
@Schema({ kind: 'subdocument' })
export class ArticleFile extends MongoSchema {
@Prop({ required: true })
url!: string;
}Prop supports explicit modes for common schema-first cases:
- omit
kindor usekind: 'normal'for persisted scalar, enum, object, array, ObjectId, and stored reference id fields. - use
kind: 'map'for raw maps/snapshots instead of passingPropType.MAPat the call site. - use
kind: 'mixed'for explicit raw Mixed payloads, including arrays of flexible provider objects. - use
kind: 'virtual', mode: 'getter'for TypeScript computed getters. - use virtual populate options such as
ref,localField, andforeignFieldfor Mongoose virtual populate fields; the wrapper inferskind: 'virtual'andmode: 'populate'.
Swagger, transform, and validation metadata are inferred from type, required, nullable, comment, example, enum, deprecated, immutable, nested, and array shape. Use swagger only as an override when the inferred metadata is not enough.
When storing raw snapshots, maps, or subdocuments, avoid relying on global id to _id conversion. The repository/helper layer should only apply API-facing id aliasing where it is safe for query semantics.
References, Populate, And Virtual Fields
Use explicit reference helper types to separate stored ids from populated instances:
import { MongoSchema, ObjectId, PopulatedRef, Prop, RefId, Schema } from '@joktec/mongo';
import { Artist } from './artist.schema';
import { User } from './user.schema';
@Schema({ collection: 'articles' })
export class Article extends MongoSchema {
@Prop({ type: ObjectId, ref: () => User })
authorId?: RefId<User>;
@Prop({ type: [ObjectId], ref: () => Artist })
artistIds?: RefId<Artist>[];
@Prop({ ref: () => User, foreignField: '_id', localField: 'authorId' })
author?: PopulatedRef<User>;
@Prop({ type: () => [Artist], ref: () => Artist, foreignField: '_id', localField: 'artistIds' })
artists?: PopulatedRef<Artist>[];
@Prop({ kind: 'virtual', mode: 'getter', comment: 'Public thumbnail URL', optional: true })
get thumbnail(): string | undefined {
return undefined;
}
@Prop({ kind: 'map', type: Object, default: null })
snapshot?: Record<string, unknown>;
@Prop({ kind: 'mixed', type: [Object], default: [] })
providerActions?: Record<string, unknown>[];
}Guidelines:
- Use
RefId<T>for persisted reference id fields such asauthorIdorartistIds; pass the raw id type only when it is not the default string id. - Use
PopulatedRef<T>andPopulatedRef<T>[]for virtual populate outputs when application code expects direct property access on populated instances. - Keep lazy resolver syntax in
@Prop({ type: () => User })or@Prop({ type: () => [User] })when the wrapper cannot infer the class fromref. - Populate-one fields can omit
typewhenrefpoints at the same class; populate arrays must still providetype: () => [Target]because runtime reflection cannot see the array element type. - Use
@Prop({ kind: 'virtual', mode: 'getter' })for computed getters that need@Exposeand Swagger metadata but must not become persisted Mongoose paths. - Use virtual populate declarations with
ref,localField, andforeignField; the wrapper infers populate mode, auto-setsjustOne: truefor non-array populated fields, and uses{}or[]as compact Swagger examples when no example is provided. - Use
@Prop({ kind: 'map' })only for Mongoose Map-shaped key/value objects. Do not use it for arrays. - Use
@Prop({ kind: 'mixed', type: Object })or@Prop({ kind: 'mixed', type: [Object] })when the field intentionally stores flexible raw payloads and should suppress Typegoose Mixed warnings. - Repository reads normalize ObjectId/BSON values and transform populated objects into schema class instances. Code that needs raw Mongoose documents should use
MongoService.getModel(...)or Typegoose/Mongoose APIs directly.
Migration Notes
Recent schema-first changes affect how applications should model references and virtual getters:
- Replace populated virtual fields typed as Typegoose
Ref<T>withPopulatedRef<T>when the field is returned throughMongoRepoand should be used like a class instance. - Keep stored id fields as
RefId<T, RawId>instead of changing them to populated instance types. - Prefer lazy
typeresolvers for relation fields to avoid circular imports. - Replace standalone
@Expose()and@ApiProperty(...)on computed getters with@Prop({ kind: 'virtual', mode: 'getter', comment, hidden, optional, expose, swagger })when the wrapper can express the same metadata. - Replace
@Schema({ schemaOptions: { _id: false, timestamps: false } })on value objects with@Schema({ kind: 'embedded' })where the defaults match. - Replace
@Schema({ schemaOptions: { _id: true, timestamps: true } })on embedded documents with@Schema({ kind: 'subdocument' })where the defaults match. - Prefer
@Prop({ kind: 'map', type: Object })over@Prop({ type: Object }, PropType.MAP)in new schemas. - Use
@Prop({ kind: 'mixed', type: [Object] })for arrays of raw provider objects such as upstream actions, targets, or certificate snapshots;kind: 'map'creates a Mongoose Map and is not valid for array payloads. - Simplify populate-one declarations from
@Prop({ kind: 'virtual', mode: 'populate', type: () => User, ref: () => User, justOne: true, ... })to@Prop({ ref: () => User, foreignField, localField })when the inferred defaults are enough. - Re-test populate and deep populate paths after migration. Consumer JSON should expose string ids, not serialized BSON or Buffer shapes.
Plugins
@joktec/mongo includes package-level mongoose plugins:
- paranoid plugin: applies soft-delete filtering, handles aggregate first-stage constraints such as
$geoNear, and preserves aggregate pipeline contents when injecting soft-delete filters. - strict reference plugin: validates referenced documents for save/update/delete flows and resolves referenced models through the active connection.
- transform plugin: centralizes shared document transformation behavior without breaking Mongo update operators.
Plugins should be treated as framework behavior, not app business logic. App-specific validation belongs in app services, repositories, or schema decorators.
Debug Output
mongoDebug(collection, method, ...args) renders common Mongoose debug callbacks as copyable Mongo shell commands:
mongoDebug('users', 'find', { username: 'ada' }, null, { limit: 5, sort: { createdAt: -1 } });
// db.users.find({ username: 'ada' }).sort({ createdAt: -1 }).limit(5)The renderer supports common Mongo shell values such as ObjectId(...), ISODate(...), regular expressions, arrays, maps, buffers, projections, sort, skip, limit, and maxTimeMS.
Error Contract
MongoCatch and Mongo exception helpers normalize common Mongoose/MongoDB failures such as validation errors, cast errors, duplicate keys, server selection failures, timeouts, transaction conflicts, and strict reference violations.
Application code should branch on stable framework-level error messages/codes rather than raw driver messages.
Repository Layout
src/mongo.module.ts: Nest module and model registration.src/mongo.service.ts: connection lifecycle service.src/mongo.repo.ts: base repository and pagination implementation.src/mongo.config.ts: config validation and defaults.src/helpers: query parsing, pipeline helpers, plugin helpers.src/plugins: Mongoose plugin hooks.src/models: schema, request, response, and options contracts.src/index.ts: public package export boundary.
Development
yarn lint --scope @joktec/mongo
yarn build --scope @joktec/mongo
yarn test --scope @joktec/mongo