@apibara/plugin-mongo 2.0.0-beta.28
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 +7 -0
- package/dist/index.cjs +292 -0
- package/dist/index.d.cts +37 -0
- package/dist/index.d.mts +37 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.mjs +287 -0
- package/package.json +41 -0
- package/src/index.ts +104 -0
- package/src/mongo.ts +51 -0
- package/src/storage.ts +258 -0
- package/src/utils.ts +19 -0
package/README.md
ADDED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const indexer = require('@apibara/indexer');
|
|
4
|
+
const plugins = require('@apibara/indexer/plugins');
|
|
5
|
+
|
|
6
|
+
async function invalidate(db, session, cursor, collections) {
|
|
7
|
+
const orderKeyValue = Number(cursor.orderKey);
|
|
8
|
+
for (const collection of collections) {
|
|
9
|
+
await db.collection(collection).deleteMany(
|
|
10
|
+
{
|
|
11
|
+
"_cursor.from": {
|
|
12
|
+
$gt: orderKeyValue
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{ session }
|
|
16
|
+
);
|
|
17
|
+
await db.collection(collection).updateMany(
|
|
18
|
+
{ "_cursor.to": { $gt: orderKeyValue } },
|
|
19
|
+
{
|
|
20
|
+
$set: {
|
|
21
|
+
"_cursor.to": null
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{ session }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function finalize(db, session, cursor, collections) {
|
|
29
|
+
const orderKeyValue = Number(cursor.orderKey);
|
|
30
|
+
for (const collection of collections) {
|
|
31
|
+
await db.collection(collection).deleteMany(
|
|
32
|
+
{
|
|
33
|
+
"_cursor.to": { $lt: orderKeyValue }
|
|
34
|
+
},
|
|
35
|
+
{ session }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class MongoStorage {
|
|
41
|
+
constructor(db, session, endCursor) {
|
|
42
|
+
this.db = db;
|
|
43
|
+
this.session = session;
|
|
44
|
+
this.endCursor = endCursor;
|
|
45
|
+
}
|
|
46
|
+
collection(name, options) {
|
|
47
|
+
const collection = this.db.collection(name, options);
|
|
48
|
+
return new MongoCollection(
|
|
49
|
+
this.session,
|
|
50
|
+
collection,
|
|
51
|
+
this.endCursor
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
class MongoCollection {
|
|
56
|
+
constructor(session, collection, endCursor) {
|
|
57
|
+
this.session = session;
|
|
58
|
+
this.collection = collection;
|
|
59
|
+
this.endCursor = endCursor;
|
|
60
|
+
}
|
|
61
|
+
async insertOne(doc, options) {
|
|
62
|
+
return await this.collection.insertOne(
|
|
63
|
+
{
|
|
64
|
+
...doc,
|
|
65
|
+
_cursor: {
|
|
66
|
+
from: Number(this.endCursor?.orderKey),
|
|
67
|
+
to: null
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{ ...options, session: this.session }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
async insertMany(docs, options) {
|
|
74
|
+
return await this.collection.insertMany(
|
|
75
|
+
docs.map((doc) => ({
|
|
76
|
+
...doc,
|
|
77
|
+
_cursor: {
|
|
78
|
+
from: Number(this.endCursor?.orderKey),
|
|
79
|
+
to: null
|
|
80
|
+
}
|
|
81
|
+
})),
|
|
82
|
+
{ ...options, session: this.session }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
async updateOne(filter, update, options) {
|
|
86
|
+
const oldDoc = await this.collection.findOneAndUpdate(
|
|
87
|
+
{
|
|
88
|
+
...filter,
|
|
89
|
+
"_cursor.to": null
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
...update,
|
|
93
|
+
$set: {
|
|
94
|
+
...update.$set,
|
|
95
|
+
"_cursor.from": Number(this.endCursor?.orderKey)
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
...options,
|
|
100
|
+
session: this.session,
|
|
101
|
+
returnDocument: "before"
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
if (oldDoc) {
|
|
105
|
+
const { _id, ...doc } = oldDoc;
|
|
106
|
+
await this.collection.insertOne(
|
|
107
|
+
{
|
|
108
|
+
...doc,
|
|
109
|
+
_cursor: {
|
|
110
|
+
...oldDoc._cursor,
|
|
111
|
+
to: Number(this.endCursor?.orderKey)
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{ session: this.session }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
acknowledged: true,
|
|
119
|
+
modifiedCount: oldDoc ? 1 : 0,
|
|
120
|
+
upsertedId: null,
|
|
121
|
+
upsertedCount: 0,
|
|
122
|
+
matchedCount: oldDoc ? 1 : 0
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async updateMany(filter, update, options) {
|
|
126
|
+
const oldDocs = await this.collection.find(
|
|
127
|
+
{
|
|
128
|
+
...filter,
|
|
129
|
+
"_cursor.to": null
|
|
130
|
+
},
|
|
131
|
+
{ session: this.session }
|
|
132
|
+
).toArray();
|
|
133
|
+
const updateResult = await this.collection.updateMany(
|
|
134
|
+
{
|
|
135
|
+
...filter,
|
|
136
|
+
"_cursor.to": null
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
...update,
|
|
140
|
+
$set: {
|
|
141
|
+
...update.$set,
|
|
142
|
+
"_cursor.from": Number(this.endCursor?.orderKey)
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{ ...options, session: this.session }
|
|
146
|
+
);
|
|
147
|
+
const oldDocsWithUpdatedCursor = oldDocs.map(({ _id, ...doc }) => ({
|
|
148
|
+
...doc,
|
|
149
|
+
_cursor: {
|
|
150
|
+
...doc._cursor,
|
|
151
|
+
to: Number(this.endCursor?.orderKey)
|
|
152
|
+
}
|
|
153
|
+
}));
|
|
154
|
+
if (oldDocsWithUpdatedCursor.length > 0) {
|
|
155
|
+
await this.collection.insertMany(
|
|
156
|
+
oldDocsWithUpdatedCursor,
|
|
157
|
+
{ session: this.session }
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return updateResult;
|
|
161
|
+
}
|
|
162
|
+
async deleteOne(filter, options) {
|
|
163
|
+
return await this.collection.updateOne(
|
|
164
|
+
{
|
|
165
|
+
...filter,
|
|
166
|
+
"_cursor.to": null
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
$set: {
|
|
170
|
+
"_cursor.to": Number(this.endCursor?.orderKey)
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{ ...options, session: this.session }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
async deleteMany(filter, options) {
|
|
177
|
+
return await this.collection.updateMany(
|
|
178
|
+
{
|
|
179
|
+
...filter ?? {},
|
|
180
|
+
"_cursor.to": null
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
$set: {
|
|
184
|
+
"_cursor.to": Number(this.endCursor?.orderKey)
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{ ...options, session: this.session }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
async findOne(filter, options) {
|
|
191
|
+
return await this.collection.findOne(
|
|
192
|
+
{
|
|
193
|
+
...filter,
|
|
194
|
+
"_cursor.to": null
|
|
195
|
+
},
|
|
196
|
+
{ ...options, session: this.session }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
find(filter, options) {
|
|
200
|
+
return this.collection.find(
|
|
201
|
+
{
|
|
202
|
+
...filter,
|
|
203
|
+
"_cursor.to": null
|
|
204
|
+
},
|
|
205
|
+
{ ...options, session: this.session }
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class MongoStorageError extends Error {
|
|
211
|
+
constructor(message) {
|
|
212
|
+
super(message);
|
|
213
|
+
this.name = "MongoStorageError";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function withTransaction(client, cb) {
|
|
217
|
+
return await client.withSession(async (session) => {
|
|
218
|
+
return await session.withTransaction(async (session2) => {
|
|
219
|
+
return await cb(session2);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const MONGO_PROPERTY = "_mongo";
|
|
225
|
+
function useMongoStorage() {
|
|
226
|
+
const context = indexer.useIndexerContext();
|
|
227
|
+
if (!context[MONGO_PROPERTY]) {
|
|
228
|
+
throw new MongoStorageError(
|
|
229
|
+
"mongo storage is not available. Did you register the plugin?"
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return context[MONGO_PROPERTY];
|
|
233
|
+
}
|
|
234
|
+
function mongoStorage({
|
|
235
|
+
client,
|
|
236
|
+
dbName,
|
|
237
|
+
dbOptions,
|
|
238
|
+
collections,
|
|
239
|
+
persistState: enablePersistence = true
|
|
240
|
+
}) {
|
|
241
|
+
return plugins.defineIndexerPlugin((indexer) => {
|
|
242
|
+
indexer.hooks.hook("message:finalize", async ({ message }) => {
|
|
243
|
+
const { cursor } = message.finalize;
|
|
244
|
+
if (!cursor) {
|
|
245
|
+
throw new MongoStorageError("finalized cursor is undefined");
|
|
246
|
+
}
|
|
247
|
+
await withTransaction(client, async (session) => {
|
|
248
|
+
const db = client.db(dbName, dbOptions);
|
|
249
|
+
await finalize(db, session, cursor, collections);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
indexer.hooks.hook("message:invalidate", async ({ message }) => {
|
|
253
|
+
const { cursor } = message.invalidate;
|
|
254
|
+
if (!cursor) {
|
|
255
|
+
throw new MongoStorageError("invalidate cursor is undefined");
|
|
256
|
+
}
|
|
257
|
+
await withTransaction(client, async (session) => {
|
|
258
|
+
const db = client.db(dbName, dbOptions);
|
|
259
|
+
await invalidate(db, session, cursor, collections);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
indexer.hooks.hook("connect:after", async ({ request }) => {
|
|
263
|
+
const cursor = request.startingCursor;
|
|
264
|
+
if (!cursor) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
await withTransaction(client, async (session) => {
|
|
268
|
+
const db = client.db(dbName, dbOptions);
|
|
269
|
+
await invalidate(db, session, cursor, collections);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
indexer.hooks.hook("handler:middleware", async ({ use }) => {
|
|
273
|
+
use(async (context, next) => {
|
|
274
|
+
const { endCursor } = context;
|
|
275
|
+
if (!endCursor) {
|
|
276
|
+
throw new MongoStorageError("end cursor is undefined");
|
|
277
|
+
}
|
|
278
|
+
await withTransaction(client, async (session) => {
|
|
279
|
+
const db = client.db(dbName, dbOptions);
|
|
280
|
+
context[MONGO_PROPERTY] = new MongoStorage(db, session, endCursor);
|
|
281
|
+
await next();
|
|
282
|
+
delete context[MONGO_PROPERTY];
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
exports.MongoCollection = MongoCollection;
|
|
290
|
+
exports.MongoStorage = MongoStorage;
|
|
291
|
+
exports.mongoStorage = mongoStorage;
|
|
292
|
+
exports.useMongoStorage = useMongoStorage;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as _apibara_indexer_plugins from '@apibara/indexer/plugins';
|
|
2
|
+
import { Db, ClientSession, Document, CollectionOptions, Collection, OptionalUnlessRequiredId, InsertOneOptions, InsertOneResult, BulkWriteOptions, InsertManyResult, Filter, UpdateFilter, UpdateOptions, UpdateResult, DeleteOptions, FindOptions, WithId, FindCursor, MongoClient, DbOptions } from 'mongodb';
|
|
3
|
+
import { Cursor } from '@apibara/protocol';
|
|
4
|
+
|
|
5
|
+
declare class MongoStorage {
|
|
6
|
+
private db;
|
|
7
|
+
private session;
|
|
8
|
+
private endCursor?;
|
|
9
|
+
constructor(db: Db, session: ClientSession, endCursor?: Cursor | undefined);
|
|
10
|
+
collection<TSchema extends Document = Document>(name: string, options?: CollectionOptions): MongoCollection<TSchema>;
|
|
11
|
+
}
|
|
12
|
+
declare class MongoCollection<TSchema extends Document> {
|
|
13
|
+
private session;
|
|
14
|
+
private collection;
|
|
15
|
+
private endCursor?;
|
|
16
|
+
constructor(session: ClientSession, collection: Collection<TSchema>, endCursor?: Cursor | undefined);
|
|
17
|
+
insertOne(doc: OptionalUnlessRequiredId<TSchema>, options?: InsertOneOptions): Promise<InsertOneResult<TSchema>>;
|
|
18
|
+
insertMany(docs: ReadonlyArray<OptionalUnlessRequiredId<TSchema>>, options?: BulkWriteOptions): Promise<InsertManyResult<TSchema>>;
|
|
19
|
+
updateOne(filter: Filter<TSchema>, update: UpdateFilter<TSchema>, options?: UpdateOptions): Promise<UpdateResult<TSchema>>;
|
|
20
|
+
updateMany(filter: Filter<TSchema>, update: UpdateFilter<TSchema>, options?: UpdateOptions): Promise<UpdateResult<TSchema>>;
|
|
21
|
+
deleteOne(filter: Filter<TSchema>, options?: DeleteOptions): Promise<UpdateResult<TSchema>>;
|
|
22
|
+
deleteMany(filter?: Filter<TSchema>, options?: DeleteOptions): Promise<UpdateResult<TSchema>>;
|
|
23
|
+
findOne(filter: Filter<TSchema>, options?: Omit<FindOptions, "timeoutMode">): Promise<WithId<TSchema> | null>;
|
|
24
|
+
find(filter: Filter<TSchema>, options?: FindOptions): FindCursor<WithId<TSchema>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare function useMongoStorage(): MongoStorage;
|
|
28
|
+
interface MongoStorageOptions {
|
|
29
|
+
client: MongoClient;
|
|
30
|
+
dbName: string;
|
|
31
|
+
dbOptions?: DbOptions;
|
|
32
|
+
collections: string[];
|
|
33
|
+
persistState?: boolean;
|
|
34
|
+
}
|
|
35
|
+
declare function mongoStorage<TFilter, TBlock>({ client, dbName, dbOptions, collections, persistState: enablePersistence, }: MongoStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
36
|
+
|
|
37
|
+
export { MongoCollection, MongoStorage, type MongoStorageOptions, mongoStorage, useMongoStorage };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as _apibara_indexer_plugins from '@apibara/indexer/plugins';
|
|
2
|
+
import { Db, ClientSession, Document, CollectionOptions, Collection, OptionalUnlessRequiredId, InsertOneOptions, InsertOneResult, BulkWriteOptions, InsertManyResult, Filter, UpdateFilter, UpdateOptions, UpdateResult, DeleteOptions, FindOptions, WithId, FindCursor, MongoClient, DbOptions } from 'mongodb';
|
|
3
|
+
import { Cursor } from '@apibara/protocol';
|
|
4
|
+
|
|
5
|
+
declare class MongoStorage {
|
|
6
|
+
private db;
|
|
7
|
+
private session;
|
|
8
|
+
private endCursor?;
|
|
9
|
+
constructor(db: Db, session: ClientSession, endCursor?: Cursor | undefined);
|
|
10
|
+
collection<TSchema extends Document = Document>(name: string, options?: CollectionOptions): MongoCollection<TSchema>;
|
|
11
|
+
}
|
|
12
|
+
declare class MongoCollection<TSchema extends Document> {
|
|
13
|
+
private session;
|
|
14
|
+
private collection;
|
|
15
|
+
private endCursor?;
|
|
16
|
+
constructor(session: ClientSession, collection: Collection<TSchema>, endCursor?: Cursor | undefined);
|
|
17
|
+
insertOne(doc: OptionalUnlessRequiredId<TSchema>, options?: InsertOneOptions): Promise<InsertOneResult<TSchema>>;
|
|
18
|
+
insertMany(docs: ReadonlyArray<OptionalUnlessRequiredId<TSchema>>, options?: BulkWriteOptions): Promise<InsertManyResult<TSchema>>;
|
|
19
|
+
updateOne(filter: Filter<TSchema>, update: UpdateFilter<TSchema>, options?: UpdateOptions): Promise<UpdateResult<TSchema>>;
|
|
20
|
+
updateMany(filter: Filter<TSchema>, update: UpdateFilter<TSchema>, options?: UpdateOptions): Promise<UpdateResult<TSchema>>;
|
|
21
|
+
deleteOne(filter: Filter<TSchema>, options?: DeleteOptions): Promise<UpdateResult<TSchema>>;
|
|
22
|
+
deleteMany(filter?: Filter<TSchema>, options?: DeleteOptions): Promise<UpdateResult<TSchema>>;
|
|
23
|
+
findOne(filter: Filter<TSchema>, options?: Omit<FindOptions, "timeoutMode">): Promise<WithId<TSchema> | null>;
|
|
24
|
+
find(filter: Filter<TSchema>, options?: FindOptions): FindCursor<WithId<TSchema>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare function useMongoStorage(): MongoStorage;
|
|
28
|
+
interface MongoStorageOptions {
|
|
29
|
+
client: MongoClient;
|
|
30
|
+
dbName: string;
|
|
31
|
+
dbOptions?: DbOptions;
|
|
32
|
+
collections: string[];
|
|
33
|
+
persistState?: boolean;
|
|
34
|
+
}
|
|
35
|
+
declare function mongoStorage<TFilter, TBlock>({ client, dbName, dbOptions, collections, persistState: enablePersistence, }: MongoStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
36
|
+
|
|
37
|
+
export { MongoCollection, MongoStorage, type MongoStorageOptions, mongoStorage, useMongoStorage };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as _apibara_indexer_plugins from '@apibara/indexer/plugins';
|
|
2
|
+
import { Db, ClientSession, Document, CollectionOptions, Collection, OptionalUnlessRequiredId, InsertOneOptions, InsertOneResult, BulkWriteOptions, InsertManyResult, Filter, UpdateFilter, UpdateOptions, UpdateResult, DeleteOptions, FindOptions, WithId, FindCursor, MongoClient, DbOptions } from 'mongodb';
|
|
3
|
+
import { Cursor } from '@apibara/protocol';
|
|
4
|
+
|
|
5
|
+
declare class MongoStorage {
|
|
6
|
+
private db;
|
|
7
|
+
private session;
|
|
8
|
+
private endCursor?;
|
|
9
|
+
constructor(db: Db, session: ClientSession, endCursor?: Cursor | undefined);
|
|
10
|
+
collection<TSchema extends Document = Document>(name: string, options?: CollectionOptions): MongoCollection<TSchema>;
|
|
11
|
+
}
|
|
12
|
+
declare class MongoCollection<TSchema extends Document> {
|
|
13
|
+
private session;
|
|
14
|
+
private collection;
|
|
15
|
+
private endCursor?;
|
|
16
|
+
constructor(session: ClientSession, collection: Collection<TSchema>, endCursor?: Cursor | undefined);
|
|
17
|
+
insertOne(doc: OptionalUnlessRequiredId<TSchema>, options?: InsertOneOptions): Promise<InsertOneResult<TSchema>>;
|
|
18
|
+
insertMany(docs: ReadonlyArray<OptionalUnlessRequiredId<TSchema>>, options?: BulkWriteOptions): Promise<InsertManyResult<TSchema>>;
|
|
19
|
+
updateOne(filter: Filter<TSchema>, update: UpdateFilter<TSchema>, options?: UpdateOptions): Promise<UpdateResult<TSchema>>;
|
|
20
|
+
updateMany(filter: Filter<TSchema>, update: UpdateFilter<TSchema>, options?: UpdateOptions): Promise<UpdateResult<TSchema>>;
|
|
21
|
+
deleteOne(filter: Filter<TSchema>, options?: DeleteOptions): Promise<UpdateResult<TSchema>>;
|
|
22
|
+
deleteMany(filter?: Filter<TSchema>, options?: DeleteOptions): Promise<UpdateResult<TSchema>>;
|
|
23
|
+
findOne(filter: Filter<TSchema>, options?: Omit<FindOptions, "timeoutMode">): Promise<WithId<TSchema> | null>;
|
|
24
|
+
find(filter: Filter<TSchema>, options?: FindOptions): FindCursor<WithId<TSchema>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare function useMongoStorage(): MongoStorage;
|
|
28
|
+
interface MongoStorageOptions {
|
|
29
|
+
client: MongoClient;
|
|
30
|
+
dbName: string;
|
|
31
|
+
dbOptions?: DbOptions;
|
|
32
|
+
collections: string[];
|
|
33
|
+
persistState?: boolean;
|
|
34
|
+
}
|
|
35
|
+
declare function mongoStorage<TFilter, TBlock>({ client, dbName, dbOptions, collections, persistState: enablePersistence, }: MongoStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
36
|
+
|
|
37
|
+
export { MongoCollection, MongoStorage, type MongoStorageOptions, mongoStorage, useMongoStorage };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { useIndexerContext } from '@apibara/indexer';
|
|
2
|
+
import { defineIndexerPlugin } from '@apibara/indexer/plugins';
|
|
3
|
+
|
|
4
|
+
async function invalidate(db, session, cursor, collections) {
|
|
5
|
+
const orderKeyValue = Number(cursor.orderKey);
|
|
6
|
+
for (const collection of collections) {
|
|
7
|
+
await db.collection(collection).deleteMany(
|
|
8
|
+
{
|
|
9
|
+
"_cursor.from": {
|
|
10
|
+
$gt: orderKeyValue
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
{ session }
|
|
14
|
+
);
|
|
15
|
+
await db.collection(collection).updateMany(
|
|
16
|
+
{ "_cursor.to": { $gt: orderKeyValue } },
|
|
17
|
+
{
|
|
18
|
+
$set: {
|
|
19
|
+
"_cursor.to": null
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{ session }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function finalize(db, session, cursor, collections) {
|
|
27
|
+
const orderKeyValue = Number(cursor.orderKey);
|
|
28
|
+
for (const collection of collections) {
|
|
29
|
+
await db.collection(collection).deleteMany(
|
|
30
|
+
{
|
|
31
|
+
"_cursor.to": { $lt: orderKeyValue }
|
|
32
|
+
},
|
|
33
|
+
{ session }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class MongoStorage {
|
|
39
|
+
constructor(db, session, endCursor) {
|
|
40
|
+
this.db = db;
|
|
41
|
+
this.session = session;
|
|
42
|
+
this.endCursor = endCursor;
|
|
43
|
+
}
|
|
44
|
+
collection(name, options) {
|
|
45
|
+
const collection = this.db.collection(name, options);
|
|
46
|
+
return new MongoCollection(
|
|
47
|
+
this.session,
|
|
48
|
+
collection,
|
|
49
|
+
this.endCursor
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
class MongoCollection {
|
|
54
|
+
constructor(session, collection, endCursor) {
|
|
55
|
+
this.session = session;
|
|
56
|
+
this.collection = collection;
|
|
57
|
+
this.endCursor = endCursor;
|
|
58
|
+
}
|
|
59
|
+
async insertOne(doc, options) {
|
|
60
|
+
return await this.collection.insertOne(
|
|
61
|
+
{
|
|
62
|
+
...doc,
|
|
63
|
+
_cursor: {
|
|
64
|
+
from: Number(this.endCursor?.orderKey),
|
|
65
|
+
to: null
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{ ...options, session: this.session }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
async insertMany(docs, options) {
|
|
72
|
+
return await this.collection.insertMany(
|
|
73
|
+
docs.map((doc) => ({
|
|
74
|
+
...doc,
|
|
75
|
+
_cursor: {
|
|
76
|
+
from: Number(this.endCursor?.orderKey),
|
|
77
|
+
to: null
|
|
78
|
+
}
|
|
79
|
+
})),
|
|
80
|
+
{ ...options, session: this.session }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
async updateOne(filter, update, options) {
|
|
84
|
+
const oldDoc = await this.collection.findOneAndUpdate(
|
|
85
|
+
{
|
|
86
|
+
...filter,
|
|
87
|
+
"_cursor.to": null
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
...update,
|
|
91
|
+
$set: {
|
|
92
|
+
...update.$set,
|
|
93
|
+
"_cursor.from": Number(this.endCursor?.orderKey)
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
...options,
|
|
98
|
+
session: this.session,
|
|
99
|
+
returnDocument: "before"
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
if (oldDoc) {
|
|
103
|
+
const { _id, ...doc } = oldDoc;
|
|
104
|
+
await this.collection.insertOne(
|
|
105
|
+
{
|
|
106
|
+
...doc,
|
|
107
|
+
_cursor: {
|
|
108
|
+
...oldDoc._cursor,
|
|
109
|
+
to: Number(this.endCursor?.orderKey)
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{ session: this.session }
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
acknowledged: true,
|
|
117
|
+
modifiedCount: oldDoc ? 1 : 0,
|
|
118
|
+
upsertedId: null,
|
|
119
|
+
upsertedCount: 0,
|
|
120
|
+
matchedCount: oldDoc ? 1 : 0
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async updateMany(filter, update, options) {
|
|
124
|
+
const oldDocs = await this.collection.find(
|
|
125
|
+
{
|
|
126
|
+
...filter,
|
|
127
|
+
"_cursor.to": null
|
|
128
|
+
},
|
|
129
|
+
{ session: this.session }
|
|
130
|
+
).toArray();
|
|
131
|
+
const updateResult = await this.collection.updateMany(
|
|
132
|
+
{
|
|
133
|
+
...filter,
|
|
134
|
+
"_cursor.to": null
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
...update,
|
|
138
|
+
$set: {
|
|
139
|
+
...update.$set,
|
|
140
|
+
"_cursor.from": Number(this.endCursor?.orderKey)
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{ ...options, session: this.session }
|
|
144
|
+
);
|
|
145
|
+
const oldDocsWithUpdatedCursor = oldDocs.map(({ _id, ...doc }) => ({
|
|
146
|
+
...doc,
|
|
147
|
+
_cursor: {
|
|
148
|
+
...doc._cursor,
|
|
149
|
+
to: Number(this.endCursor?.orderKey)
|
|
150
|
+
}
|
|
151
|
+
}));
|
|
152
|
+
if (oldDocsWithUpdatedCursor.length > 0) {
|
|
153
|
+
await this.collection.insertMany(
|
|
154
|
+
oldDocsWithUpdatedCursor,
|
|
155
|
+
{ session: this.session }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return updateResult;
|
|
159
|
+
}
|
|
160
|
+
async deleteOne(filter, options) {
|
|
161
|
+
return await this.collection.updateOne(
|
|
162
|
+
{
|
|
163
|
+
...filter,
|
|
164
|
+
"_cursor.to": null
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
$set: {
|
|
168
|
+
"_cursor.to": Number(this.endCursor?.orderKey)
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{ ...options, session: this.session }
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
async deleteMany(filter, options) {
|
|
175
|
+
return await this.collection.updateMany(
|
|
176
|
+
{
|
|
177
|
+
...filter ?? {},
|
|
178
|
+
"_cursor.to": null
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
$set: {
|
|
182
|
+
"_cursor.to": Number(this.endCursor?.orderKey)
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{ ...options, session: this.session }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
async findOne(filter, options) {
|
|
189
|
+
return await this.collection.findOne(
|
|
190
|
+
{
|
|
191
|
+
...filter,
|
|
192
|
+
"_cursor.to": null
|
|
193
|
+
},
|
|
194
|
+
{ ...options, session: this.session }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
find(filter, options) {
|
|
198
|
+
return this.collection.find(
|
|
199
|
+
{
|
|
200
|
+
...filter,
|
|
201
|
+
"_cursor.to": null
|
|
202
|
+
},
|
|
203
|
+
{ ...options, session: this.session }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
class MongoStorageError extends Error {
|
|
209
|
+
constructor(message) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.name = "MongoStorageError";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function withTransaction(client, cb) {
|
|
215
|
+
return await client.withSession(async (session) => {
|
|
216
|
+
return await session.withTransaction(async (session2) => {
|
|
217
|
+
return await cb(session2);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const MONGO_PROPERTY = "_mongo";
|
|
223
|
+
function useMongoStorage() {
|
|
224
|
+
const context = useIndexerContext();
|
|
225
|
+
if (!context[MONGO_PROPERTY]) {
|
|
226
|
+
throw new MongoStorageError(
|
|
227
|
+
"mongo storage is not available. Did you register the plugin?"
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return context[MONGO_PROPERTY];
|
|
231
|
+
}
|
|
232
|
+
function mongoStorage({
|
|
233
|
+
client,
|
|
234
|
+
dbName,
|
|
235
|
+
dbOptions,
|
|
236
|
+
collections,
|
|
237
|
+
persistState: enablePersistence = true
|
|
238
|
+
}) {
|
|
239
|
+
return defineIndexerPlugin((indexer) => {
|
|
240
|
+
indexer.hooks.hook("message:finalize", async ({ message }) => {
|
|
241
|
+
const { cursor } = message.finalize;
|
|
242
|
+
if (!cursor) {
|
|
243
|
+
throw new MongoStorageError("finalized cursor is undefined");
|
|
244
|
+
}
|
|
245
|
+
await withTransaction(client, async (session) => {
|
|
246
|
+
const db = client.db(dbName, dbOptions);
|
|
247
|
+
await finalize(db, session, cursor, collections);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
indexer.hooks.hook("message:invalidate", async ({ message }) => {
|
|
251
|
+
const { cursor } = message.invalidate;
|
|
252
|
+
if (!cursor) {
|
|
253
|
+
throw new MongoStorageError("invalidate cursor is undefined");
|
|
254
|
+
}
|
|
255
|
+
await withTransaction(client, async (session) => {
|
|
256
|
+
const db = client.db(dbName, dbOptions);
|
|
257
|
+
await invalidate(db, session, cursor, collections);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
indexer.hooks.hook("connect:after", async ({ request }) => {
|
|
261
|
+
const cursor = request.startingCursor;
|
|
262
|
+
if (!cursor) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
await withTransaction(client, async (session) => {
|
|
266
|
+
const db = client.db(dbName, dbOptions);
|
|
267
|
+
await invalidate(db, session, cursor, collections);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
indexer.hooks.hook("handler:middleware", async ({ use }) => {
|
|
271
|
+
use(async (context, next) => {
|
|
272
|
+
const { endCursor } = context;
|
|
273
|
+
if (!endCursor) {
|
|
274
|
+
throw new MongoStorageError("end cursor is undefined");
|
|
275
|
+
}
|
|
276
|
+
await withTransaction(client, async (session) => {
|
|
277
|
+
const db = client.db(dbName, dbOptions);
|
|
278
|
+
context[MONGO_PROPERTY] = new MongoStorage(db, session, endCursor);
|
|
279
|
+
await next();
|
|
280
|
+
delete context[MONGO_PROPERTY];
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export { MongoCollection, MongoStorage, mongoStorage, useMongoStorage };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@apibara/plugin-mongo",
|
|
3
|
+
"version": "2.0.0-beta.28",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"src",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"main": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"require": "./dist/index.cjs",
|
|
17
|
+
"default": "./dist/index.mjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "unbuild",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"lint": "biome check .",
|
|
24
|
+
"lint:fix": "pnpm lint --write",
|
|
25
|
+
"test": "vitest",
|
|
26
|
+
"test:ci": "vitest run"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.14.0",
|
|
30
|
+
"mongodb": "^6.12.0",
|
|
31
|
+
"unbuild": "^2.0.0",
|
|
32
|
+
"vitest": "^1.6.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"mongodb": "^6.12.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@apibara/indexer": "2.0.0-beta.28",
|
|
39
|
+
"@apibara/protocol": "2.0.0-beta.28"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useIndexerContext } from "@apibara/indexer";
|
|
2
|
+
import { defineIndexerPlugin } from "@apibara/indexer/plugins";
|
|
3
|
+
import type { DbOptions, MongoClient } from "mongodb";
|
|
4
|
+
|
|
5
|
+
import { finalize, invalidate } from "./mongo";
|
|
6
|
+
import { MongoStorage } from "./storage";
|
|
7
|
+
import { MongoStorageError, withTransaction } from "./utils";
|
|
8
|
+
|
|
9
|
+
export { MongoCollection, MongoStorage } from "./storage";
|
|
10
|
+
|
|
11
|
+
const MONGO_PROPERTY = "_mongo";
|
|
12
|
+
|
|
13
|
+
export function useMongoStorage(): MongoStorage {
|
|
14
|
+
const context = useIndexerContext();
|
|
15
|
+
|
|
16
|
+
if (!context[MONGO_PROPERTY]) {
|
|
17
|
+
throw new MongoStorageError(
|
|
18
|
+
"mongo storage is not available. Did you register the plugin?",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return context[MONGO_PROPERTY] as MongoStorage;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MongoStorageOptions {
|
|
26
|
+
client: MongoClient;
|
|
27
|
+
dbName: string;
|
|
28
|
+
dbOptions?: DbOptions;
|
|
29
|
+
collections: string[];
|
|
30
|
+
persistState?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function mongoStorage<TFilter, TBlock>({
|
|
34
|
+
client,
|
|
35
|
+
dbName,
|
|
36
|
+
dbOptions,
|
|
37
|
+
collections,
|
|
38
|
+
persistState: enablePersistence = true,
|
|
39
|
+
}: MongoStorageOptions) {
|
|
40
|
+
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
41
|
+
indexer.hooks.hook("message:finalize", async ({ message }) => {
|
|
42
|
+
const { cursor } = message.finalize;
|
|
43
|
+
|
|
44
|
+
if (!cursor) {
|
|
45
|
+
throw new MongoStorageError("finalized cursor is undefined");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await withTransaction(client, async (session) => {
|
|
49
|
+
const db = client.db(dbName, dbOptions);
|
|
50
|
+
await finalize(db, session, cursor, collections);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
indexer.hooks.hook("message:invalidate", async ({ message }) => {
|
|
55
|
+
const { cursor } = message.invalidate;
|
|
56
|
+
|
|
57
|
+
if (!cursor) {
|
|
58
|
+
throw new MongoStorageError("invalidate cursor is undefined");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await withTransaction(client, async (session) => {
|
|
62
|
+
const db = client.db(dbName, dbOptions);
|
|
63
|
+
await invalidate(db, session, cursor, collections);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
indexer.hooks.hook("connect:after", async ({ request }) => {
|
|
68
|
+
// On restart, we need to invalidate data for blocks that were processed but not persisted.
|
|
69
|
+
const cursor = request.startingCursor;
|
|
70
|
+
|
|
71
|
+
if (!cursor) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await withTransaction(client, async (session) => {
|
|
76
|
+
const db = client.db(dbName, dbOptions);
|
|
77
|
+
await invalidate(db, session, cursor, collections);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
indexer.hooks.hook("handler:middleware", async ({ use }) => {
|
|
82
|
+
use(async (context, next) => {
|
|
83
|
+
const { endCursor } = context;
|
|
84
|
+
|
|
85
|
+
if (!endCursor) {
|
|
86
|
+
throw new MongoStorageError("end cursor is undefined");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await withTransaction(client, async (session) => {
|
|
90
|
+
const db = client.db(dbName, dbOptions);
|
|
91
|
+
context[MONGO_PROPERTY] = new MongoStorage(db, session, endCursor);
|
|
92
|
+
|
|
93
|
+
await next();
|
|
94
|
+
|
|
95
|
+
delete context[MONGO_PROPERTY];
|
|
96
|
+
|
|
97
|
+
if (enablePersistence) {
|
|
98
|
+
// TODO: persist state
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
package/src/mongo.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Cursor } from "@apibara/protocol";
|
|
2
|
+
import type { ClientSession, Db } from "mongodb";
|
|
3
|
+
|
|
4
|
+
export async function invalidate(
|
|
5
|
+
db: Db,
|
|
6
|
+
session: ClientSession,
|
|
7
|
+
cursor: Cursor,
|
|
8
|
+
collections: string[],
|
|
9
|
+
) {
|
|
10
|
+
const orderKeyValue = Number(cursor.orderKey);
|
|
11
|
+
for (const collection of collections) {
|
|
12
|
+
// Delete documents where the lower bound of _cursor is greater than the invalidate cursor
|
|
13
|
+
await db.collection(collection).deleteMany(
|
|
14
|
+
{
|
|
15
|
+
"_cursor.from": {
|
|
16
|
+
$gt: orderKeyValue,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{ session },
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Update documents where the upper bound of _cursor is greater than the invalidate cursor
|
|
23
|
+
await db.collection(collection).updateMany(
|
|
24
|
+
{ "_cursor.to": { $gt: orderKeyValue } },
|
|
25
|
+
{
|
|
26
|
+
$set: {
|
|
27
|
+
"_cursor.to": null,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{ session },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function finalize(
|
|
36
|
+
db: Db,
|
|
37
|
+
session: ClientSession,
|
|
38
|
+
cursor: Cursor,
|
|
39
|
+
collections: string[],
|
|
40
|
+
) {
|
|
41
|
+
const orderKeyValue = Number(cursor.orderKey);
|
|
42
|
+
for (const collection of collections) {
|
|
43
|
+
// Delete documents where the upper bound of _cursor is less than the finalize cursor
|
|
44
|
+
await db.collection(collection).deleteMany(
|
|
45
|
+
{
|
|
46
|
+
"_cursor.to": { $lt: orderKeyValue },
|
|
47
|
+
},
|
|
48
|
+
{ session },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { Cursor } from "@apibara/protocol";
|
|
2
|
+
import type {
|
|
3
|
+
BulkWriteOptions,
|
|
4
|
+
ClientSession,
|
|
5
|
+
Collection,
|
|
6
|
+
CollectionOptions,
|
|
7
|
+
Db,
|
|
8
|
+
DeleteOptions,
|
|
9
|
+
Document,
|
|
10
|
+
Filter,
|
|
11
|
+
FindCursor,
|
|
12
|
+
FindOneAndUpdateOptions,
|
|
13
|
+
FindOptions,
|
|
14
|
+
InsertManyResult,
|
|
15
|
+
InsertOneOptions,
|
|
16
|
+
InsertOneResult,
|
|
17
|
+
MatchKeysAndValues,
|
|
18
|
+
OptionalUnlessRequiredId,
|
|
19
|
+
UpdateFilter,
|
|
20
|
+
UpdateOptions,
|
|
21
|
+
UpdateResult,
|
|
22
|
+
WithId,
|
|
23
|
+
} from "mongodb";
|
|
24
|
+
|
|
25
|
+
export class MongoStorage {
|
|
26
|
+
constructor(
|
|
27
|
+
private db: Db,
|
|
28
|
+
private session: ClientSession,
|
|
29
|
+
private endCursor?: Cursor,
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
collection<TSchema extends Document = Document>(
|
|
33
|
+
name: string,
|
|
34
|
+
options?: CollectionOptions,
|
|
35
|
+
) {
|
|
36
|
+
const collection = this.db.collection<TSchema>(name, options);
|
|
37
|
+
|
|
38
|
+
return new MongoCollection<TSchema>(
|
|
39
|
+
this.session,
|
|
40
|
+
collection,
|
|
41
|
+
this.endCursor,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type MongoCursor = {
|
|
47
|
+
from: number | null;
|
|
48
|
+
to: number | null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type CursoredSchema<TSchema extends Document> = TSchema & {
|
|
52
|
+
_cursor: MongoCursor;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export class MongoCollection<TSchema extends Document> {
|
|
56
|
+
constructor(
|
|
57
|
+
private session: ClientSession,
|
|
58
|
+
private collection: Collection<TSchema>,
|
|
59
|
+
private endCursor?: Cursor,
|
|
60
|
+
) {}
|
|
61
|
+
|
|
62
|
+
async insertOne(
|
|
63
|
+
doc: OptionalUnlessRequiredId<TSchema>,
|
|
64
|
+
options?: InsertOneOptions,
|
|
65
|
+
): Promise<InsertOneResult<TSchema>> {
|
|
66
|
+
return await this.collection.insertOne(
|
|
67
|
+
{
|
|
68
|
+
...doc,
|
|
69
|
+
_cursor: {
|
|
70
|
+
from: Number(this.endCursor?.orderKey),
|
|
71
|
+
to: null,
|
|
72
|
+
} as MongoCursor,
|
|
73
|
+
},
|
|
74
|
+
{ ...options, session: this.session },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async insertMany(
|
|
79
|
+
docs: ReadonlyArray<OptionalUnlessRequiredId<TSchema>>,
|
|
80
|
+
options?: BulkWriteOptions,
|
|
81
|
+
): Promise<InsertManyResult<TSchema>> {
|
|
82
|
+
return await this.collection.insertMany(
|
|
83
|
+
docs.map((doc) => ({
|
|
84
|
+
...doc,
|
|
85
|
+
_cursor: {
|
|
86
|
+
from: Number(this.endCursor?.orderKey),
|
|
87
|
+
to: null,
|
|
88
|
+
} as MongoCursor,
|
|
89
|
+
})),
|
|
90
|
+
{ ...options, session: this.session },
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async updateOne(
|
|
95
|
+
filter: Filter<TSchema>,
|
|
96
|
+
update: UpdateFilter<TSchema>,
|
|
97
|
+
options?: UpdateOptions,
|
|
98
|
+
): Promise<UpdateResult<TSchema>> {
|
|
99
|
+
// 1. Find and update the document, getting the old version
|
|
100
|
+
const oldDoc = await this.collection.findOneAndUpdate(
|
|
101
|
+
{
|
|
102
|
+
...filter,
|
|
103
|
+
"_cursor.to": null,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
...update,
|
|
107
|
+
$set: {
|
|
108
|
+
...update.$set,
|
|
109
|
+
"_cursor.from": Number(this.endCursor?.orderKey),
|
|
110
|
+
} as unknown as MatchKeysAndValues<TSchema>,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
...options,
|
|
114
|
+
session: this.session,
|
|
115
|
+
returnDocument: "before",
|
|
116
|
+
} as FindOneAndUpdateOptions,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// 2. If we found and updated a document, insert its old version
|
|
120
|
+
if (oldDoc) {
|
|
121
|
+
const { _id, ...doc } = oldDoc;
|
|
122
|
+
await this.collection.insertOne(
|
|
123
|
+
{
|
|
124
|
+
...doc,
|
|
125
|
+
_cursor: {
|
|
126
|
+
...oldDoc._cursor,
|
|
127
|
+
to: Number(this.endCursor?.orderKey),
|
|
128
|
+
},
|
|
129
|
+
} as unknown as OptionalUnlessRequiredId<TSchema>,
|
|
130
|
+
{ session: this.session },
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3. Return an UpdateResult-compatible object
|
|
135
|
+
return {
|
|
136
|
+
acknowledged: true,
|
|
137
|
+
modifiedCount: oldDoc ? 1 : 0,
|
|
138
|
+
upsertedId: null,
|
|
139
|
+
upsertedCount: 0,
|
|
140
|
+
matchedCount: oldDoc ? 1 : 0,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async updateMany(
|
|
145
|
+
filter: Filter<TSchema>,
|
|
146
|
+
update: UpdateFilter<TSchema>,
|
|
147
|
+
options?: UpdateOptions,
|
|
148
|
+
): Promise<UpdateResult<TSchema>> {
|
|
149
|
+
// 1. Find all documents matching the filter that are latest (to: null)
|
|
150
|
+
const oldDocs = await this.collection
|
|
151
|
+
.find(
|
|
152
|
+
{
|
|
153
|
+
...filter,
|
|
154
|
+
"_cursor.to": null,
|
|
155
|
+
},
|
|
156
|
+
{ session: this.session },
|
|
157
|
+
)
|
|
158
|
+
.toArray();
|
|
159
|
+
|
|
160
|
+
// 2. Update to the new values with updateMany
|
|
161
|
+
// (setting _cursor.from to endCursor, leaving _cursor.to unchanged)
|
|
162
|
+
const updateResult = await this.collection.updateMany(
|
|
163
|
+
{
|
|
164
|
+
...filter,
|
|
165
|
+
"_cursor.to": null,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
...update,
|
|
169
|
+
$set: {
|
|
170
|
+
...update.$set,
|
|
171
|
+
"_cursor.from": Number(this.endCursor?.orderKey),
|
|
172
|
+
} as unknown as MatchKeysAndValues<TSchema>,
|
|
173
|
+
},
|
|
174
|
+
{ ...options, session: this.session },
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// 3. Adjust the cursor.to of the old values
|
|
178
|
+
const oldDocsWithUpdatedCursor = oldDocs.map(({ _id, ...doc }) => ({
|
|
179
|
+
...doc,
|
|
180
|
+
_cursor: {
|
|
181
|
+
...doc._cursor,
|
|
182
|
+
to: Number(this.endCursor?.orderKey),
|
|
183
|
+
},
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
// 4. Insert the old values back into the db
|
|
187
|
+
if (oldDocsWithUpdatedCursor.length > 0) {
|
|
188
|
+
await this.collection.insertMany(
|
|
189
|
+
oldDocsWithUpdatedCursor as unknown as OptionalUnlessRequiredId<TSchema>[],
|
|
190
|
+
{ session: this.session },
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return updateResult;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async deleteOne(
|
|
198
|
+
filter: Filter<TSchema>,
|
|
199
|
+
options?: DeleteOptions,
|
|
200
|
+
): Promise<UpdateResult<TSchema>> {
|
|
201
|
+
return await this.collection.updateOne(
|
|
202
|
+
{
|
|
203
|
+
...filter,
|
|
204
|
+
"_cursor.to": null,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
$set: {
|
|
208
|
+
"_cursor.to": Number(this.endCursor?.orderKey),
|
|
209
|
+
} as unknown as MatchKeysAndValues<TSchema>,
|
|
210
|
+
},
|
|
211
|
+
{ ...options, session: this.session },
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async deleteMany(
|
|
216
|
+
filter?: Filter<TSchema>,
|
|
217
|
+
options?: DeleteOptions,
|
|
218
|
+
): Promise<UpdateResult<TSchema>> {
|
|
219
|
+
return await this.collection.updateMany(
|
|
220
|
+
{
|
|
221
|
+
...((filter ?? {}) as Filter<TSchema>),
|
|
222
|
+
"_cursor.to": null,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
$set: {
|
|
226
|
+
"_cursor.to": Number(this.endCursor?.orderKey),
|
|
227
|
+
} as unknown as MatchKeysAndValues<TSchema>,
|
|
228
|
+
},
|
|
229
|
+
{ ...options, session: this.session },
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async findOne(
|
|
234
|
+
filter: Filter<TSchema>,
|
|
235
|
+
options?: Omit<FindOptions, "timeoutMode">,
|
|
236
|
+
): Promise<WithId<TSchema> | null> {
|
|
237
|
+
return await this.collection.findOne(
|
|
238
|
+
{
|
|
239
|
+
...filter,
|
|
240
|
+
"_cursor.to": null,
|
|
241
|
+
},
|
|
242
|
+
{ ...options, session: this.session },
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
find(
|
|
247
|
+
filter: Filter<TSchema>,
|
|
248
|
+
options?: FindOptions,
|
|
249
|
+
): FindCursor<WithId<TSchema>> {
|
|
250
|
+
return this.collection.find(
|
|
251
|
+
{
|
|
252
|
+
...filter,
|
|
253
|
+
"_cursor.to": null,
|
|
254
|
+
},
|
|
255
|
+
{ ...options, session: this.session },
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ClientSession, MongoClient } from "mongodb";
|
|
2
|
+
|
|
3
|
+
export class MongoStorageError extends Error {
|
|
4
|
+
constructor(message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "MongoStorageError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function withTransaction<T>(
|
|
11
|
+
client: MongoClient,
|
|
12
|
+
cb: (session: ClientSession) => Promise<T>,
|
|
13
|
+
) {
|
|
14
|
+
return await client.withSession(async (session) => {
|
|
15
|
+
return await session.withTransaction(async (session) => {
|
|
16
|
+
return await cb(session);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|