@owlmeans/mongo-resource
v0.1.1
Published
MongoDB resource implementation for OwlMeans Common applications. This package provides a complete MongoDB integration for the OwlMeans resource system, offering document storage, querying, indexing, and schema validation with the familiar OwlMeans resour
Downloads
133
Readme
@owlmeans/mongo-resource
MongoDB resource implementation for OwlMeans Common applications. This package provides a complete MongoDB integration for the OwlMeans resource system, offering document storage, querying, indexing, and schema validation with the familiar OwlMeans resource interface.
Overview
The @owlmeans/mongo-resource package extends the base @owlmeans/resource system with MongoDB-specific functionality. It provides:
- MongoDB Resource Implementation: Complete resource interface backed by MongoDB collections
- Document Management: Full CRUD operations with MongoDB ObjectId handling
- Schema Integration: AJV schema validation with MongoDB document structure
- Index Management: Automated index creation and management
- Query Support: MongoDB query capabilities with pagination and sorting
- Locking Mechanisms: Document-level locking for concurrent access control
- Collection Lifecycle: Automated collection and index initialization
- Type Safety: Full TypeScript support with MongoDB-specific types
This package is part of the OwlMeans database integration ecosystem, providing MongoDB as a storage backend for resources.
Installation
npm install @owlmeans/mongo-resource mongodb ajvDependencies
This package requires:
@owlmeans/resource: Core resource system@owlmeans/server-context: Server context managementmongodb: MongoDB Node.js driver (peer dependency)ajv: JSON Schema validation (peer dependency)
Core Concepts
MongoDB Resource
A MongoDB resource wraps a MongoDB collection with the OwlMeans resource interface, providing seamless integration between MongoDB documents and the resource system.
ObjectId Handling
The package automatically converts MongoDB ObjectIds to string IDs in the resource interface while maintaining MongoDB-native ObjectId usage internally.
Schema Validation
JSON Schema validation is applied to documents before storage, ensuring data integrity and consistency.
Collection Management
Collections are automatically created and configured with indexes during resource initialization.
API Reference
Types
MongoResource<T extends ResourceRecord>
Main MongoDB resource interface that extends the base Resource interface with MongoDB-specific capabilities.
interface MongoResource<T extends ResourceRecord> extends Resource<T>, ResourceLocker<T> {
name?: string // Collection name override
schema?: AnySchema // AJV schema for validation
indexes?: Array<IndexDefinition> // Index definitions
collection: Collection // MongoDB collection instance
db(): Promise<Db> // Get database instance
client(): Promise<MongoClient> // Get MongoDB client
index<Type extends MongoResource<T>>( // Add index definition
name: string,
index: IndexSpecification,
options?: CreateIndexesOptions
): Type
getDefaults(): Partial<T> // Get default values from schema
}MongoDbService
Database service interface for MongoDB operations and connection management.
interface MongoDbService extends ResourceDbService<Db, MongoClient>, DbLocker<ResourceRecord> {
// Inherits database service methods and locking capabilities
}IndexDefinition
Type for defining MongoDB indexes.
interface IndexDefinition {
name: string // Index name
index: IndexSpecification // MongoDB index specification
options?: CreateIndexesOptions // Index creation options
}Factory Functions
makeMongoResource<R extends ResourceRecord, T extends MongoResource<R>>(alias: string, dbAlias?: string, serviceAlias?: string, makeCustomResource?: ResourceMaker<R, T>): T
Creates a MongoDB resource instance with full CRUD capabilities.
Parameters:
alias: Resource alias for registrationdbAlias: Database alias (default: 'mongo')serviceAlias: Service alias (default: same as dbAlias)makeCustomResource: Optional custom resource factory
Returns: MongoResource instance
Example:
import { makeMongoResource } from '@owlmeans/mongo-resource'
interface User extends ResourceRecord {
id?: string
name: string
email: string
createdAt?: Date
}
const userResource = makeMongoResource<User>('users')
// With custom collection and service
const customResource = makeMongoResource<User>('users', 'primary-db', 'mongo-service')MongoDB Resource Methods
get(id: string, field?: string, opts?: LifecycleOptions): Promise<T>
Retrieves a document by ID or specified field. Throws UnknownRecordError if not found.
Parameters:
id: Document identifierfield: Field to search by (default: '_id')opts: Lifecycle options
Returns: Promise resolving to the document
Example:
// Get by MongoDB ObjectId (converted from string)
const user = await userResource.get('507f1f77bcf86cd799439011')
// Get by custom field
const userByEmail = await userResource.get('[email protected]', 'email')load(id: string, field?: string, opts?: LifecycleOptions): Promise<T | null>
Loads a document by ID or specified field. Returns null if not found.
Example:
const user = await userResource.load('507f1f77bcf86cd799439011')
if (user) {
console.log('User found:', user.name)
} else {
console.log('User not found')
}create(record: Partial<T>, opts?: LifecycleOptions): Promise<T>
Creates a new document. Automatically applies schema defaults and generates ObjectId.
Parameters:
record: Document data (id will be generated)opts: Lifecycle options
Returns: Promise resolving to the created document
Throws: RecordExists if record already has an ID
Example:
const newUser = await userResource.create({
name: 'John Doe',
email: '[email protected]'
})
console.log('Created user with ID:', newUser.id)save(record: Partial<T>, opts?: Getter): Promise<T>
Saves a document (creates if new, updates if exists).
Example:
// Create new user (no id)
const user1 = await userResource.save({
name: 'Jane Doe',
email: '[email protected]'
})
// Update existing user (with id)
const user2 = await userResource.save({
id: user1.id,
name: 'Jane Smith'
})update(record: Partial<T>, opts?: Getter): Promise<T>
Updates an existing document. Throws UnknownRecordError if not found.
Example:
const updatedUser = await userResource.update({
id: '507f1f77bcf86cd799439011',
name: 'Updated Name'
})delete(id: string | T, opts?: Getter): Promise<T | null>
Deletes a document by ID or document object.
Parameters:
id: Document ID or document objectopts: Additional options or field name
Returns: Promise resolving to deleted document or null
Example:
// Delete by ID
const deleted = await userResource.delete('507f1f77bcf86cd799439011')
// Delete by document
const deleted2 = await userResource.delete(userObject)
// Delete by custom field
const deleted3 = await userResource.delete('[email protected]', 'email')pick(id: string | T, opts?: Getter): Promise<T>
Deletes and returns a document. Throws UnknownRecordError if not found.
Example:
try {
const removedUser = await userResource.pick('507f1f77bcf86cd799439011')
console.log('Removed user:', removedUser.name)
} catch (error) {
console.error('User not found for removal')
}list(criteria?: ListOptions | ListCriteria, opts?: ListOptions): Promise<ListResult<T>>
Lists documents with optional filtering, pagination, and sorting.
Parameters:
criteria: MongoDB query criteria or list optionsopts: Additional list options
Returns: Promise resolving to paginated results
Example:
// List all users with pagination
const result = await userResource.list({
pager: { page: 0, size: 10 }
})
// List with MongoDB query
const activeUsers = await userResource.list({
criteria: { status: 'active' },
pager: { page: 0, size: 20, sort: [['createdAt', false]] }
})
console.log(`Found ${result.pager?.total} users`)
result.items.forEach(user => console.log(user.name))Database Access Methods
db(): Promise<Db>
Gets the MongoDB database instance.
Example:
const db = await userResource.db()
const stats = await db.stats()
console.log('Database stats:', stats)client(): Promise<MongoClient>
Gets the MongoDB client instance.
Example:
const client = await userResource.client()
const admin = client.db().admin()
const serverStatus = await admin.serverStatus()Index Management
index<Type>(name: string, index: IndexSpecification, options?: CreateIndexesOptions): Type
Adds an index definition to the resource. Indexes are created during resource initialization.
Parameters:
name: Index nameindex: MongoDB index specificationoptions: Index creation options
Returns: Resource instance for method chaining
Example:
// Single field index
userResource.index('email-unique', { email: 1 }, { unique: true })
// Compound index
userResource.index('name-email', { name: 1, email: 1 })
// Text index
userResource.index('text-search', { name: 'text', email: 'text' })
// TTL index
userResource.index('expire-sessions', { createdAt: 1 }, { expireAfterSeconds: 3600 })Schema and Defaults
getDefaults(): Partial<T>
Gets default values from the AJV schema.
Example:
// Assuming schema has default values
const defaults = userResource.getDefaults()
console.log('Default values:', defaults)
// Defaults are automatically applied during create()Locking Methods
lock(record: Partial<T>, fields?: string[]): Promise<T>
Locks specified fields of a document for concurrent access control.
Parameters:
record: Document to lockfields: Fields to lock (default: secure fields from schema)
Returns: Promise resolving to locked document
unlock(record: Partial<T>, fields?: string[]): Promise<T>
Unlocks specified fields of a document.
Parameters:
record: Document to unlockfields: Fields to unlock (default: secure fields from schema)
Returns: Promise resolving to unlocked document
Example:
// Lock user for critical update
const lockedUser = await userResource.lock({ id: userId }, ['balance'])
try {
// Perform critical operations
await updateUserBalance(lockedUser)
} finally {
// Always unlock
await userResource.unlock({ id: userId }, ['balance'])
}Helper Functions
getSchemaSecureFeilds(schema: AnySchema): string[]
Extracts fields marked as secure in the AJV schema.
Parameters:
schema: AJV schema object
Returns: Array of secure field names
Example:
import { getSchemaSecureFeilds } from '@owlmeans/mongo-resource'
const schema = {
type: 'object',
properties: {
password: { type: 'string', secure: true },
balance: { type: 'number', secure: true },
name: { type: 'string' }
}
}
const secureFields = getSchemaSecureFeilds(schema)
// ['password', 'balance']Constants
const DEFAULT_DB_ALIAS = 'mongo' // Default database alias
const DEFAULT_PAGE_SIZE = 10 // Default pagination sizeUsage Examples
Basic Resource Setup
import { makeMongoResource } from '@owlmeans/mongo-resource'
import { makeServerContext } from '@owlmeans/server-context'
// Define document interface
interface Product extends ResourceRecord {
id?: string
name: string
price: number
category: string
inStock: boolean
createdAt?: Date
}
// Create context with MongoDB configuration
const context = makeServerContext({
service: 'product-service',
dbs: [{
service: 'mongo',
alias: 'primary',
host: 'localhost',
port: 27017,
schema: 'products_db'
}]
})
// Create and configure resource
const productResource = makeMongoResource<Product>('products', 'primary')
// Add indexes
productResource
.index('category-price', { category: 1, price: -1 })
.index('name-text', { name: 'text' })
.index('inStock', { inStock: 1 })
// Register resource with context
context.registerResource(productResource)
// Initialize context
await context.configure().init()CRUD Operations
// Create product
const newProduct = await productResource.create({
name: 'Gaming Laptop',
price: 1299.99,
category: 'electronics',
inStock: true
})
// Get product
const product = await productResource.get(newProduct.id!)
// Update product
const updatedProduct = await productResource.update({
id: product.id,
price: 1199.99
})
// List products with filtering
const electronicsResult = await productResource.list({
criteria: { category: 'electronics', inStock: true },
pager: {
page: 0,
size: 20,
sort: [['price', true]] // Sort by price descending
}
})
// Delete product
await productResource.delete(product.id!)Schema Validation Integration
import Ajv, { JSONSchemaType } from 'ajv'
// Define schema with validation and defaults
const productSchema: JSONSchemaType<Product> = {
type: 'object',
properties: {
id: { type: 'string', nullable: true },
name: { type: 'string', minLength: 1, maxLength: 100 },
price: { type: 'number', minimum: 0 },
category: { type: 'string', enum: ['electronics', 'clothing', 'books'] },
inStock: { type: 'boolean', default: true },
createdAt: { type: 'string', format: 'date-time', nullable: true }
},
required: ['name', 'price', 'category'],
additionalProperties: false
}
// Create resource with schema
const productResource = makeMongoResource<Product>('products')
productResource.schema = productSchema
// Now all operations will validate against schema
try {
await productResource.create({
name: '', // Will fail validation (minLength: 1)
price: -10, // Will fail validation (minimum: 0)
category: 'invalid' // Will fail validation (not in enum)
})
} catch (error) {
console.error('Validation failed:', error)
}Advanced Querying
// Complex MongoDB queries
const advancedResults = await productResource.list({
criteria: {
$and: [
{ price: { $gte: 100, $lte: 1000 } },
{ category: { $in: ['electronics', 'books'] } },
{ inStock: true }
]
},
pager: {
page: 0,
size: 15,
sort: [['createdAt', false], ['price', true]]
}
})
// Text search (requires text index)
const searchResults = await productResource.list({
criteria: { $text: { $search: 'gaming laptop' } }
})
// Aggregation via direct database access
const db = await productResource.db()
const categoryStats = await db.collection('products').aggregate([
{ $group: { _id: '$category', count: { $sum: 1 }, avgPrice: { $avg: '$price' } } }
]).toArray()Index Management
// Add various types of indexes
productResource
// Unique index
.index('sku-unique', { sku: 1 }, { unique: true })
// Compound index for common queries
.index('category-instock-price', {
category: 1,
inStock: 1,
price: -1
})
// Text search index
.index('search', {
name: 'text',
description: 'text'
}, {
weights: { name: 10, description: 5 }
})
// TTL index for temporary records
.index('temp-expire', {
createdAt: 1
}, {
expireAfterSeconds: 86400 // 24 hours
})
// Partial index
.index('active-products', {
category: 1,
price: 1
}, {
partialFilterExpression: { inStock: true }
})
// Indexes are automatically created during resource initializationDocument Locking
// Schema with secure fields
const userSchema = {
type: 'object',
properties: {
id: { type: 'string', nullable: true },
username: { type: 'string' },
balance: { type: 'number', secure: true }, // Secure field
password: { type: 'string', secure: true } // Secure field
},
required: ['username']
}
const userResource = makeMongoResource<User>('users')
userResource.schema = userSchema
// Lock user for balance update
const userId = 'user123'
const lockedUser = await userResource.lock({ id: userId })
try {
// Perform balance update
const user = await userResource.get(userId)
await userResource.update({
id: userId,
balance: user.balance + 100
})
} finally {
// Always unlock
await userResource.unlock({ id: userId })
}Multiple Database Support
// Configure multiple MongoDB databases
const context = makeServerContext({
dbs: [
{
service: 'mongo',
alias: 'primary',
host: 'primary-mongo.example.com',
schema: 'main_db'
},
{
service: 'mongo',
alias: 'analytics',
host: 'analytics-mongo.example.com',
schema: 'analytics_db'
}
]
})
// Create resources for different databases
const userResource = makeMongoResource<User>('users', 'primary')
const analyticsResource = makeMongoResource<AnalyticsRecord>('events', 'analytics')
// Both resources work independently with their respective databasesError Handling
import {
UnknownRecordError,
RecordExists,
RecordUpdateFailed,
MisshapedRecord
} from '@owlmeans/resource'
try {
// Various operations that can fail
const user = await userResource.get('nonexistent-id')
} catch (error) {
if (error instanceof UnknownRecordError) {
console.error('User not found:', error.id)
}
}
try {
await userResource.create({ id: 'existing-id', name: 'Test' })
} catch (error) {
if (error instanceof RecordExists) {
console.error('User already exists')
}
}Advanced Features
Custom Collection Names
// Override collection name
const customResource = makeMongoResource<User>('users')
customResource.name = 'custom_users_collection'Direct MongoDB Operations
// Access MongoDB collection directly for advanced operations
const collection = userResource.collection
// Use MongoDB-specific features
const bulkOps = collection.initializeUnorderedBulkOp()
bulkOps.insert({ name: 'User 1' })
bulkOps.insert({ name: 'User 2' })
await bulkOps.execute()
// Aggregation pipelines
const pipeline = [
{ $match: { active: true } },
{ $group: { _id: '$department', count: { $sum: 1 } } }
]
const results = await collection.aggregate(pipeline).toArray()Schema Defaults Integration
const schemaWithDefaults = {
type: 'object',
properties: {
status: { type: 'string', default: 'active' },
createdAt: { type: 'string', format: 'date-time', default: new Date().toISOString() },
settings: {
type: 'object',
default: { theme: 'light', notifications: true }
}
}
}
const resource = makeMongoResource<MyRecord>('records')
resource.schema = schemaWithDefaults
// Defaults are automatically applied during creation
const record = await resource.create({ name: 'Test' })
// record.status === 'active'
// record.settings === { theme: 'light', notifications: true }Performance Considerations
- Indexing: Create appropriate indexes for your query patterns
- Pagination: Use pagination for large result sets
- Connection Pooling: MongoDB driver handles connection pooling automatically
- Schema Validation: Validation happens before database operations
- Document Size: MongoDB has a 16MB document size limit
- Query Optimization: Use MongoDB explain() to optimize queries
Best Practices
- Index Strategy: Create indexes that match your query patterns
- Schema Design: Design schemas that reflect your document structure
- Error Handling: Handle MongoDB-specific errors appropriately
- Resource Cleanup: Properly close MongoDB connections
- Security: Use secure fields for sensitive data that requires locking
- Validation: Always use schema validation for data integrity
- Pagination: Implement pagination for list operations
Integration with OwlMeans Ecosystem
Server Context Integration
import { makeServerContext } from '@owlmeans/server-context'
// MongoDB resources integrate seamlessly with server context
const context = makeServerContext(config)
context.registerResource(mongoResource)Service Integration
import { createDbService } from '@owlmeans/resource'
// MongoDB service provides database access
const mongoService = createDbService('mongo', mongoConfig)
context.registerService(mongoService)Authentication Integration
// MongoDB resources work with authentication scopes
const authenticatedResource = makeMongoResource<ProtectedRecord>('protected')
// Access control is handled at the application levelRelated Packages
@owlmeans/resource- Core resource system@owlmeans/server-context- Server context management@owlmeans/redis-resource- Redis resource implementation@owlmeans/static-resource- Static file resources
