@ahrowe/mongo 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -0
- package/dist/batchify.d.ts +36 -0
- package/dist/batchify.d.ts.map +1 -0
- package/dist/batchify.js +57 -0
- package/dist/batchify.js.map +1 -0
- package/dist/db.d.ts +356 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +390 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/outboxRelay.d.ts +34 -0
- package/dist/outboxRelay.d.ts.map +1 -0
- package/dist/outboxRelay.js +152 -0
- package/dist/outboxRelay.js.map +1 -0
- package/dist/types/outbox.d.ts +37 -0
- package/dist/types/outbox.d.ts.map +1 -0
- package/dist/types/outbox.js +22 -0
- package/dist/types/outbox.js.map +1 -0
- package/docs/CLAUDE.md +250 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# @ahrowe/mongo
|
|
2
|
+
|
|
3
|
+
A typed MongoDB CRUD service factory: `connect()` once, then `createService<T>(...)`
|
|
4
|
+
gives you a collection wrapper with built-in transactions, an event bus
|
|
5
|
+
(`created`/`updated`/`removed`), and optional document validation on writes.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @ahrowe/mongo
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import db from '@ahrowe/mongo';
|
|
17
|
+
|
|
18
|
+
const { createService } = db.connect(process.env.MONGO_DB_URI!, 'myDb');
|
|
19
|
+
|
|
20
|
+
type User = { _id: string; email: string; createdOn?: Date; updatedOn?: Date };
|
|
21
|
+
const users = createService<User>('users');
|
|
22
|
+
|
|
23
|
+
const user = await users.insert({ email: 'a@b.com' });
|
|
24
|
+
const found = await users.findOne({ _id: user._id });
|
|
25
|
+
await users.updateOne({ _id: user._id }, { $set: { email: 'c@d.com' } });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Validation on writes
|
|
29
|
+
|
|
30
|
+
Pass a validator function as the second argument to `createService`. It's called on
|
|
31
|
+
every insert and on the document resulting from an update; a non-passing result
|
|
32
|
+
throws. The validator contract is `(entity: T) => { error?: unknown }` — plug in
|
|
33
|
+
whatever validation library you like, or none at all. On a non-empty `error`, that
|
|
34
|
+
value is thrown as-is.
|
|
35
|
+
|
|
36
|
+
With Zod:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { z } from 'zod';
|
|
40
|
+
|
|
41
|
+
const userSchema = z.object({ _id: z.string(), email: z.string().email() });
|
|
42
|
+
const users = createService<User>('users', (entity) => userSchema.safeParse(entity));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or a plain TS guard with no dependency at all:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const users = createService<User>('users', (entity) => (
|
|
49
|
+
entity.email.includes('@') ? {} : { error: new Error('email must contain @') }
|
|
50
|
+
));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Pass `{ skipValidation: true }` on an individual call to bypass it.
|
|
54
|
+
|
|
55
|
+
### Events (transactional outbox)
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
users.on('created', ({ doc, meta }) => { /* ... */ });
|
|
59
|
+
users.on('updated', ({ prevDoc, doc, meta }) => { /* ... */ });
|
|
60
|
+
users.on('removed', ({ doc, meta }) => { /* ... */ });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Writes don't dispatch events directly. Every `insert`/`updateOne`/`updateMany`/
|
|
64
|
+
`removeOne`/`removeMany` that actually changes a document writes one row per
|
|
65
|
+
affected document to an internal `outbox` collection, **in the same transaction**
|
|
66
|
+
as the data write. A separate relay process claims and dispatches those rows to
|
|
67
|
+
your `.on(...)` listeners. This makes delivery durable: events survive a crash
|
|
68
|
+
between the write and the listener running, and are never lost or duplicated
|
|
69
|
+
across multiple pods racing the same row (claims are leased atomically).
|
|
70
|
+
`updateOne`/`updateMany` only enqueue `'updated'` for documents that actually
|
|
71
|
+
changed.
|
|
72
|
+
|
|
73
|
+
`updateMany`/`removeMany` always process every matched document individually —
|
|
74
|
+
one atomic operation per document, all inside one transaction, so the whole
|
|
75
|
+
batch still rolls back together on failure — so each outbox row's
|
|
76
|
+
`{ prevDoc, doc }` pair is accurate and no document is missed even if the
|
|
77
|
+
matched set changes mid-operation. Run the relay to drain the outbox:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { startOutboxRelay } from '@ahrowe/mongo';
|
|
81
|
+
|
|
82
|
+
const relay = startOutboxRelay({ instanceId: process.env.HOSTNAME ?? 'local' });
|
|
83
|
+
// relay.stop() on shutdown
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The reliable per-document path has **no size cap by default** — but a single
|
|
87
|
+
transaction processing a very large matched set risks hitting MongoDB's
|
|
88
|
+
transaction lifetime limit. Pass `{ maxBatchSize }` (per-call, or as a
|
|
89
|
+
`createService` option for a service-wide default) to enforce one and fail fast
|
|
90
|
+
instead:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
await users.updateMany({}, { $set: { plan: 'pro' } }, { maxBatchSize: 500 });
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Past that limit it throws, pointing you at `bulkWrite` for larger jobs (no
|
|
97
|
+
transaction/validation/event guarantees, but no cap either).
|
|
98
|
+
|
|
99
|
+
Pass `{ meta: {...} }` on a call to thread arbitrary data through to listeners,
|
|
100
|
+
so a specific handler can decide to skip its own side effect without anyone else
|
|
101
|
+
missing the event:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
await users.updateOne({ _id }, { $set: { email: 'c@d.com' } }, { meta: { dontRegenPdf: true } });
|
|
105
|
+
|
|
106
|
+
users.on('updated', ({ doc, meta }) => {
|
|
107
|
+
if (meta?.dontRegenPdf) return;
|
|
108
|
+
regenerateInvoicePdf(doc);
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Infra collections with no listeners (sequences, the outbox itself, etc.) can
|
|
113
|
+
skip outbox writes entirely by passing `{ emitOutboxEvents: false }` to
|
|
114
|
+
`createService` — writes take a leaner fast path with no pre-reads or
|
|
115
|
+
transaction-wrapped outbox insert.
|
|
116
|
+
|
|
117
|
+
If a listener needs request/actor context (e.g. an audit log reading from
|
|
118
|
+
`AsyncLocalStorage`), register a provider once at startup. It's called
|
|
119
|
+
**synchronously at write time** and the captured value is stored on the outbox
|
|
120
|
+
row, so it's still available to the listener even after a process restart:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { setOutboxContextProvider } from '@ahrowe/mongo';
|
|
124
|
+
|
|
125
|
+
setOutboxContextProvider(() => myAsyncLocalStorage.getStore());
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Transactions
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import db from '@ahrowe/mongo';
|
|
132
|
+
|
|
133
|
+
const session = await db.startSession();
|
|
134
|
+
await session.withTransaction(async () => {
|
|
135
|
+
await users.insert({ email: 'a@b.com' }, { session });
|
|
136
|
+
await otherService.updateOne({ _id }, { $set: { count: 1 } }, { session });
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`insert`/`updateOne`/`updateMany` automatically wrap themselves in a transaction when
|
|
141
|
+
no `session` is passed.
|
|
142
|
+
|
|
143
|
+
### Connection health
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const { createService, on } = db.connect(process.env.MONGO_DB_URI!, 'myDb');
|
|
147
|
+
|
|
148
|
+
on('error', ({ source, error }) => { /* 'primary' | 'read' */ });
|
|
149
|
+
on('close', ({ source, error }) => { /* ... */ });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`connect()` wires up `error`/`close` handlers on both the primary and
|
|
153
|
+
secondary-preferred read clients internally (so a connection issue can't crash the
|
|
154
|
+
process), and re-emits them on a connection-level bus you can subscribe to for your
|
|
155
|
+
own alerting.
|
|
156
|
+
|
|
157
|
+
### Shutdown
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import db from '@ahrowe/mongo';
|
|
161
|
+
|
|
162
|
+
await db.disconnect();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`disconnect()` closes both MongoDB clients. Outbox rows already committed are
|
|
166
|
+
durable in MongoDB regardless — call this on graceful shutdown (e.g. on
|
|
167
|
+
`SIGTERM`); if you also run `startOutboxRelay` in-process, call `relay.stop()`
|
|
168
|
+
first so it releases any leases it's holding.
|
|
169
|
+
|
|
170
|
+
## API
|
|
171
|
+
|
|
172
|
+
- `connect(connectionString, databaseName?) → { createService, on }`
|
|
173
|
+
- `createService<T>(collectionName, validate?, { addCreatedOnField?, addUpdatedOnField?, maxLimit?, maxBatchSize?, emitOutboxEvents? }) → DbService<T>`
|
|
174
|
+
- `withSession(cb)`, `startSession(options?)`, `disconnect()`
|
|
175
|
+
- `setOutboxContextProvider(fn)` — capture request/actor context at write time for outbox rows
|
|
176
|
+
- `startOutboxRelay({ instanceId, pollIntervalMs?, batchSize?, leaseMs?, maxAttempts?, autoStart? }) → { stop, tick, drain }`
|
|
177
|
+
- `getServiceBus(collectionName)`, `getRegisteredCollections()`, `getOutboxCollection()` — relay/advanced internals
|
|
178
|
+
- `OutboxOp`, `OutboxStatus`, `OutboxRow`, `OUTBOX_COLLECTION` — outbox row shape, for tooling/inspection
|
|
179
|
+
- `DbService<T>` methods: `find`, `findOne`, `findCursor`, `insert`, `updateOne`,
|
|
180
|
+
`updateMany`, `removeOne`, `removeMany`, `count`, `exists`, `aggregate`, `distinct`,
|
|
181
|
+
`createIndex`, `bulkWrite`, `on`, `onPropertiesUpdated`, `eventBus`, `generateId`, `name`
|
|
182
|
+
|
|
183
|
+
See [docs/CLAUDE.md](docs/CLAUDE.md) for gotchas (upserts are rejected, `findOne`
|
|
184
|
+
strictness, the opt-in `maxLimit`/`maxBatchSize` caps, `bulkWrite` safety gate).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface BatchifyOptions<T> {
|
|
2
|
+
/**
|
|
3
|
+
* Returns the next batch of data, or an array containing all data (sliced into
|
|
4
|
+
* batches automatically). When a function: receives the requested batch size
|
|
5
|
+
* and the number of items already processed.
|
|
6
|
+
*/
|
|
7
|
+
dataDelivery?: T[] | ((requestedBatchSize: number, alreadyProcessed: number) => T[] | Promise<T[]>);
|
|
8
|
+
/** Number of items fetched per `dataDelivery` call. Default 1000. */
|
|
9
|
+
batchSize?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Number of items from the current batch processed concurrently. Defaults to
|
|
12
|
+
* `batchSize` (the whole batch at once). Set to `1` for strictly sequential
|
|
13
|
+
* processing — required when items share a MongoDB session/transaction, which
|
|
14
|
+
* doesn't allow concurrent operations.
|
|
15
|
+
*/
|
|
16
|
+
concurrency?: number;
|
|
17
|
+
/** Function to run for each item. Receives the item and its global index. */
|
|
18
|
+
functionToRun?: (data: T, index: number) => void | Promise<void>;
|
|
19
|
+
/** Invoked after each batch completes, with the total number of items processed so far. */
|
|
20
|
+
onProgress?: (processedAmount: number) => void | Promise<void>;
|
|
21
|
+
/** Called when processing an item fails. Receives the failed item and the error. */
|
|
22
|
+
onError?: (entry: T, err: unknown) => void | Promise<void>;
|
|
23
|
+
/** If true, stop fetching further batches once any error has occurred. */
|
|
24
|
+
returnOnError?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* If true, rethrow the first error out of `batchify()` itself once the batch
|
|
27
|
+
* it occurred in has settled, instead of only invoking `onError`. Use this when
|
|
28
|
+
* the caller needs the failure to actually propagate (e.g. to abort a transaction).
|
|
29
|
+
*/
|
|
30
|
+
throwOnError?: boolean;
|
|
31
|
+
/** Milliseconds to wait after completing each batch before starting the next. */
|
|
32
|
+
pauseAfterBatch?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function batchify<T>({ dataDelivery, batchSize, concurrency, functionToRun, onProgress, onError, returnOnError, throwOnError, pauseAfterBatch, }?: BatchifyOptions<T>): Promise<void>;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=batchify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batchify.d.ts","sourceRoot":"","sources":["../src/batchify.ts"],"names":[],"mappings":"AAAA,UAAU,eAAe,CAAC,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,kBAAkB,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpG,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,2FAA2F;IAC3F,UAAU,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,oFAAoF;IACpF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,0EAA0E;IAC1E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,EAChC,YAAuB,EACvB,SAAgB,EAChB,WAAW,EACX,aAA+B,EAC/B,UAA4B,EAC5B,OAAyB,EACzB,aAAqB,EACrB,YAAoB,EACpB,eAAmB,GACpB,GAAE,eAAe,CAAC,CAAC,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDzC"}
|
package/dist/batchify.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export async function batchify({ dataDelivery = () => [], batchSize = 1000, concurrency, functionToRun = () => undefined, onProgress = () => undefined, onError = () => undefined, returnOnError = false, throwOnError = false, pauseAfterBatch = 0, } = {}) {
|
|
2
|
+
const effectiveConcurrency = concurrency ?? batchSize;
|
|
3
|
+
let iteration = 0;
|
|
4
|
+
let hasError = false;
|
|
5
|
+
let firstError;
|
|
6
|
+
let currentBatch = [];
|
|
7
|
+
const getNextBatch = async () => {
|
|
8
|
+
if (typeof dataDelivery === 'function') {
|
|
9
|
+
currentBatch = await dataDelivery(batchSize, iteration * batchSize);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
currentBatch = dataDelivery.slice(batchSize * iteration, batchSize + batchSize * iteration);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const runItem = async (entry, globalIndex) => {
|
|
16
|
+
try {
|
|
17
|
+
await functionToRun(entry, globalIndex);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
hasError = true;
|
|
21
|
+
if (throwOnError && !firstError)
|
|
22
|
+
firstError = { error };
|
|
23
|
+
await onError(entry, error);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const processCurrentBatch = async () => {
|
|
27
|
+
for (let start = 0; start < currentBatch.length; start += effectiveConcurrency) {
|
|
28
|
+
if (firstError)
|
|
29
|
+
return;
|
|
30
|
+
const chunk = currentBatch.slice(start, start + effectiveConcurrency);
|
|
31
|
+
await Promise.allSettled(chunk.map((entry, i) => runItem(entry, batchSize * iteration + start + i)));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
await getNextBatch();
|
|
35
|
+
await onProgress(0);
|
|
36
|
+
while (currentBatch.length) {
|
|
37
|
+
await processCurrentBatch();
|
|
38
|
+
if (firstError)
|
|
39
|
+
throw firstError.error;
|
|
40
|
+
if (currentBatch.length < batchSize) {
|
|
41
|
+
await onProgress(batchSize * iteration + currentBatch.length);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
iteration += 1;
|
|
45
|
+
await onProgress(batchSize * iteration);
|
|
46
|
+
if (hasError && returnOnError) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
await getNextBatch();
|
|
50
|
+
if (currentBatch.length && pauseAfterBatch) {
|
|
51
|
+
await new Promise((resolve) => {
|
|
52
|
+
setTimeout(resolve, pauseAfterBatch);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=batchify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batchify.js","sourceRoot":"","sources":["../src/batchify.ts"],"names":[],"mappings":"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"}
|