@twasik4/pocket-service 1.0.0
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 +241 -0
- package/package.json +39 -0
- package/src/index.ts +1 -0
- package/src/worker-thread.ts +159 -0
- package/src/workers/db/clickhouse/index.ts +76 -0
- package/src/workers/db/mongo/collection.ts +292 -0
- package/src/workers/db/mongo/mongo.ts +285 -0
- package/src/workers/express-types.ts +314 -0
- package/src/workers/index.ts +6 -0
- package/src/workers/logger.ts +19 -0
- package/src/workers/service.ts +921 -0
- package/src/workers/stream-handler.ts +25 -0
- package/src/workers/types.ts +59 -0
- package/src/workers/utils.ts +19 -0
- package/tests/mongo.spec.ts +92 -0
- package/tests/redis.spec.ts +36 -0
- package/tests/service.spec.ts +82 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Collection,
|
|
3
|
+
Document,
|
|
4
|
+
Filter,
|
|
5
|
+
ObjectId,
|
|
6
|
+
OptionalUnlessRequiredId,
|
|
7
|
+
UpdateFilter,
|
|
8
|
+
WithId,
|
|
9
|
+
} from "mongodb";
|
|
10
|
+
|
|
11
|
+
export type SchemaParser<T extends Document> = {
|
|
12
|
+
parse: (input: unknown) => T;
|
|
13
|
+
shape?: unknown;
|
|
14
|
+
_def?: {
|
|
15
|
+
shape?: unknown | (() => unknown);
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class CollectionWrapper<T extends Document> {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly collection: Collection<T>,
|
|
22
|
+
private readonly schema?: SchemaParser<T>,
|
|
23
|
+
private readonly revision?: Collection<T>,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
private parseWrite(doc: unknown): T {
|
|
27
|
+
if (!this.schema) {
|
|
28
|
+
return doc as T;
|
|
29
|
+
}
|
|
30
|
+
return this.schema.parse(doc);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private parseRead(doc: WithId<T>): WithId<T> {
|
|
34
|
+
if (!this.schema) {
|
|
35
|
+
return doc;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parsed = this.schema.parse(doc) as T;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...(parsed as Document),
|
|
42
|
+
_id: doc._id,
|
|
43
|
+
} as WithId<T>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async trackRevision(doc: T) {
|
|
47
|
+
if (this.revision) {
|
|
48
|
+
const clone = {
|
|
49
|
+
...doc,
|
|
50
|
+
_id: new ObjectId(),
|
|
51
|
+
} as OptionalUnlessRequiredId<T>;
|
|
52
|
+
await this.revision.insertOne(clone);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get all documents from the collection.
|
|
58
|
+
* @param filter - The filter to find documents to retrieve. If no filter is provided, it will return all documents in the collection.
|
|
59
|
+
* @returns
|
|
60
|
+
*/
|
|
61
|
+
async get(filter: Filter<T> = {}): Promise<WithId<T>[]> {
|
|
62
|
+
try {
|
|
63
|
+
if (this.hasProperty("deletedAt")) {
|
|
64
|
+
// If the schema has a `deletedAt` field, filter out soft-deleted documents
|
|
65
|
+
filter = { ...filter, deletedAt: { $exists: false } };
|
|
66
|
+
}
|
|
67
|
+
const result = await this.collection.find(filter).toArray();
|
|
68
|
+
return result.map((doc) => this.parseRead(doc));
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get a single document from the collection.
|
|
76
|
+
* @param filter - The filter to find the document to retrieve. If no filter is provided, it will return the first document in the collection.
|
|
77
|
+
* @returns
|
|
78
|
+
*/
|
|
79
|
+
async getOne(filter: Filter<T> = {}): Promise<WithId<T> | null> {
|
|
80
|
+
try {
|
|
81
|
+
if (this.hasProperty("deletedAt")) {
|
|
82
|
+
// If the schema has a `deletedAt` field, filter out soft-deleted documents
|
|
83
|
+
filter = { ...filter, deletedAt: { $exists: false } };
|
|
84
|
+
}
|
|
85
|
+
const result = await this.collection.findOne(filter);
|
|
86
|
+
return result ? this.parseRead(result) : null;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get all revisions for a document.
|
|
94
|
+
* @param filter - The filter to find revisions to retrieve. If no filter is provided, it will return all revisions in the revision collection.
|
|
95
|
+
* @returns An array of revision documents.
|
|
96
|
+
*/
|
|
97
|
+
async getRevisions(filter: Filter<T> = {}): Promise<WithId<T>[]> {
|
|
98
|
+
try {
|
|
99
|
+
if (!this.revision) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const result = await this.revision
|
|
103
|
+
.find(filter, { sort: { revision: 1 } })
|
|
104
|
+
.toArray();
|
|
105
|
+
return result.map((doc) => this.parseRead(doc as WithId<T>));
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the most recent revision for a document.
|
|
113
|
+
* @returns Revision document or null if no revisions exist or if revision tracking is not enabled.
|
|
114
|
+
*/
|
|
115
|
+
async getMostRecentRevision(): Promise<WithId<T> | null> {
|
|
116
|
+
try {
|
|
117
|
+
if (!this.revision) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const result = await this.revision
|
|
121
|
+
.find({}, { sort: { revision: -1 } })
|
|
122
|
+
.limit(1)
|
|
123
|
+
.toArray();
|
|
124
|
+
return result.length > 0 ? this.parseRead(result[0] as WithId<T>) : null;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Insert a document into the collection.
|
|
132
|
+
*
|
|
133
|
+
* If this collection has revision tracking enabled, it will also insert the document into the revision collection.
|
|
134
|
+
*
|
|
135
|
+
* @param doc - The document to insert into the collection.
|
|
136
|
+
*/
|
|
137
|
+
async insert(doc: T): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = this.parseWrite(doc);
|
|
140
|
+
await this.collection.insertOne(parsed as OptionalUnlessRequiredId<T>);
|
|
141
|
+
await this.trackRevision(parsed);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
throw new Error(`Failed to insert document: ${(err as Error).message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Update multiple documents by filter with a partial document.
|
|
149
|
+
* @param filter - The filter to find documents to update.
|
|
150
|
+
* @param update - The partial document to update.
|
|
151
|
+
*/
|
|
152
|
+
async update(filter: Filter<T>, update: Partial<T>): Promise<void> {
|
|
153
|
+
try {
|
|
154
|
+
const existing = await this.collection.find(filter).toArray();
|
|
155
|
+
if (!existing.length) return;
|
|
156
|
+
|
|
157
|
+
await this.collection.updateMany(filter, { $set: update });
|
|
158
|
+
|
|
159
|
+
// record updated versions in revisions
|
|
160
|
+
if (this.revision) {
|
|
161
|
+
const updatedDocs = existing.map((doc) => ({
|
|
162
|
+
...doc,
|
|
163
|
+
...update,
|
|
164
|
+
_id: new ObjectId(),
|
|
165
|
+
}));
|
|
166
|
+
await this.revision.insertMany(
|
|
167
|
+
updatedDocs as OptionalUnlessRequiredId<T>[],
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
throw new Error(`Failed to update documents: ${(err as Error).message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Update a single document by filter with a partial document. This will automatically set the `updatedAt` field to the current date if it exists in the schema, and increment the `revision` field by 1 if it exists in the schema. It will also insert the updated document into the revision collection if revision tracking is enabled.
|
|
177
|
+
* @param filter - The filter to find the document to update.
|
|
178
|
+
* @param update - The partial document to update.
|
|
179
|
+
*/
|
|
180
|
+
async updateOne(filter: Filter<T>, update: Partial<T>): Promise<void> {
|
|
181
|
+
try {
|
|
182
|
+
const incomingUpdate = this.hasProperty("updatedAt")
|
|
183
|
+
? { ...update, updatedAt: new Date() }
|
|
184
|
+
: update;
|
|
185
|
+
const updateDoc = {
|
|
186
|
+
$set: incomingUpdate,
|
|
187
|
+
...(this.revision ? { $inc: { revision: 1 } } : {}),
|
|
188
|
+
} as UpdateFilter<T>;
|
|
189
|
+
|
|
190
|
+
await this.collection.updateOne(filter, updateDoc);
|
|
191
|
+
if (this.revision) {
|
|
192
|
+
await this.collection
|
|
193
|
+
.aggregate([
|
|
194
|
+
{
|
|
195
|
+
$match: filter,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
$set: { ...update, _id: new ObjectId() },
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
$merge: {
|
|
202
|
+
into: {
|
|
203
|
+
db: this.collection.dbName,
|
|
204
|
+
col: this.revision.collectionName,
|
|
205
|
+
on: "_id",
|
|
206
|
+
whenNotMatched: "insert",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
])
|
|
211
|
+
.toArray();
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error("Failed to update document:", err);
|
|
215
|
+
throw new Error(`Failed to update document: ${(err as Error).message}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Soft delete a document by setting the `deletedAt` field to the current date.
|
|
221
|
+
* If the schema does not have a `deletedAt` field, it will perform a hard delete.
|
|
222
|
+
* @param filter - The filter to find documents to delete.
|
|
223
|
+
*/
|
|
224
|
+
async delete(filter: Filter<T>): Promise<void> {
|
|
225
|
+
try {
|
|
226
|
+
if (this.hasProperty("deletedAt")) {
|
|
227
|
+
const deletedAt = new Date();
|
|
228
|
+
const update = {
|
|
229
|
+
$set: {
|
|
230
|
+
deletedAt,
|
|
231
|
+
},
|
|
232
|
+
} as unknown as UpdateFilter<T>;
|
|
233
|
+
await this.collection.updateMany(filter, update);
|
|
234
|
+
if (this.revision) {
|
|
235
|
+
await this.collection
|
|
236
|
+
.aggregate([
|
|
237
|
+
{
|
|
238
|
+
$match: filter,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
$set: { _id: new ObjectId(), deletedAt },
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
$merge: {
|
|
245
|
+
into: {
|
|
246
|
+
db: this.collection.dbName,
|
|
247
|
+
col: this.revision.collectionName,
|
|
248
|
+
on: "_id",
|
|
249
|
+
whenNotMatched: "insert",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
])
|
|
254
|
+
.toArray();
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
await this.collection.deleteMany(filter);
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
throw new Error(`Failed to delete documents: ${(err as Error).message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Checks if the schema has a given property.
|
|
266
|
+
* @param schema - The Zod schema to check against.
|
|
267
|
+
* @param property - The property to check for in the schema.
|
|
268
|
+
* @returns
|
|
269
|
+
*/
|
|
270
|
+
private hasProperty(property: string): boolean {
|
|
271
|
+
if (!this.schema) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const schemaLike = this.schema as SchemaParser<T>;
|
|
276
|
+
const shapeSource =
|
|
277
|
+
schemaLike.shape ??
|
|
278
|
+
(typeof schemaLike._def?.shape === "function"
|
|
279
|
+
? schemaLike._def.shape()
|
|
280
|
+
: schemaLike._def?.shape);
|
|
281
|
+
|
|
282
|
+
if (
|
|
283
|
+
shapeSource !== null &&
|
|
284
|
+
typeof shapeSource === "object" &&
|
|
285
|
+
!Array.isArray(shapeSource)
|
|
286
|
+
) {
|
|
287
|
+
return property in shapeSource;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Collection, Db, Document, MongoClient } from "mongodb";
|
|
2
|
+
import { CollectionWrapper, SchemaParser } from "./collection";
|
|
3
|
+
|
|
4
|
+
type AnyMongoMethod = (...args: any[]) => any;
|
|
5
|
+
|
|
6
|
+
export type MongoCollectionMethods = Record<string, AnyMongoMethod>;
|
|
7
|
+
|
|
8
|
+
export type MongoIndexSpec = {
|
|
9
|
+
/**
|
|
10
|
+
* Index key specification. Example: { userId: 1 } for ascending, { userId: -1 } for descending
|
|
11
|
+
*/
|
|
12
|
+
key: Record<string, 1 | -1>;
|
|
13
|
+
/**
|
|
14
|
+
* Optional name for the index. If not provided, MongoDB will auto-generate.
|
|
15
|
+
*/
|
|
16
|
+
name?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the index should enforce uniqueness.
|
|
19
|
+
*/
|
|
20
|
+
unique?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the index should be sparse (only index documents with the indexed field).
|
|
23
|
+
*/
|
|
24
|
+
sparse?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to build the index in the background without blocking other operations.
|
|
27
|
+
*/
|
|
28
|
+
background?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The time-to-live (TTL) for documents in the collection, in seconds. Only applicable for TTL indexes.
|
|
31
|
+
*/
|
|
32
|
+
expireAfterSeconds?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type MongoCollectionMethodContext<TDocument extends Document> = {
|
|
36
|
+
/**
|
|
37
|
+
* The MongoDB client instance
|
|
38
|
+
*/
|
|
39
|
+
client: MongoClient;
|
|
40
|
+
/**
|
|
41
|
+
* The MongoDB database instance
|
|
42
|
+
*/
|
|
43
|
+
db: Db;
|
|
44
|
+
/**
|
|
45
|
+
* The MongoDB collection instance
|
|
46
|
+
*/
|
|
47
|
+
collection: Collection<TDocument>;
|
|
48
|
+
/**
|
|
49
|
+
* The base collection wrapper instance
|
|
50
|
+
*/
|
|
51
|
+
base: CollectionWrapper<TDocument>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type MongoCollectionDefinition<
|
|
55
|
+
TDocument extends Document = Document,
|
|
56
|
+
TMethods extends MongoCollectionMethods = {},
|
|
57
|
+
> = {
|
|
58
|
+
name?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Whether to automatically track revisions of documents in this collection. If enabled, every update will insert a copy of the updated document into a separate revision collection, allowing you to keep a history of changes. The revision collection will have the same name as the main collection with `_revisions` appended to it (e.g. `users` -> `users_revisions`).
|
|
61
|
+
*/
|
|
62
|
+
trackRevisions: boolean;
|
|
63
|
+
schema: SchemaParser<TDocument>;
|
|
64
|
+
/**
|
|
65
|
+
* Custom methods to add to the collection wrapper. These methods receive a context object with the MongoDB client, database, collection, and base wrapper instance, allowing you to build complex queries and operations while still benefiting from the schema validation and soft delete functionality provided by the base wrapper.
|
|
66
|
+
* @param ctx
|
|
67
|
+
* @returns
|
|
68
|
+
*/
|
|
69
|
+
methods?: (ctx: MongoCollectionMethodContext<TDocument>) => TMethods;
|
|
70
|
+
/**
|
|
71
|
+
* Optional array of index specifications to create on this collection. Indexes are created idempotently on service startup, so duplicate indexes are safely ignored.
|
|
72
|
+
*/
|
|
73
|
+
indexes?: MongoIndexSpec[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type MongoCollectionsConfig = Record<
|
|
77
|
+
string,
|
|
78
|
+
MongoCollectionDefinition<any, MongoCollectionMethods>
|
|
79
|
+
>;
|
|
80
|
+
|
|
81
|
+
type InferDocument<TDefinition> = TDefinition extends {
|
|
82
|
+
schema: SchemaParser<infer TDocument>;
|
|
83
|
+
}
|
|
84
|
+
? TDocument
|
|
85
|
+
: Document;
|
|
86
|
+
|
|
87
|
+
type NormalizeDocument<TDocument> = TDocument extends Document
|
|
88
|
+
? TDocument
|
|
89
|
+
: Document;
|
|
90
|
+
|
|
91
|
+
type InferMethods<TDefinition> = TDefinition extends {
|
|
92
|
+
methods: (...args: any[]) => infer TMethods;
|
|
93
|
+
}
|
|
94
|
+
? TMethods
|
|
95
|
+
: TDefinition extends {
|
|
96
|
+
methods?: (...args: any[]) => infer TMethods;
|
|
97
|
+
}
|
|
98
|
+
? TMethods extends undefined
|
|
99
|
+
? {}
|
|
100
|
+
: TMethods
|
|
101
|
+
: {};
|
|
102
|
+
|
|
103
|
+
export type MongoBoundCollection<
|
|
104
|
+
TDocument extends Document,
|
|
105
|
+
TMethods extends MongoCollectionMethods = {},
|
|
106
|
+
> = CollectionWrapper<TDocument> & TMethods;
|
|
107
|
+
|
|
108
|
+
export type InferMongoCollections<TCollections extends MongoCollectionsConfig> =
|
|
109
|
+
{
|
|
110
|
+
[K in keyof TCollections]: MongoBoundCollection<
|
|
111
|
+
NormalizeDocument<InferDocument<TCollections[K]>>,
|
|
112
|
+
InferMethods<TCollections[K]>
|
|
113
|
+
>;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type MongoDatabase<TCollections extends MongoCollectionsConfig> = {
|
|
117
|
+
client: MongoClient;
|
|
118
|
+
db: Db;
|
|
119
|
+
collections: InferMongoCollections<TCollections>;
|
|
120
|
+
disconnect: () => Promise<void>;
|
|
121
|
+
connect: () => Promise<MongoClient>;
|
|
122
|
+
} & InferMongoCollections<TCollections>;
|
|
123
|
+
|
|
124
|
+
type SchemaOutput<TSchema> = TSchema extends {
|
|
125
|
+
parse: (input: unknown) => infer TOutput;
|
|
126
|
+
}
|
|
127
|
+
? NormalizeDocument<TOutput>
|
|
128
|
+
: Document;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Define a MongoDB collection with an associated Zod schema for validation and optional custom methods. The returned collection definition can then be used to create a MongoDatabase instance, which will automatically wrap the collection with the provided schema validation and methods.
|
|
132
|
+
* @param config The configuration object for the MongoDB collection, including the schema and optional custom methods.
|
|
133
|
+
*/
|
|
134
|
+
export function defineMongoCollection<
|
|
135
|
+
TSchema extends SchemaParser<any>,
|
|
136
|
+
>(config: {
|
|
137
|
+
/**
|
|
138
|
+
* Optional name for the collection. If not provided, the key used in the collections config will be used as the collection name. This allows you to have more control over the actual collection names in the database, while still using descriptive keys in your code.
|
|
139
|
+
*/
|
|
140
|
+
name?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Zod schema for validating documents in the collection. The schema will be used to parse and validate documents returned from the database, ensuring that they conform to the expected structure and types. If a document does not match the schema, an error will be thrown.
|
|
143
|
+
*/
|
|
144
|
+
schema: TSchema;
|
|
145
|
+
methods?: undefined;
|
|
146
|
+
/**
|
|
147
|
+
* Optional array of index specifications to create on this collection.
|
|
148
|
+
*/
|
|
149
|
+
indexes?: MongoIndexSpec[];
|
|
150
|
+
}): MongoCollectionDefinition<SchemaOutput<TSchema>, {}>;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Define a MongoDB collection with an associated Zod schema for validation and optional custom methods. The returned collection definition can then be used to create a MongoDatabase instance, which will automatically wrap the collection with the provided schema validation and methods.
|
|
154
|
+
* @param config The configuration object for the MongoDB collection, including the schema and optional custom methods.
|
|
155
|
+
*/
|
|
156
|
+
export function defineMongoCollection<
|
|
157
|
+
TSchema extends SchemaParser<any>,
|
|
158
|
+
TMethods extends MongoCollectionMethods,
|
|
159
|
+
>(config: {
|
|
160
|
+
/**
|
|
161
|
+
* Optional name for the collection. If not provided, the key used in the collections config will be used as the collection name. This allows you to have more control over the actual collection names in the database, while still using descriptive keys in your code.
|
|
162
|
+
*/
|
|
163
|
+
name?: string;
|
|
164
|
+
/**
|
|
165
|
+
* Zod schema for validating documents in the collection. The schema will be used to parse and validate documents returned from the database, ensuring that they conform to the expected structure and types. If a document does not match the schema, an error will be thrown.
|
|
166
|
+
*/
|
|
167
|
+
schema: TSchema;
|
|
168
|
+
/**
|
|
169
|
+
* Optional custom methods for the collection. These methods will be added to the collection instance and can be used to implement custom queries or operations.
|
|
170
|
+
* @param ctx The context object containing the MongoDB client, database, collection, and base collection wrapper.
|
|
171
|
+
* @returns An object containing the custom methods to be added to the collection instance.
|
|
172
|
+
*/
|
|
173
|
+
methods: (
|
|
174
|
+
ctx: MongoCollectionMethodContext<SchemaOutput<TSchema>>,
|
|
175
|
+
) => TMethods;
|
|
176
|
+
/**
|
|
177
|
+
* Optional array of index specifications to create on this collection.
|
|
178
|
+
*/
|
|
179
|
+
indexes?: MongoIndexSpec[];
|
|
180
|
+
}): MongoCollectionDefinition<SchemaOutput<TSchema>, TMethods>;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Define a MongoDB collection with an associated Zod schema for validation and optional custom methods. The returned collection definition can then be used to create a MongoDatabase instance, which will automatically wrap the collection with the provided schema validation and methods.
|
|
184
|
+
* @param config The configuration object for the MongoDB collection, including the schema and optional custom methods.
|
|
185
|
+
*/
|
|
186
|
+
export function defineMongoCollection<
|
|
187
|
+
TSchema extends SchemaParser<any>,
|
|
188
|
+
TMethods extends MongoCollectionMethods = {},
|
|
189
|
+
>(config: {
|
|
190
|
+
/**
|
|
191
|
+
* Optional name for the collection. If not provided, the key used in the collections config will be used as the collection name. This allows you to have more control over the actual collection names in the database, while still using descriptive keys in your code.
|
|
192
|
+
*/
|
|
193
|
+
name?: string;
|
|
194
|
+
/**
|
|
195
|
+
* Zod schema for validating documents in the collection. The schema will be used to parse and validate documents returned from the database, ensuring that they conform to the expected structure and types. If a document does not match the schema, an error will be thrown.
|
|
196
|
+
*/
|
|
197
|
+
schema: TSchema;
|
|
198
|
+
/**
|
|
199
|
+
* Optional custom methods for the collection. These methods will be added to the collection instance and can be used to implement custom queries or operations.
|
|
200
|
+
* @param ctx The context object containing the MongoDB client, database, collection, and base collection wrapper.
|
|
201
|
+
* @returns An object containing the custom methods to be added to the collection instance.
|
|
202
|
+
*/
|
|
203
|
+
methods?: (
|
|
204
|
+
ctx: MongoCollectionMethodContext<SchemaOutput<TSchema>>,
|
|
205
|
+
) => TMethods;
|
|
206
|
+
/**
|
|
207
|
+
* Optional array of index specifications to create on this collection.
|
|
208
|
+
*/
|
|
209
|
+
indexes?: MongoIndexSpec[];
|
|
210
|
+
}): MongoCollectionDefinition<SchemaOutput<TSchema>, TMethods> {
|
|
211
|
+
return {
|
|
212
|
+
name: config.name,
|
|
213
|
+
schema: config.schema,
|
|
214
|
+
/**
|
|
215
|
+
* Optional custom methods for the collection. These methods will be added to the collection instance and can be used to implement custom queries or operations.
|
|
216
|
+
* @param ctx The context object containing the MongoDB client, database, collection, and base collection wrapper.
|
|
217
|
+
* @returns An object containing the custom methods to be added to the collection instance.
|
|
218
|
+
*/
|
|
219
|
+
methods: config.methods,
|
|
220
|
+
indexes: config.indexes,
|
|
221
|
+
/**
|
|
222
|
+
* Whether to automatically track revisions of documents in this collection. If enabled, every update will insert a copy of the updated document into a separate revision collection, allowing you to keep a history of changes. The revision collection will have the same name as the main collection with `_revisions` appended to it (e.g. `users` -> `users_revisions`).
|
|
223
|
+
*/
|
|
224
|
+
trackRevisions: false,
|
|
225
|
+
} as MongoCollectionDefinition<SchemaOutput<TSchema>, TMethods>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function createMongo<TCollections extends MongoCollectionsConfig>(
|
|
229
|
+
dbName: string,
|
|
230
|
+
config: TCollections,
|
|
231
|
+
options: { uri?: string } = {},
|
|
232
|
+
): MongoDatabase<TCollections> {
|
|
233
|
+
const client = new MongoClient(
|
|
234
|
+
options.uri || process.env.MONGO_URI || "mongodb://mongo:27017",
|
|
235
|
+
);
|
|
236
|
+
const db = client.db(dbName);
|
|
237
|
+
|
|
238
|
+
const wrapped = {} as InferMongoCollections<TCollections>;
|
|
239
|
+
|
|
240
|
+
for (const key in config) {
|
|
241
|
+
const def = config[key];
|
|
242
|
+
const collectionName = def.name || key;
|
|
243
|
+
const collection = db.collection(collectionName);
|
|
244
|
+
const revisionCollection = def.trackRevisions
|
|
245
|
+
? db.collection(`${collectionName}_revisions`)
|
|
246
|
+
: undefined;
|
|
247
|
+
const base = new CollectionWrapper(
|
|
248
|
+
collection as Collection<any>,
|
|
249
|
+
def.schema,
|
|
250
|
+
revisionCollection,
|
|
251
|
+
);
|
|
252
|
+
const methods =
|
|
253
|
+
def.methods?.({
|
|
254
|
+
client,
|
|
255
|
+
db,
|
|
256
|
+
collection: collection as Collection<any>,
|
|
257
|
+
base,
|
|
258
|
+
}) || {};
|
|
259
|
+
|
|
260
|
+
if (def.indexes && def.indexes.length > 0) {
|
|
261
|
+
for (const indexSpec of def.indexes) {
|
|
262
|
+
const { key, ...options } = indexSpec;
|
|
263
|
+
collection.createIndex(key, options).catch((err) => {
|
|
264
|
+
console.error(
|
|
265
|
+
`Failed to create index on ${collectionName}: ${err.message}`,
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
wrapped[key] = Object.assign(
|
|
272
|
+
base,
|
|
273
|
+
methods,
|
|
274
|
+
) as InferMongoCollections<TCollections>[typeof key];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
client,
|
|
279
|
+
db,
|
|
280
|
+
collections: wrapped,
|
|
281
|
+
...wrapped,
|
|
282
|
+
disconnect: async () => client.close(),
|
|
283
|
+
connect: async () => client.connect(),
|
|
284
|
+
} as MongoDatabase<TCollections>;
|
|
285
|
+
}
|