mongochangestream
Version:
Sync MongoDB collections via change streams into any database.
179 lines (151 loc) • 4.65 kB
text/typescript
import type {
AggregationCursor,
ChangeStream,
ChangeStreamDocument,
ChangeStreamInsertDocument,
Document,
MongoAPIError,
MongoServerError,
} from 'mongodb'
import { type Options as RetryOptions } from 'p-retry'
import type { LastFlush, QueueStats } from 'prom-utils'
export type Cursor = ChangeStream | AggregationCursor
export type JSONSchema = Record<string, any>
type MaybePromise<T> = T | Promise<T>
/**
* Extract the operationType field from a type.
* Create a union of all possible operation types.
*/
type ExtractOperationType<T> = T extends { operationType: infer Type }
? Type
: never
/**
* Mapping from operation type to document type. For example, if the operation
* type is 'insert', the document type is ChangeStreamInsertDocument.
*/
type OperationTypeMap = {
[K in ExtractOperationType<ChangeStreamDocument>]: Extract<
ChangeStreamDocument,
{ operationType: K }
>
}
/**
* Union of all possible operation types - 'insert' | 'update' | 'delete' etc.
*/
export type OperationType = keyof OperationTypeMap
/**
* Extract the specific document types from an array of operation types.
* For example, if the type parameter is ['insert', 'update'], the return type
* is ChangeStreamInsertDocument | ChangeStreamUpdateDocument.
*/
export type DocumentsForOperationTypes<T extends OperationType[] | undefined> =
T extends OperationType[] ? OperationTypeMap[T[number]] : ChangeStreamDocument
export type ProcessChangeStreamRecords<
T extends OperationType[] | undefined = undefined,
> = (docs: DocumentsForOperationTypes<T>[]) => MaybePromise<void>
export type ProcessInitialScanRecords = (
docs: ChangeStreamInsertDocument[]
) => MaybePromise<void>
// Options
export interface SyncOptions {
/** Field paths to omit. */
omit?: string[]
/**
* Added to all Redis keys to allow the same collection
* to be synced in parallel. Otherwise, the Redis keys
* would be the same for a given collection and parallel
* syncing jobs would overwrite each other.
*/
uniqueId?: string
/**
* Maximum time to pause in ms after calling `pausable.pause()`.
* By default, pausing is indefinite.
*/
maxPauseTime?: number
/**
* Options for handling retry logic when calling `processRecords`.
* Defaults to `{ minTimeout: 30s, maxTimeout: 1h, retries: 30 }`.
* Options passed here will be applied over the default options.
*/
retry?: RetryOptions
}
export interface SortField<T> {
field: string
/** Function to serialize value to string. */
serialize: (x: T) => string
deserialize: (x: string) => T
/** Sort order: asc or desc. Defaults to asc */
order?: 'asc' | 'desc'
}
export interface ScanOptions<T = any> {
/** Defaults to _id */
sortField?: SortField<T>
/** Extend the pipeline. Be careful not to exclude the sort field or change the sort order. */
pipeline?: Document[]
}
export interface ChangeStreamOptions<
T extends OperationType[] | undefined = undefined,
> {
pipeline?: Document[]
operationTypes?: T
}
export interface ChangeOptions {
/** How often to retrieve the schema and look for a change. */
interval?: number
/**
* Remove fields that are not used when converting the schema
* in a downstream library like mongo2elastic or mongo2crate.
* Preserves bsonType, properties, additionalProperties, items, and enum.
*/
shouldRemoveUnusedFields?: boolean
}
// Events
export type Events =
| 'cursorError'
| 'resync'
| 'schemaChange'
| 'stateChange'
| 'initialScanComplete'
| 'stats'
| 'processError'
export interface ResyncEvent {
type: 'resync'
}
export interface SchemaChangeEvent {
type: 'schemaChange'
previousSchema?: JSONSchema
currentSchema: JSONSchema
}
export interface StateChangeEvent {
type: 'stateChange'
name: string
from: string
to: string
}
export interface InitialScanCompleteEvent {
type: 'initialScanComplete'
}
export type CursorError = MongoServerError | MongoAPIError
export interface CursorErrorEvent {
type: 'cursorError'
name: 'runInitialScan' | 'processChangeStream'
error: CursorError
}
export interface ProcessErrorEvent {
type: 'processError'
name: 'runInitialScan' | 'processChangeStream'
error: unknown
}
/**
* If `maxItemsPerSec` is not set, `stats.itemsPerSec` will be 0.
* If `maxBytesPerSec` is not set, `stats.bytesPerSec` will be 0.
*/
export interface StatsEvent {
type: 'stats'
name: 'runInitialScan' | 'processChangeStream'
stats: QueueStats
lastFlush?: LastFlush
}
// State
export type State = 'starting' | 'started' | 'stopping' | 'stopped'
export type SimpleState = 'started' | 'stopped'