@rebasepro/server-mongo 0.0.1-canary.4829d6e
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/LICENSE +22 -0
- package/README.md +86 -0
- package/dist/MongoBootstrapper.d.ts +18 -0
- package/dist/MongoBootstrapper.d.ts.map +1 -0
- package/dist/auth/ensure-collections.d.ts +3 -0
- package/dist/auth/ensure-collections.d.ts.map +1 -0
- package/dist/auth/services.d.ts +156 -0
- package/dist/auth/services.d.ts.map +1 -0
- package/dist/connection.d.ts +35 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/db/MongoConditionBuilder.d.ts +64 -0
- package/dist/db/MongoConditionBuilder.d.ts.map +1 -0
- package/dist/db/MongoDataService.d.ts +101 -0
- package/dist/db/MongoDataService.d.ts.map +1 -0
- package/dist/ensure-collections-Bkx_O5CQ.js +94 -0
- package/dist/ensure-collections-Bkx_O5CQ.js.map +1 -0
- package/dist/ensure-history-collection-yajOt2dv.js +21 -0
- package/dist/ensure-history-collection-yajOt2dv.js.map +1 -0
- package/dist/factory.d.ts +151 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/history/ensure-history-collection.d.ts +3 -0
- package/dist/history/ensure-history-collection.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.js +2508 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +2925 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/services/MongoDriver.d.ts +125 -0
- package/dist/services/MongoDriver.d.ts.map +1 -0
- package/dist/services/MongoHistoryService.d.ts +37 -0
- package/dist/services/MongoHistoryService.d.ts.map +1 -0
- package/dist/services/MongoRealtimeService.d.ts +103 -0
- package/dist/services/MongoRealtimeService.d.ts.map +1 -0
- package/dist/websocket-DQlwCHFq.js +281 -0
- package/dist/websocket-DQlwCHFq.js.map +1 -0
- package/dist/websocket.d.ts +7 -0
- package/dist/websocket.d.ts.map +1 -0
- package/package.json +81 -0
- package/src/MongoBootstrapper.ts +196 -0
- package/src/auth/ensure-collections.ts +105 -0
- package/src/auth/services.ts +732 -0
- package/src/connection.ts +60 -0
- package/src/db/MongoConditionBuilder.ts +224 -0
- package/src/db/MongoDataService.ts +368 -0
- package/src/factory.ts +305 -0
- package/src/history/ensure-history-collection.ts +22 -0
- package/src/index.ts +25 -0
- package/src/services/MongoDriver.ts +1120 -0
- package/src/services/MongoHistoryService.ts +181 -0
- package/src/services/MongoRealtimeService.ts +446 -0
- package/src/websocket.ts +297 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MongoDB Connection
|
|
3
|
+
*
|
|
4
|
+
* Wraps MongoDB connection to implement the DatabaseConnection interface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Db, MongoClient } from "mongodb";
|
|
8
|
+
import { DatabaseConnection } from "@rebasepro/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* MongoDB database connection wrapper that implements DatabaseConnection interface.
|
|
12
|
+
*/
|
|
13
|
+
export class MongoDBConnection implements DatabaseConnection {
|
|
14
|
+
readonly type = "mongodb";
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
public readonly db: Db,
|
|
18
|
+
public readonly client: MongoClient
|
|
19
|
+
) { }
|
|
20
|
+
|
|
21
|
+
get isConnected(): boolean {
|
|
22
|
+
// MongoClient doesn't have a direct isConnected property in v6+
|
|
23
|
+
// We check if the client topology is connected
|
|
24
|
+
try {
|
|
25
|
+
const clientInternal = this.client as unknown as Record<string, { isConnected?: () => boolean } | undefined>;
|
|
26
|
+
return clientInternal.topology?.isConnected?.() ?? false;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async close(): Promise<void> {
|
|
33
|
+
await this.client.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a MongoDB database connection from a connection string.
|
|
39
|
+
*
|
|
40
|
+
* @param connectionString - MongoDB connection string (e.g., mongodb://localhost:27017)
|
|
41
|
+
* @param databaseName - Name of the database to use
|
|
42
|
+
* @returns Promise resolving to MongoDBConnection
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const connection = await createMongoDBConnection(
|
|
47
|
+
* "mongodb://localhost:27017",
|
|
48
|
+
* "my_database"
|
|
49
|
+
* );
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export async function createMongoDBConnection(
|
|
53
|
+
connectionString: string,
|
|
54
|
+
databaseName: string
|
|
55
|
+
): Promise<MongoDBConnection> {
|
|
56
|
+
const client = new MongoClient(connectionString);
|
|
57
|
+
await client.connect();
|
|
58
|
+
const db = client.db(databaseName);
|
|
59
|
+
return new MongoDBConnection(db, client);
|
|
60
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MongoDB Condition Builder
|
|
3
|
+
*
|
|
4
|
+
* Translates Rebase filter conditions to MongoDB query operators.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { FilterValues, WhereFilterOp } from "@rebasepro/types";
|
|
8
|
+
import { Filter, Document } from "mongodb";
|
|
9
|
+
import { logger } from "@rebasepro/server";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Mapping from Rebase filter operators to MongoDB query operators
|
|
13
|
+
*/
|
|
14
|
+
const REBASE_TO_MONGO_OP: Partial<Record<WhereFilterOp, string>> = {
|
|
15
|
+
"<": "$lt",
|
|
16
|
+
"<=": "$lte",
|
|
17
|
+
"==": "$eq",
|
|
18
|
+
"!=": "$ne",
|
|
19
|
+
">=": "$gte",
|
|
20
|
+
">": "$gt",
|
|
21
|
+
"array-contains": "$elemMatch",
|
|
22
|
+
"array-contains-any": "$in",
|
|
23
|
+
"in": "$in",
|
|
24
|
+
"not-in": "$nin"
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function escapeRegExp(str: string): string {
|
|
28
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Translate a SQL LIKE/ILIKE pattern into an anchored regular expression.
|
|
33
|
+
* `%` matches any sequence of characters, `_` matches a single character;
|
|
34
|
+
* every other character is matched literally.
|
|
35
|
+
*/
|
|
36
|
+
function likePatternToRegExp(pattern: string, caseInsensitive: boolean): RegExp {
|
|
37
|
+
let body = "";
|
|
38
|
+
for (const ch of String(pattern)) {
|
|
39
|
+
if (ch === "%") body += ".*";
|
|
40
|
+
else if (ch === "_") body += ".";
|
|
41
|
+
else body += escapeRegExp(ch);
|
|
42
|
+
}
|
|
43
|
+
return new RegExp(`^${body}$`, caseInsensitive ? "i" : "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* MongoDB Condition Builder
|
|
48
|
+
*
|
|
49
|
+
* Provides static methods to translate Rebase filter conditions
|
|
50
|
+
* to MongoDB query filters.
|
|
51
|
+
*/
|
|
52
|
+
export class MongoConditionBuilder {
|
|
53
|
+
/**
|
|
54
|
+
* Build MongoDB filter conditions from Rebase FilterValues
|
|
55
|
+
*
|
|
56
|
+
* @param filter - Rebase filter values
|
|
57
|
+
* @returns Array of MongoDB filter objects
|
|
58
|
+
*/
|
|
59
|
+
static buildFilterConditions<M extends Record<string, any>>(
|
|
60
|
+
filter: FilterValues<Extract<keyof M, string>>
|
|
61
|
+
): Filter<Document>[] {
|
|
62
|
+
if (!filter) return [];
|
|
63
|
+
|
|
64
|
+
const conditions: Filter<Document>[] = [];
|
|
65
|
+
|
|
66
|
+
for (const [field, filterParam] of Object.entries(filter)) {
|
|
67
|
+
if (!filterParam) continue;
|
|
68
|
+
|
|
69
|
+
const [op, value] = filterParam as [WhereFilterOp, any];
|
|
70
|
+
|
|
71
|
+
// Null-testing operators ignore their value.
|
|
72
|
+
if (op === "is-null") {
|
|
73
|
+
conditions.push({ [field]: { $eq: null } });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (op === "is-not-null") {
|
|
77
|
+
conditions.push({ [field]: { $ne: null } });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Pattern matching → regular expressions.
|
|
82
|
+
if (op === "like" || op === "ilike" || op === "not-like" || op === "not-ilike") {
|
|
83
|
+
const caseInsensitive = op === "ilike" || op === "not-ilike";
|
|
84
|
+
const regex = likePatternToRegExp(value, caseInsensitive);
|
|
85
|
+
const negated = op === "not-like" || op === "not-ilike";
|
|
86
|
+
conditions.push({
|
|
87
|
+
[field]: negated ? { $not: regex } : { $regex: regex }
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const mongoOp = REBASE_TO_MONGO_OP[op];
|
|
93
|
+
|
|
94
|
+
if (!mongoOp) {
|
|
95
|
+
logger.warn(`Unsupported filter operator: ${op}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle array-contains specially
|
|
100
|
+
if (op === "array-contains") {
|
|
101
|
+
conditions.push({
|
|
102
|
+
[field]: { $elemMatch: { $eq: value } }
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
conditions.push({
|
|
106
|
+
[field]: { [mongoOp]: value }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return conditions;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build search conditions for text search
|
|
116
|
+
*
|
|
117
|
+
* @param searchString - Text to search for
|
|
118
|
+
* @param properties - Properties to search in
|
|
119
|
+
* @returns Array of MongoDB filter objects for text search
|
|
120
|
+
*/
|
|
121
|
+
static buildSearchConditions(
|
|
122
|
+
searchString: string,
|
|
123
|
+
properties: Record<string, any>
|
|
124
|
+
): Filter<Document>[] {
|
|
125
|
+
if (!searchString) return [];
|
|
126
|
+
|
|
127
|
+
// Build regex conditions for each searchable string property
|
|
128
|
+
const orConditions: Filter<Document>[] = [];
|
|
129
|
+
const escapedSearch = escapeRegExp(searchString);
|
|
130
|
+
const searchRegex = new RegExp(escapedSearch, "i");
|
|
131
|
+
|
|
132
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
133
|
+
// Only search in string-type properties
|
|
134
|
+
if (prop?.dataType === "string" || typeof prop === "string") {
|
|
135
|
+
orConditions.push({
|
|
136
|
+
[key]: { $regex: searchRegex }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If no properties to search, use MongoDB text search
|
|
142
|
+
if (orConditions.length === 0) {
|
|
143
|
+
return [{ $text: { $search: searchString } }];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return orConditions;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Combine multiple conditions with AND operator
|
|
151
|
+
*
|
|
152
|
+
* @param conditions - Array of filter conditions
|
|
153
|
+
* @returns Combined filter or undefined if empty
|
|
154
|
+
*/
|
|
155
|
+
static combineConditionsWithAnd(conditions: Filter<Document>[]): Filter<Document> | undefined {
|
|
156
|
+
if (conditions.length === 0) return undefined;
|
|
157
|
+
if (conditions.length === 1) return conditions[0];
|
|
158
|
+
return { $and: conditions };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Combine multiple conditions with OR operator
|
|
163
|
+
*
|
|
164
|
+
* @param conditions - Array of filter conditions
|
|
165
|
+
* @returns Combined filter or undefined if empty
|
|
166
|
+
*/
|
|
167
|
+
static combineConditionsWithOr(conditions: Filter<Document>[]): Filter<Document> | undefined {
|
|
168
|
+
if (conditions.length === 0) return undefined;
|
|
169
|
+
if (conditions.length === 1) return conditions[0];
|
|
170
|
+
return { $or: conditions };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Build a complete MongoDB query from Rebase options
|
|
175
|
+
*
|
|
176
|
+
* @param options - Rebase fetch options
|
|
177
|
+
* @returns MongoDB filter object
|
|
178
|
+
*/
|
|
179
|
+
static buildQuery<M extends Record<string, any>>(options: {
|
|
180
|
+
filter?: FilterValues<Extract<keyof M, string>>;
|
|
181
|
+
searchString?: string;
|
|
182
|
+
properties?: Record<string, any>;
|
|
183
|
+
}): Filter<Document> {
|
|
184
|
+
const conditions: Filter<Document>[] = [];
|
|
185
|
+
|
|
186
|
+
// Add filter conditions
|
|
187
|
+
if (options.filter) {
|
|
188
|
+
const filterConditions = this.buildFilterConditions<M>(options.filter);
|
|
189
|
+
conditions.push(...filterConditions);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add search conditions
|
|
193
|
+
if (options.searchString && options.properties) {
|
|
194
|
+
const searchConditions = this.buildSearchConditions(
|
|
195
|
+
options.searchString,
|
|
196
|
+
options.properties
|
|
197
|
+
);
|
|
198
|
+
if (searchConditions.length > 0) {
|
|
199
|
+
// Search conditions are OR'd together
|
|
200
|
+
const searchFilter = this.combineConditionsWithOr(searchConditions);
|
|
201
|
+
if (searchFilter) {
|
|
202
|
+
conditions.push(searchFilter);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return this.combineConditionsWithAnd(conditions) ?? {};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build MongoDB sort options from Rebase options
|
|
212
|
+
*
|
|
213
|
+
* @param orderBy - Field to order by
|
|
214
|
+
* @param order - Sort direction
|
|
215
|
+
* @returns MongoDB sort object
|
|
216
|
+
*/
|
|
217
|
+
static buildSort(
|
|
218
|
+
orderBy?: string,
|
|
219
|
+
order?: "asc" | "desc"
|
|
220
|
+
): Record<string, 1 | -1> | undefined {
|
|
221
|
+
if (!orderBy) return undefined;
|
|
222
|
+
return { [orderBy]: order === "desc" ? -1 : 1 };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MongoDB Row Service
|
|
3
|
+
*
|
|
4
|
+
* Implements DataRepository interface for MongoDB.
|
|
5
|
+
* Provides all CRUD operations for rows.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Db, ObjectId, Collection, Document, FindOptions, Filter } from "mongodb";
|
|
9
|
+
import { FilterValues, DataRepository, CollectionConfig, EntityReference } from "@rebasepro/types";
|
|
10
|
+
import { MongoConditionBuilder } from "./MongoConditionBuilder";
|
|
11
|
+
import { logger } from "@rebasepro/server";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MongoDB Row Service
|
|
15
|
+
*
|
|
16
|
+
* Implements the DataRepository interface for MongoDB.
|
|
17
|
+
* Provides all CRUD operations for rows stored in MongoDB collections.
|
|
18
|
+
*/
|
|
19
|
+
export class MongoDataService implements DataRepository {
|
|
20
|
+
constructor(private db: Db) { }
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get a MongoDB collection by its path
|
|
24
|
+
*/
|
|
25
|
+
private getCollection(collectionPath: string): Collection<Document> {
|
|
26
|
+
// Handle nested paths (e.g., "posts/123/comments" -> "posts_123_comments")
|
|
27
|
+
const collectionName = collectionPath.replace(/\//g, "_");
|
|
28
|
+
return this.db.collection(collectionName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert a string ID to ObjectId if it's a valid ObjectId string
|
|
33
|
+
*/
|
|
34
|
+
private toObjectId(id: string | number): ObjectId | string | number {
|
|
35
|
+
if (typeof id === "string" && ObjectId.isValid(id) && id.length === 24) {
|
|
36
|
+
return new ObjectId(id);
|
|
37
|
+
}
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert a MongoDB document to a flat row (`{ id, ...fields }`)
|
|
43
|
+
*/
|
|
44
|
+
private documentToRow(doc: Document): Record<string, unknown> {
|
|
45
|
+
const { _id, ...values } = doc;
|
|
46
|
+
return {
|
|
47
|
+
...this.convertFromMongoValues(values),
|
|
48
|
+
// Spread the canonical id last so it wins over a literal `id` field
|
|
49
|
+
id: _id.toString()
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Convert values from MongoDB format to Rebase format
|
|
55
|
+
*/
|
|
56
|
+
private convertFromMongoValues(values: Record<string, any>): Record<string, any> {
|
|
57
|
+
const result: Record<string, any> = {};
|
|
58
|
+
|
|
59
|
+
for (const [key, value] of Object.entries(values)) {
|
|
60
|
+
result[key] = this.convertFromMongoValue(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert a single value from MongoDB format
|
|
68
|
+
*/
|
|
69
|
+
private convertFromMongoValue(value: any): any {
|
|
70
|
+
if (value === null || value === undefined) return value;
|
|
71
|
+
|
|
72
|
+
// Handle ObjectId
|
|
73
|
+
if (value instanceof ObjectId) {
|
|
74
|
+
return value.toString();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle Date
|
|
78
|
+
if (value instanceof Date) {
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle arrays
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
return value.map(v => this.convertFromMongoValue(v));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle stored EntityReference. New writes are tagged with the
|
|
88
|
+
// `__type: "reference"` sentinel (see convertToMongoValue), which is
|
|
89
|
+
// unambiguous. We also accept the legacy shape — an object whose ONLY
|
|
90
|
+
// keys are `id` and `path` — so references written before the sentinel
|
|
91
|
+
// existed still decode. We deliberately do NOT treat any object that
|
|
92
|
+
// merely *contains* `id` and `path` as a reference, because that
|
|
93
|
+
// silently rewrites ordinary embedded sub-documents.
|
|
94
|
+
if (typeof value === "object") {
|
|
95
|
+
const keys = Object.keys(value);
|
|
96
|
+
const isTagged = value.__type === "reference" && "id" in value && "path" in value;
|
|
97
|
+
const isLegacy = keys.length === 2 && keys.includes("id") && keys.includes("path");
|
|
98
|
+
if (isTagged || isLegacy) {
|
|
99
|
+
return new EntityReference({
|
|
100
|
+
id: value.id instanceof ObjectId ? value.id.toString() : String(value.id),
|
|
101
|
+
path: value.path,
|
|
102
|
+
driver: value.driver,
|
|
103
|
+
databaseId: value.databaseId
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle nested objects
|
|
109
|
+
if (typeof value === "object") {
|
|
110
|
+
return this.convertFromMongoValues(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Convert values to MongoDB format for storage
|
|
118
|
+
*/
|
|
119
|
+
private convertToMongoValues(values: Record<string, any>): Record<string, any> {
|
|
120
|
+
const result: Record<string, any> = {};
|
|
121
|
+
|
|
122
|
+
for (const [key, value] of Object.entries(values)) {
|
|
123
|
+
result[key] = this.convertToMongoValue(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Convert a single value to MongoDB format
|
|
131
|
+
*/
|
|
132
|
+
private convertToMongoValue(value: any): any {
|
|
133
|
+
if (value === null || value === undefined) return value;
|
|
134
|
+
|
|
135
|
+
// Handle EntityReference. Persist with a `__type: "reference"` sentinel
|
|
136
|
+
// so it round-trips unambiguously, and preserve `driver`/`databaseId`
|
|
137
|
+
// so cross-datasource pointers don't lose their target on write.
|
|
138
|
+
if (typeof value === "object" && value.isEntityReference?.()) {
|
|
139
|
+
const ref: Record<string, unknown> = {
|
|
140
|
+
__type: "reference",
|
|
141
|
+
id: ObjectId.isValid(value.id) ? new ObjectId(value.id) : value.id,
|
|
142
|
+
path: value.path
|
|
143
|
+
};
|
|
144
|
+
if (value.driver !== undefined) ref.driver = value.driver;
|
|
145
|
+
if (value.databaseId !== undefined) ref.databaseId = value.databaseId;
|
|
146
|
+
return ref;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Handle Date
|
|
150
|
+
if (value instanceof Date) {
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Handle arrays
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
return value.map(v => this.convertToMongoValue(v));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Handle nested objects
|
|
160
|
+
if (typeof value === "object") {
|
|
161
|
+
return this.convertToMongoValues(value);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// =============================================================
|
|
168
|
+
// DataRepository Implementation
|
|
169
|
+
// =============================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Fetch a single row by ID
|
|
173
|
+
*/
|
|
174
|
+
async fetchOne<M extends Record<string, any>>(
|
|
175
|
+
collectionPath: string,
|
|
176
|
+
id: string | number,
|
|
177
|
+
_databaseId?: string
|
|
178
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
179
|
+
const collection = this.getCollection(collectionPath);
|
|
180
|
+
const objectId = this.toObjectId(id);
|
|
181
|
+
|
|
182
|
+
const doc = await collection.findOne({ _id: objectId } as Filter<Document>);
|
|
183
|
+
|
|
184
|
+
if (!doc) return undefined;
|
|
185
|
+
|
|
186
|
+
return this.documentToRow(doc);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Fetch a collection of rows with optional filtering, ordering, and pagination
|
|
191
|
+
*/
|
|
192
|
+
async fetchCollection<M extends Record<string, any>>(
|
|
193
|
+
collectionPath: string,
|
|
194
|
+
options: {
|
|
195
|
+
filter?: FilterValues<Extract<keyof M, string>>;
|
|
196
|
+
orderBy?: string;
|
|
197
|
+
order?: "desc" | "asc";
|
|
198
|
+
limit?: number;
|
|
199
|
+
startAfter?: any;
|
|
200
|
+
searchString?: string;
|
|
201
|
+
databaseId?: string;
|
|
202
|
+
collection?: CollectionConfig;
|
|
203
|
+
rawQuery?: Filter<Document>;
|
|
204
|
+
} = {}
|
|
205
|
+
): Promise<Record<string, unknown>[]> {
|
|
206
|
+
const collection = this.getCollection(collectionPath);
|
|
207
|
+
|
|
208
|
+
// Build query
|
|
209
|
+
const query = options.rawQuery ?? MongoConditionBuilder.buildQuery<M>({
|
|
210
|
+
filter: options.filter,
|
|
211
|
+
searchString: options.searchString,
|
|
212
|
+
properties: options.collection?.properties ?? {}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Build find options
|
|
216
|
+
const findOptions: FindOptions = {};
|
|
217
|
+
|
|
218
|
+
// Apply sorting
|
|
219
|
+
const sort = MongoConditionBuilder.buildSort(options.orderBy, options.order);
|
|
220
|
+
if (sort) {
|
|
221
|
+
findOptions.sort = sort;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Apply limit
|
|
225
|
+
if (options.limit) {
|
|
226
|
+
findOptions.limit = options.limit;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Apply pagination (skip-based for now, cursor-based would be better)
|
|
230
|
+
if (options.startAfter !== undefined) {
|
|
231
|
+
findOptions.skip = Number(options.startAfter);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const docs = await collection.find(query, findOptions).toArray();
|
|
235
|
+
|
|
236
|
+
return docs.map((doc: Document) => this.documentToRow(doc));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Search rows by text
|
|
241
|
+
*/
|
|
242
|
+
async searchRows<M extends Record<string, any>>(
|
|
243
|
+
collectionPath: string,
|
|
244
|
+
searchString: string,
|
|
245
|
+
options: {
|
|
246
|
+
filter?: FilterValues<Extract<keyof M, string>>;
|
|
247
|
+
orderBy?: string;
|
|
248
|
+
order?: "desc" | "asc";
|
|
249
|
+
limit?: number;
|
|
250
|
+
databaseId?: string;
|
|
251
|
+
collection?: CollectionConfig;
|
|
252
|
+
rawQuery?: Filter<Document>;
|
|
253
|
+
} = {}
|
|
254
|
+
): Promise<Record<string, unknown>[]> {
|
|
255
|
+
return this.fetchCollection<M>(collectionPath, {
|
|
256
|
+
...options,
|
|
257
|
+
searchString
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Count rows in a collection
|
|
263
|
+
*/
|
|
264
|
+
async count<M extends Record<string, any>>(
|
|
265
|
+
collectionPath: string,
|
|
266
|
+
options: {
|
|
267
|
+
filter?: FilterValues<Extract<keyof M, string>>;
|
|
268
|
+
databaseId?: string;
|
|
269
|
+
rawQuery?: Filter<Document>;
|
|
270
|
+
} = {}
|
|
271
|
+
): Promise<number> {
|
|
272
|
+
const collection = this.getCollection(collectionPath);
|
|
273
|
+
|
|
274
|
+
const query = options.rawQuery ?? (options.filter
|
|
275
|
+
? MongoConditionBuilder.buildQuery<M>({ filter: options.filter })
|
|
276
|
+
: {});
|
|
277
|
+
|
|
278
|
+
return collection.countDocuments(query);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Save an row (create or update)
|
|
283
|
+
*/
|
|
284
|
+
async save<M extends Record<string, any>>(
|
|
285
|
+
collectionPath: string,
|
|
286
|
+
values: Partial<M>,
|
|
287
|
+
id?: string | number,
|
|
288
|
+
_databaseId?: string
|
|
289
|
+
): Promise<Record<string, unknown>> {
|
|
290
|
+
const collection = this.getCollection(collectionPath);
|
|
291
|
+
const mongoValues = this.convertToMongoValues(values as Record<string, any>);
|
|
292
|
+
|
|
293
|
+
if (id) {
|
|
294
|
+
// Update existing row
|
|
295
|
+
const objectId = this.toObjectId(id);
|
|
296
|
+
await collection.updateOne(
|
|
297
|
+
{ _id: objectId } as Filter<Document>,
|
|
298
|
+
{ $set: mongoValues },
|
|
299
|
+
{ upsert: true }
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
...values,
|
|
304
|
+
id: id.toString()
|
|
305
|
+
};
|
|
306
|
+
} else {
|
|
307
|
+
// Create new row
|
|
308
|
+
const newId = new ObjectId();
|
|
309
|
+
await collection.insertOne({
|
|
310
|
+
_id: newId,
|
|
311
|
+
...mongoValues
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
...values,
|
|
316
|
+
id: newId.toString()
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Delete an row by ID
|
|
323
|
+
*/
|
|
324
|
+
async delete(
|
|
325
|
+
collectionPath: string,
|
|
326
|
+
id: string | number,
|
|
327
|
+
_databaseId?: string
|
|
328
|
+
): Promise<void> {
|
|
329
|
+
const collection = this.getCollection(collectionPath);
|
|
330
|
+
const objectId = this.toObjectId(id);
|
|
331
|
+
|
|
332
|
+
const result = await collection.deleteOne({ _id: objectId } as Filter<Document>);
|
|
333
|
+
|
|
334
|
+
if (result.deletedCount === 0) {
|
|
335
|
+
logger.warn(`Row ${id} not found in collection ${collectionPath}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Check if a field value is unique in a collection
|
|
341
|
+
*/
|
|
342
|
+
async checkUniqueField(
|
|
343
|
+
collectionPath: string,
|
|
344
|
+
fieldName: string,
|
|
345
|
+
value: any,
|
|
346
|
+
excludeEntityId?: string,
|
|
347
|
+
_databaseId?: string
|
|
348
|
+
): Promise<boolean> {
|
|
349
|
+
const collection = this.getCollection(collectionPath);
|
|
350
|
+
|
|
351
|
+
const query: Filter<Document> = { [fieldName]: value };
|
|
352
|
+
|
|
353
|
+
if (excludeEntityId) {
|
|
354
|
+
const objectId = this.toObjectId(excludeEntityId);
|
|
355
|
+
(query as Record<string, unknown>)._id = { $ne: objectId };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const count = await collection.countDocuments(query);
|
|
359
|
+
return count === 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Generate a new row ID
|
|
364
|
+
*/
|
|
365
|
+
generateId(): string {
|
|
366
|
+
return new ObjectId().toString();
|
|
367
|
+
}
|
|
368
|
+
}
|