@rebasepro/server-mongo 0.17.3 → 0.18.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/LICENSE +0 -1
- package/README.md +4 -0
- package/dist/MongoBootstrapper.d.ts +0 -1
- package/dist/auth/ensure-collections.d.ts +0 -1
- package/dist/auth/services.d.ts +0 -1
- package/dist/connection.d.ts +0 -1
- package/dist/db/MongoConditionBuilder.d.ts +0 -1
- package/dist/db/MongoDataService.d.ts +0 -1
- package/dist/db/securityRuleFilter.d.ts +0 -1
- package/dist/factory.d.ts +0 -1
- package/dist/history/ensure-history-collection.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +83 -79
- package/dist/index.es.js.map +1 -1
- package/dist/schema/plan-schema-change.d.ts +0 -1
- package/dist/services/MongoDriver.d.ts +0 -1
- package/dist/services/MongoHistoryService.d.ts +0 -1
- package/dist/services/MongoRealtimeService.d.ts +0 -1
- package/dist/websocket.d.ts +0 -1
- package/package.json +28 -24
- package/dist/MongoBootstrapper.d.ts.map +0 -1
- package/dist/auth/ensure-collections.d.ts.map +0 -1
- package/dist/auth/services.d.ts.map +0 -1
- package/dist/connection.d.ts.map +0 -1
- package/dist/db/MongoConditionBuilder.d.ts.map +0 -1
- package/dist/db/MongoDataService.d.ts.map +0 -1
- package/dist/db/securityRuleFilter.d.ts.map +0 -1
- package/dist/factory.d.ts.map +0 -1
- package/dist/history/ensure-history-collection.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/schema/plan-schema-change.d.ts.map +0 -1
- package/dist/services/MongoDriver.d.ts.map +0 -1
- package/dist/services/MongoHistoryService.d.ts.map +0 -1
- package/dist/services/MongoRealtimeService.d.ts.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/src/MongoBootstrapper.ts +0 -204
- package/src/auth/ensure-collections.ts +0 -153
- package/src/auth/services.ts +0 -866
- package/src/connection.ts +0 -60
- package/src/db/MongoConditionBuilder.ts +0 -348
- package/src/db/MongoDataService.ts +0 -412
- package/src/db/securityRuleFilter.ts +0 -398
- package/src/factory.ts +0 -331
- package/src/history/ensure-history-collection.ts +0 -22
- package/src/index.ts +0 -25
- package/src/schema/plan-schema-change.ts +0 -159
- package/src/services/MongoDriver.ts +0 -950
- package/src/services/MongoHistoryService.ts +0 -186
- package/src/services/MongoRealtimeService.ts +0 -592
- 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
|
-
}
|