@rebasepro/server-mongo 0.17.3 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +4 -0
  3. package/dist/MongoBootstrapper.d.ts +0 -1
  4. package/dist/auth/ensure-collections.d.ts +0 -1
  5. package/dist/auth/services.d.ts +0 -1
  6. package/dist/connection.d.ts +0 -1
  7. package/dist/db/MongoConditionBuilder.d.ts +0 -1
  8. package/dist/db/MongoDataService.d.ts +0 -1
  9. package/dist/db/securityRuleFilter.d.ts +0 -1
  10. package/dist/factory.d.ts +0 -1
  11. package/dist/history/ensure-history-collection.d.ts +0 -1
  12. package/dist/index.d.ts +0 -1
  13. package/dist/index.es.js +83 -79
  14. package/dist/index.es.js.map +1 -1
  15. package/dist/schema/plan-schema-change.d.ts +0 -1
  16. package/dist/services/MongoDriver.d.ts +0 -1
  17. package/dist/services/MongoHistoryService.d.ts +0 -1
  18. package/dist/services/MongoRealtimeService.d.ts +0 -1
  19. package/dist/websocket.d.ts +0 -1
  20. package/package.json +28 -24
  21. package/dist/MongoBootstrapper.d.ts.map +0 -1
  22. package/dist/auth/ensure-collections.d.ts.map +0 -1
  23. package/dist/auth/services.d.ts.map +0 -1
  24. package/dist/connection.d.ts.map +0 -1
  25. package/dist/db/MongoConditionBuilder.d.ts.map +0 -1
  26. package/dist/db/MongoDataService.d.ts.map +0 -1
  27. package/dist/db/securityRuleFilter.d.ts.map +0 -1
  28. package/dist/factory.d.ts.map +0 -1
  29. package/dist/history/ensure-history-collection.d.ts.map +0 -1
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/schema/plan-schema-change.d.ts.map +0 -1
  32. package/dist/services/MongoDriver.d.ts.map +0 -1
  33. package/dist/services/MongoHistoryService.d.ts.map +0 -1
  34. package/dist/services/MongoRealtimeService.d.ts.map +0 -1
  35. package/dist/websocket.d.ts.map +0 -1
  36. package/src/MongoBootstrapper.ts +0 -204
  37. package/src/auth/ensure-collections.ts +0 -153
  38. package/src/auth/services.ts +0 -866
  39. package/src/connection.ts +0 -60
  40. package/src/db/MongoConditionBuilder.ts +0 -348
  41. package/src/db/MongoDataService.ts +0 -412
  42. package/src/db/securityRuleFilter.ts +0 -398
  43. package/src/factory.ts +0 -331
  44. package/src/history/ensure-history-collection.ts +0 -22
  45. package/src/index.ts +0 -25
  46. package/src/schema/plan-schema-change.ts +0 -159
  47. package/src/services/MongoDriver.ts +0 -950
  48. package/src/services/MongoHistoryService.ts +0 -186
  49. package/src/services/MongoRealtimeService.ts +0 -592
  50. package/src/websocket.ts +0 -387
package/src/connection.ts DELETED
@@ -1,60 +0,0 @@
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
- }
@@ -1,348 +0,0 @@
1
- /**
2
- * MongoDB Condition Builder
3
- *
4
- * Translates Rebase filter conditions to MongoDB query operators.
5
- */
6
-
7
- import { CollectionConfig, FilterCondition, FilterValues, LogicalCondition, OrderByTuple, WhereFilterOp } from "@rebasepro/types";
8
- import { normalizeDriverOrderBy, toFilterTuples } from "@rebasepro/common";
9
- import { Filter, Document } from "mongodb";
10
- import { ApiError, logger } from "@rebasepro/server";
11
-
12
- /**
13
- * Mapping from Rebase filter operators to MongoDB query operators
14
- */
15
- const REBASE_TO_MONGO_OP: Partial<Record<WhereFilterOp, string>> = {
16
- "<": "$lt",
17
- "<=": "$lte",
18
- "==": "$eq",
19
- "!=": "$ne",
20
- ">=": "$gte",
21
- ">": "$gt",
22
- "array-contains": "$elemMatch",
23
- "array-contains-any": "$in",
24
- "in": "$in",
25
- "not-in": "$nin"
26
- };
27
-
28
- function escapeRegExp(str: string): string {
29
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
- }
31
-
32
- /**
33
- * Translate a SQL LIKE/ILIKE pattern into an anchored regular expression.
34
- * `%` matches any sequence of characters, `_` matches a single character;
35
- * every other character is matched literally.
36
- */
37
- function likePatternToRegExp(pattern: string, caseInsensitive: boolean): RegExp {
38
- let body = "";
39
- // Runs of `%` collapse to one. `%%%%X` means exactly what `%X` means, but as
40
- // a regular expression it is four adjacent unbounded quantifiers, and on a
41
- // subject that does not match, the engine tries every way of splitting the
42
- // subject between them — exponential time. The pattern comes from a public
43
- // filter operator over HTTP (`?title=like.%25%25%25…`), and this expression
44
- // is handed to MongoDB as `$regex`, so the time is spent on a database
45
- // thread rather than the caller's.
46
- let lastWasWildcard = false;
47
- for (const ch of String(pattern)) {
48
- if (ch === "%") {
49
- if (!lastWasWildcard) body += ".*";
50
- lastWasWildcard = true;
51
- continue;
52
- }
53
- body += ch === "_" ? "." : escapeRegExp(ch);
54
- lastWasWildcard = false;
55
- }
56
- return new RegExp(`^${body}$`, caseInsensitive ? "i" : "");
57
- }
58
-
59
- /**
60
- * MongoDB Condition Builder
61
- *
62
- * Provides static methods to translate Rebase filter conditions
63
- * to MongoDB query filters.
64
- */
65
- export class MongoConditionBuilder {
66
- /**
67
- * Build MongoDB filter conditions from Rebase FilterValues
68
- *
69
- * @param filter - Rebase filter values
70
- * @returns Array of MongoDB filter objects
71
- */
72
- static buildFilterConditions<M extends Record<string, any>>(
73
- filter: FilterValues<Extract<keyof M, string>>
74
- ): Filter<Document>[] {
75
- if (!filter) return [];
76
-
77
- const conditions: Filter<Document>[] = [];
78
-
79
- for (const [field, filterParam] of Object.entries(filter)) {
80
- if (!filterParam) continue;
81
-
82
- // One tuple or an array of them. This destructured the param
83
- // directly, so `{ age: [[">=", 18], ["<", 65]] }` bound `op` to the
84
- // tuple `[">=", 18]`, matched no operator, and dropped **both**
85
- // conditions — a filtered read answering 200 with the whole
86
- // collection. The grammar is shared with the Postgres compiler now.
87
- for (const [op, value] of toFilterTuples(filterParam)) {
88
- conditions.push(this.buildCondition(field, op, value));
89
- }
90
- }
91
-
92
- return conditions;
93
- }
94
-
95
- /**
96
- * One field, one operator, one value.
97
- *
98
- * Extracted so that `logical` groups translate through exactly this code.
99
- * A group written as a second dialect is a group where `array-contains` or
100
- * `ilike` quietly means something else than it does in `filter`, which is
101
- * the kind of difference nobody finds until a query returns the wrong rows.
102
- *
103
- * Always returns a condition or throws: there is no operator this can be
104
- * given that legitimately means "no condition".
105
- */
106
- private static buildCondition(
107
- field: string,
108
- op: WhereFilterOp,
109
- value: any
110
- ): Filter<Document> {
111
- // Null-testing operators ignore their value.
112
- if (op === "is-null") return { [field]: { $eq: null } };
113
- if (op === "is-not-null") return { [field]: { $ne: null } };
114
-
115
- // Pattern matching → regular expressions.
116
- if (op === "like" || op === "ilike" || op === "not-like" || op === "not-ilike") {
117
- const caseInsensitive = op === "ilike" || op === "not-ilike";
118
- const regex = likePatternToRegExp(value, caseInsensitive);
119
- const negated = op === "not-like" || op === "not-ilike";
120
- return { [field]: negated ? { $not: regex } : { $regex: regex } };
121
- }
122
-
123
- const mongoOp = REBASE_TO_MONGO_OP[op];
124
-
125
- if (!mongoOp) {
126
- // A filter that cannot be compiled must not compile to "no filter".
127
- // Returning `undefined` here dropped the condition and widened the
128
- // read — inside an `or(...)` group it drops a branch, which widens
129
- // it further — and the only trace was a line in the server log
130
- // behind a 200.
131
- logger.warn(`Unsupported filter operator '${op}' on field '${field}'`);
132
- throw ApiError.badRequest(
133
- `Operator '${op}' is not supported on field '${field}' by the MongoDB driver.`,
134
- "UNSUPPORTED_FILTER_OPERATOR",
135
- { field, operator: op }
136
- );
137
- }
138
-
139
- // Handle array-contains specially
140
- if (op === "array-contains") {
141
- return { [field]: { $elemMatch: { $eq: value } } };
142
- }
143
- return { [field]: { [mongoOp]: value } };
144
- }
145
-
146
- /**
147
- * Translate an `or(...)` / `and(...)` group, nesting included.
148
- *
149
- * Returns `undefined` for a group with nothing in it. `$or: []` is an error
150
- * in Mongo and `$and: []` matches every document, so neither is a
151
- * defensible reading of "no conditions".
152
- */
153
- static buildLogicalConditions(logical: LogicalCondition | undefined): Filter<Document> | undefined {
154
- if (!logical || !Array.isArray(logical.conditions)) return undefined;
155
-
156
- const parts: Filter<Document>[] = [];
157
- for (const entry of logical.conditions) {
158
- if (!entry) continue;
159
- if ("type" in entry && "conditions" in entry) {
160
- const nested = this.buildLogicalConditions(entry as LogicalCondition);
161
- if (nested) parts.push(nested);
162
- continue;
163
- }
164
- const { column, operator, value } = entry as FilterCondition;
165
- parts.push(this.buildCondition(column, operator, value));
166
- }
167
-
168
- // Through the same combiners the rest of this class uses, so a
169
- // one-condition group reads as the bare condition — identical to how
170
- // `filter` would have expressed it — rather than as `{ $and: [x] }`.
171
- return logical.type === "or"
172
- ? this.combineConditionsWithOr(parts)
173
- : this.combineConditionsWithAnd(parts);
174
- }
175
-
176
- /**
177
- * Build search conditions for text search
178
- *
179
- * @param searchString - Text to search for
180
- * @param properties - The collection's properties, searched for string fields
181
- * @returns Array of MongoDB filter objects for text search
182
- */
183
- static buildSearchConditions(
184
- searchString: string,
185
- // Typed as the real property map, not `Record<string, any>`. The loose
186
- // type is what let the `dataType` bug below survive: a caller — and,
187
- // more to the point, a test fixture — could invent any key it liked and
188
- // nothing checked it against a property a user can actually declare.
189
- properties: CollectionConfig["properties"]
190
- ): Filter<Document>[] {
191
- if (!searchString) return [];
192
-
193
- // Build regex conditions for each searchable string property
194
- const orConditions: Filter<Document>[] = [];
195
- const escapedSearch = escapeRegExp(searchString);
196
- const searchRegex = new RegExp(escapedSearch, "i");
197
-
198
- for (const [key, prop] of Object.entries(properties)) {
199
- // `type`, not `dataType`. No property in `@rebasepro/types` has ever
200
- // had a `dataType` field — a real collection carries `type:
201
- // "string"` — so this matched nothing for every collection a user
202
- // could actually declare. With no field matching, the fallback
203
- // below took over and every search became a `$text` query, which
204
- // needs a text index and throws `IndexNotFound` without one.
205
- //
206
- // The suite passed because its fixtures were written with the same
207
- // wrong key, so the test data agreed with the bug and the two
208
- // never met a real collection between them.
209
- if (prop?.type === "string" || typeof prop === "string") {
210
- orConditions.push({
211
- [key]: { $regex: searchRegex }
212
- });
213
- }
214
- }
215
-
216
- // If no properties to search, use MongoDB text search
217
- if (orConditions.length === 0) {
218
- return [{ $text: { $search: searchString } }];
219
- }
220
-
221
- return orConditions;
222
- }
223
-
224
- /**
225
- * Combine multiple conditions with AND operator
226
- *
227
- * @param conditions - Array of filter conditions
228
- * @returns Combined filter or undefined if empty
229
- */
230
- static combineConditionsWithAnd(conditions: Filter<Document>[]): Filter<Document> | undefined {
231
- if (conditions.length === 0) return undefined;
232
- if (conditions.length === 1) return conditions[0];
233
- return { $and: conditions };
234
- }
235
-
236
- /**
237
- * Combine multiple conditions with OR operator
238
- *
239
- * @param conditions - Array of filter conditions
240
- * @returns Combined filter or undefined if empty
241
- */
242
- static combineConditionsWithOr(conditions: Filter<Document>[]): Filter<Document> | undefined {
243
- if (conditions.length === 0) return undefined;
244
- if (conditions.length === 1) return conditions[0];
245
- return { $or: conditions };
246
- }
247
-
248
- /**
249
- * Build a complete MongoDB query from Rebase options
250
- *
251
- * @param options - Rebase fetch options
252
- * @returns MongoDB filter object
253
- */
254
- static buildQuery<M extends Record<string, any>>(options: {
255
- filter?: FilterValues<Extract<keyof M, string>>;
256
- /**
257
- * An `or(...)`/`and(...)` group, AND-ed with `filter` and
258
- * `searchString` — the three are independent, as `FindParams`
259
- * documents. Absent here until now, so a group that reached this
260
- * driver was dropped and the read ran unfiltered.
261
- */
262
- logical?: LogicalCondition;
263
- searchString?: string;
264
- properties?: CollectionConfig["properties"];
265
- }): Filter<Document> {
266
- const conditions: Filter<Document>[] = [];
267
-
268
- // Add filter conditions
269
- if (options.filter) {
270
- const filterConditions = this.buildFilterConditions<M>(options.filter);
271
- conditions.push(...filterConditions);
272
- }
273
-
274
- const logicalCondition = this.buildLogicalConditions(options.logical);
275
- if (logicalCondition) conditions.push(logicalCondition);
276
-
277
- // Add search conditions
278
- if (options.searchString && options.properties) {
279
- const searchConditions = this.buildSearchConditions(
280
- options.searchString,
281
- options.properties
282
- );
283
- if (searchConditions.length > 0) {
284
- // Search conditions are OR'd together
285
- const searchFilter = this.combineConditionsWithOr(searchConditions);
286
- if (searchFilter) {
287
- conditions.push(searchFilter);
288
- }
289
- }
290
- }
291
-
292
- return this.combineConditionsWithAnd(conditions) ?? {};
293
- }
294
-
295
- /**
296
- * The primary key's name in a stored document.
297
- *
298
- * Rows leave this driver with `_id` renamed to `id` — see
299
- * `MongoDataService.documentToRow` — so `id` is the only name a caller ever
300
- * sees, and the one the SDK's own examples use for a tie-breaker. A sort
301
- * document naming `id` names a field no document has: Mongo does not
302
- * complain, it just returns them in natural order, so `.orderBy("id")` read
303
- * as "no sort at all" with a 200 to go with it.
304
- */
305
- private static readonly ID_FIELD = "_id";
306
-
307
- /**
308
- * Build MongoDB sort options from Rebase options
309
- *
310
- * A Mongo sort document is already an ordered map of field to direction, so
311
- * a multi-key sort is expressed directly: the keys are applied in insertion
312
- * order, each breaking ties on the one before it.
313
- *
314
- * The id closes every sort, descending, for the same reason the Postgres
315
- * driver appends `id DESC`: it is what makes the ordering *total*. Without
316
- * it, two rows sharing a sort value are returned in whatever order the
317
- * engine pleases, and that order is free to differ between two executions of
318
- * the same query — so paging by `offset` over a non-unique sort column
319
- * repeats some rows and skips others. A single-column sort has always had
320
- * this exposure here; a multi-key sort merely made it easier to reach, since
321
- * the whole point of the later keys is that the earlier ones tie.
322
- *
323
- * @param orderBy - Field to order by, or the `[field, direction]` list
324
- * @param order - Sort direction, for the single-field spelling
325
- * @returns MongoDB sort object
326
- */
327
- static buildSort(
328
- orderBy?: string | OrderByTuple[],
329
- order?: "asc" | "desc"
330
- ): Record<string, 1 | -1> | undefined {
331
- const keys = normalizeDriverOrderBy(orderBy, order);
332
- if (!keys) return undefined;
333
- const sort: Record<string, 1 | -1> = {};
334
- // A repeated field is the first occurrence's: that is the key Mongo
335
- // would sort by, and letting a later duplicate overwrite it would order
336
- // by a direction the caller listed as less significant.
337
- for (const [field, direction] of keys) {
338
- const key = field === "id" ? MongoConditionBuilder.ID_FIELD : field;
339
- if (!(key in sort)) sort[key] = direction === "desc" ? -1 : 1;
340
- }
341
- // Already named by the caller — at whatever direction and rank they
342
- // chose — the sort is total and there is nothing left to break.
343
- if (!(MongoConditionBuilder.ID_FIELD in sort)) {
344
- sort[MongoConditionBuilder.ID_FIELD] = -1;
345
- }
346
- return sort;
347
- }
348
- }