@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 ADDED
@@ -0,0 +1,7 @@
1
+ # `@apibara/sink-mongo`
2
+
3
+ TODO
4
+
5
+ ## Installation
6
+
7
+ TODO
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;
@@ -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 };
@@ -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 };
@@ -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
+ }