@rebasepro/server-mongo 0.21.2-canary.g1ea48be → 0.23.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.
@@ -20,6 +20,14 @@ export declare class MongoUserService implements UserRepository {
20
20
  getUserIdentities(uid: string): Promise<UserIdentityData[]>;
21
21
  linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
22
22
  updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null>;
23
+ /**
24
+ * Everything keyed by the user goes with them. Postgres does this by
25
+ * foreign-key cascade; Mongo has none, so it is spelled out — and the
26
+ * refresh tokens are the part that matters: left behind, a deleted user's
27
+ * session kept minting access tokens for a uid that no longer exists.
28
+ * Sessions first, so a failure part-way leaves an account that cannot sign
29
+ * in rather than a session with no account.
30
+ */
23
31
  deleteUser(id: string): Promise<void>;
24
32
  listUsers(): Promise<UserData[]>;
25
33
  listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult>;
@@ -4,6 +4,7 @@
4
4
  * Translates Rebase filter conditions to MongoDB query operators.
5
5
  */
6
6
  import { CollectionConfig, FilterValues, LogicalCondition, OrderByTuple } from "@rebasepro/types";
7
+ import { type FieldViewer } from "@rebasepro/common";
7
8
  import { Filter, Document } from "mongodb";
8
9
  /**
9
10
  * MongoDB Condition Builder
@@ -29,24 +30,53 @@ export declare class MongoConditionBuilder {
29
30
  *
30
31
  * Always returns a condition or throws: there is no operator this can be
31
32
  * given that legitimately means "no condition".
33
+ *
34
+ * `negated` asks for the documents where the condition is *false* — not
35
+ * merely not true. The two differ by the documents where it is unknown,
36
+ * which in SQL is a comparison against NULL: `NOT (status = 'draft')` does
37
+ * not return a row whose status is NULL, and the Postgres driver is the
38
+ * reference every caller of this API writes against. So a comparison is
39
+ * false only where the field holds a value and the comparison fails on it.
40
+ * The null tests are never unknown, and negate to each other.
32
41
  */
33
42
  private static buildCondition;
34
43
  /**
35
- * Translate an `or(...)` / `and(...)` group, nesting included.
44
+ * Translate an `or(...)` / `and(...)` / `not(...)` group, nesting included.
45
+ *
46
+ * `not` negates the conjunction of its conditions — the rule stated on
47
+ * `LogicalCondition`. It used to fall through to the `and` branch, so
48
+ * `not(status = 'draft')` returned exactly the drafts. It is compiled by
49
+ * pushing the negation down to the leaves (De Morgan), each of which knows
50
+ * which documents make it false rather than merely not true — see
51
+ * {@link buildCondition}. A `$nor` around the group would be shorter and
52
+ * wrong: it also matches every document where the condition is unknown.
36
53
  *
37
54
  * Returns `undefined` for a group with nothing in it. `$or: []` is an error
38
55
  * in Mongo and `$and: []` matches every document, so neither is a
39
56
  * defensible reading of "no conditions".
40
57
  */
41
- static buildLogicalConditions(logical: LogicalCondition | undefined): Filter<Document> | undefined;
58
+ static buildLogicalConditions(logical: LogicalCondition | undefined, negated?: boolean): Filter<Document> | undefined;
42
59
  /**
43
- * Build search conditions for text search
60
+ * Build search conditions for text search.
61
+ *
62
+ * Terms are split on whitespace and AND-ed, while the fields are OR-ed
63
+ * within each term: a person typing `sebastian melendez` into a search box
64
+ * means both words, and the two live in different fields, so matching the
65
+ * whole string per field finds nothing. See {@link splitSearchTerms}; the
66
+ * Postgres driver's fallback path answers the same way.
44
67
  *
45
68
  * @param searchString - Text to search for
46
69
  * @param properties - The collection's properties, searched for string fields
47
- * @returns Array of MongoDB filter objects for text search
70
+ * @param viewer - Who the search is for. A field they cannot read is not
71
+ * one they may search: the strip keeps its value out of the response, and
72
+ * without this the value is still recoverable a substring at a time by
73
+ * watching which searches return the row. Absent is the trusted server
74
+ * plane, which still does not search an `excludeFromApi` field — the same
75
+ * answer the Postgres builder gives.
76
+ * @returns At most one MongoDB filter — callers OR what they get back, which
77
+ * is right across fields and wrong across terms, so the AND is built here
48
78
  */
49
- static buildSearchConditions(searchString: string, properties: CollectionConfig["properties"]): Filter<Document>[];
79
+ static buildSearchConditions(searchString: string, properties: CollectionConfig["properties"], viewer?: FieldViewer): Filter<Document>[];
50
80
  /**
51
81
  * Combine multiple conditions with AND operator
52
82
  *
@@ -78,6 +108,8 @@ export declare class MongoConditionBuilder {
78
108
  logical?: LogicalCondition;
79
109
  searchString?: string;
80
110
  properties?: CollectionConfig["properties"];
111
+ /** Who the query is for — see {@link buildSearchConditions}. */
112
+ viewer?: FieldViewer;
81
113
  }): Filter<Document>;
82
114
  /**
83
115
  * The primary key's name in a stored document.
@@ -96,8 +96,39 @@ export declare class MongoDataService implements DataRepository {
96
96
  * partial row everywhere it went: the REST response, `afterSave`, the
97
97
  * history entry a revert restores from, and the row pushed to realtime
98
98
  * subscribers. Postgres returns the whole row here (`RETURNING *`).
99
+ *
100
+ * `mode` is which write the caller means when it names an `id`:
101
+ *
102
+ * - `"create"` inserts under that id, and a taken id is a 409. The driver
103
+ * authorizes a create as an insert, against the values sent — so a
104
+ * create that could land on an existing row is an update no update rule
105
+ * ever saw. It was one: an upsert with `$set`, which let a caller name
106
+ * another user's id with `status: "new"` and rewrite their row.
107
+ * - `"update"` updates that row and nothing else, and a missing one is a
108
+ * 404 — never a row conjured under an id nobody created.
109
+ * - absent, the repository's own "create or update": an upsert. Only the
110
+ * repository's direct callers reach it; the driver always says which.
111
+ *
112
+ * `values` may carry field operations (`{ views: { $inc: 1 } }`), which
113
+ * change the stored value in the same write rather than replace it — see
114
+ * {@link compileFieldOp}. They were written into the document as literal
115
+ * objects, so a counter became `{ "$inc": 1 }` behind a 200. Only an
116
+ * update can carry one: there is no stored value for an insert to act on,
117
+ * and an upsert may be an insert.
118
+ */
119
+ save<M extends Record<string, any>>(collectionPath: string, values: Partial<M>, id?: string | number, _databaseId?: string, mode?: "create" | "update"): Promise<Record<string, unknown>>;
120
+ /**
121
+ * The aggregation expression one field operation compiles to, in an update
122
+ * pipeline's `$set`.
123
+ *
124
+ * The same meaning the Postgres compiler gives each one, NULL-safety
125
+ * included: an unset or null counter increments from zero and an absent
126
+ * array is pushed onto as an empty one, rather than the write failing on
127
+ * the first use of a field. `$push` and `$pull` take a value or a list of
128
+ * values, `$pull` removes every occurrence, and `$merge` is shallow — a
129
+ * nested object replaces, it is not merged into.
99
130
  */
100
- save<M extends Record<string, any>>(collectionPath: string, values: Partial<M>, id?: string | number, _databaseId?: string): Promise<Record<string, unknown>>;
131
+ private compileFieldOp;
101
132
  /**
102
133
  * Read the document back after a write, falling back to the caller's own
103
134
  * values if it has already been removed by a concurrent delete.
@@ -115,9 +146,21 @@ export declare class MongoDataService implements DataRepository {
115
146
  */
116
147
  delete(collectionPath: string, id: string | number, _databaseId?: string): Promise<void>;
117
148
  /**
118
- * Check if a field value is unique in a collection
149
+ * Check if a field value is unique in a collection.
150
+ *
151
+ * An equality test on one field, and nothing else a query can express. The
152
+ * name and the value arrive from a socket frame, and this used to be
153
+ * `countDocuments({ [name]: value })`: a value of `{ $regex: "^123" }`
154
+ * answered, one prefix at a time, what a field of a row the caller cannot
155
+ * see begins with, and a name of `$expr` evaluated an aggregation
156
+ * expression. So an operator name is refused, the value must be a scalar,
157
+ * and it is compared with `$eq` — which reads an object as a literal even
158
+ * if one got this far.
159
+ *
160
+ * An absent value is unique, as it is on Postgres: an empty optional field
161
+ * is not "taken" by every row that also left it empty.
119
162
  */
120
- checkUniqueField(collectionPath: string, fieldName: string, value: any, excludeEntityId?: string, _databaseId?: string): Promise<boolean>;
163
+ checkUniqueField(collectionPath: string, fieldName: string, value: unknown, excludeEntityId?: string, _databaseId?: string): Promise<boolean>;
121
164
  /**
122
165
  * Generate a new row ID
123
166
  */