@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/factory.ts DELETED
@@ -1,331 +0,0 @@
1
- /**
2
- * MongoDB Backend Factory
3
- *
4
- * This module provides factory functions for creating MongoDB backend instances.
5
- * It abstracts the creation of drivers, realtime services, and row services.
6
- */
7
-
8
- import { Db, MongoClient } from "mongodb";
9
- import { DataDriver, CollectionConfig, getCollectionDataPath } from "@rebasepro/types";
10
-
11
- import { MongoDataService } from "./db/MongoDataService";
12
- import { MongoRealtimeService } from "./services/MongoRealtimeService";
13
- import { MongoDriver } from "./services/MongoDriver";
14
- import { MongoHistoryService, HistoryRetentionConfig } from "./services/MongoHistoryService";
15
- import { MongoDBConnection } from "./connection";
16
- import { BackendConfig, BackendInstance, CollectionRegistryInterface, DataRepository, RealtimeProvider, DatabaseConnection, DatabaseAdmin, DocumentAdmin, SchemaAdmin, SchemaEditingAdmin, HealthCheckResult } from "@rebasepro/types";
17
- import { planMongoSchemaChange } from "./schema/plan-schema-change";
18
-
19
- /**
20
- * Configuration for creating a MongoDB backend.
21
- */
22
- export interface MongoBackendConfig extends BackendConfig {
23
- type: "mongodb";
24
- /** MongoDB database instance */
25
- connection: Db;
26
- /** MongoDB client (for connection management) */
27
- client: MongoClient;
28
- /** Collections to register (optional, can be registered later) */
29
- collections?: CollectionConfig[];
30
- /** History retention configuration */
31
- historyRetention?: Partial<HistoryRetentionConfig>;
32
- }
33
-
34
- /**
35
- * MongoDB-specific backend instance with additional MongoDB types.
36
- */
37
- export interface MongoBackendInstance extends BackendInstance {
38
- /** The MongoDB database instance */
39
- db: Db;
40
- /** The MongoDB client */
41
- client: MongoClient;
42
- /** MongoDB DataDriver for use with Rebase */
43
- driver: DataDriver;
44
- /** Entity service for direct database operations */
45
- dataService: MongoDataService;
46
- /** Realtime service for subscriptions */
47
- realtimeService: MongoRealtimeService;
48
- /** Admin capabilities (DocumentAdmin + SchemaAdmin) */
49
- admin: DatabaseAdmin;
50
- }
51
-
52
- // =============================================================================
53
- // Simple Collection Registry
54
- // =============================================================================
55
-
56
- /**
57
- * Simple in-memory collection registry for MongoDB.
58
- */
59
- export class MongoCollectionRegistry implements CollectionRegistryInterface {
60
- /** Every addressable key → collection. See {@link register}. */
61
- private collections = new Map<string, CollectionConfig>();
62
- /** Registration order, so `getCollections()` returns each collection once. */
63
- private registered: CollectionConfig[] = [];
64
- private _globalCallbacks?: any;
65
-
66
- /**
67
- * Register a collection under every name it can be addressed by.
68
- *
69
- * A Mongo collection has up to three: `slug` (the routing key), `path` (the
70
- * MongoDB collection-name override, which is what `getCollectionDataPath`
71
- * hands the driver) and `name` (the human label). Registering only `name`
72
- * meant every `getCollectionByPath` lookup missed — and the realtime path,
73
- * whose only source of the collection is this registry, ran with no
74
- * `securityRules`, no `properties` and no callbacks.
75
- */
76
- register(collection: CollectionConfig): void {
77
- this.registered.push(collection);
78
- for (const key of [getCollectionDataPath(collection), collection.slug, collection.name]) {
79
- if (key) this.collections.set(key, collection);
80
- }
81
- }
82
-
83
- /**
84
- * Get a collection by its path
85
- */
86
- getCollectionByPath(path: string): CollectionConfig | undefined {
87
- return this.collections.get(path);
88
- }
89
-
90
- /**
91
- * Get all registered collections
92
- */
93
- getCollections(): CollectionConfig[] {
94
- return [...this.registered];
95
- }
96
-
97
- /**
98
- * Get the currently registered global callbacks, if any.
99
- */
100
- getGlobalCallbacks(): any | undefined {
101
- return this._globalCallbacks;
102
- }
103
-
104
- /**
105
- * Set global lifecycle callbacks that apply to every collection.
106
- */
107
- setGlobalCallbacks(callbacks: any): void {
108
- this._globalCallbacks = callbacks;
109
- }
110
- }
111
-
112
- // =============================================================================
113
- // Factory Functions
114
- // =============================================================================
115
-
116
- /**
117
- * Create a complete MongoDB backend instance.
118
- *
119
- * This factory function creates all the necessary services for a MongoDB backend:
120
- * - MongoDBConnection (database connection wrapper)
121
- * - MongoDataService (implements DataRepository)
122
- * - MongoRealtimeService (implements RealtimeProvider)
123
- * - MongoCollectionRegistry (implements CollectionRegistryInterface)
124
- * - MongoDriver (for Rebase integration)
125
- *
126
- * @example
127
- * ```typescript
128
- * import { createMongoBackend } from "@rebasepro/server-mongo";
129
- *
130
- * const client = new MongoClient("mongodb://localhost:27017");
131
- * await client.connect();
132
- * const db = client.db("my_database");
133
- *
134
- * const backend = createMongoBackend({
135
- * type: "mongodb",
136
- * connection: db,
137
- * client: client,
138
- * collections: myCollections
139
- * });
140
- *
141
- * // Use the backend
142
- * const rows = await backend.entityRepository.fetchCollection("users", {});
143
- * ```
144
- */
145
- export function createMongoBackend(config: MongoBackendConfig): MongoBackendInstance {
146
- const { connection: db, client, collections } = config;
147
-
148
- // Create collection registry
149
- const collectionRegistry = new MongoCollectionRegistry();
150
-
151
- // Register collections if provided
152
- if (collections) {
153
- collections.forEach(collection => collectionRegistry.register(collection));
154
- }
155
-
156
- // Create services
157
- const dataService = new MongoDataService(db);
158
- const realtimeService = new MongoRealtimeService(db);
159
- const historyService = new MongoHistoryService(db, config.historyRetention);
160
- const driver = new MongoDriver(db, realtimeService, historyService, collectionRegistry);
161
- const mongoConnection = new MongoDBConnection(db, client);
162
-
163
- // Build admin capabilities for MongoDB
164
- const admin: DatabaseAdmin = {
165
- // A document database has no table to alter, so a collection change is
166
- // the commit and nothing else. Offered here because `isSchemaEditingAdmin`
167
- // is a structural check: without this method a Mongo project was told
168
- // live schema editing was *unsupported*, when it is the one place the
169
- // change cannot fail.
170
- planSchemaChange: async (before, after) => planMongoSchemaChange(
171
- before as CollectionConfig[],
172
- after as CollectionConfig[]
173
- ),
174
- async executeAggregate(pipeline: Record<string, unknown>[]) {
175
- // Run aggregation on a collection — requires a target collection
176
- // from the pipeline's $match or $lookup stage:
177
- const firstStage = pipeline[0];
178
- const collName = typeof firstStage.$from === "string" ? firstStage.$from : "__admin__";
179
- const cursor = db.collection(collName).aggregate(pipeline);
180
- return await cursor.toArray() as Record<string, unknown>[];
181
- },
182
- async fetchCollectionStats(collectionName: string) {
183
- const stats = await db.command({ collStats: collectionName }) as { count: number; size: number };
184
- return { count: stats.count,
185
- sizeBytes: stats.size };
186
- },
187
- async fetchUnmappedTables(mappedPaths?: string[]) {
188
- const allCollections = await db.listCollections().toArray();
189
- const names = allCollections.map(c => c.name).filter(n => !n.startsWith("system."));
190
- if (!mappedPaths || mappedPaths.length === 0) return names;
191
- const mappedSet = new Set(mappedPaths.map(p => p.toLowerCase()));
192
- return names.filter(n => !mappedSet.has(n.toLowerCase()));
193
- },
194
- async fetchTableMetadata(collectionName: string) {
195
- // Sample a document to infer fields
196
- const sample = await db.collection(collectionName).findOne();
197
- if (!sample) return { columns: [],
198
- foreignKeys: [],
199
- junctions: [],
200
- policies: [] };
201
- const columns = Object.entries(sample).map(([key, value]) => ({
202
- column_name: key,
203
- data_type: typeof value,
204
- udt_name: typeof value,
205
- is_nullable: "YES",
206
- column_default: null,
207
- character_maximum_length: null
208
- }));
209
- return { columns,
210
- foreignKeys: [],
211
- junctions: [],
212
- policies: [] };
213
- }
214
- // `satisfies` narrows the literal regardless of the annotation above, so
215
- // the schema-editing method has to be named here too or it reads as an
216
- // excess property.
217
- } satisfies DocumentAdmin & SchemaAdmin & SchemaEditingAdmin;
218
-
219
- return {
220
- // Abstract interface implementations
221
- connection: mongoConnection,
222
- entityRepository: dataService,
223
- realtimeProvider: realtimeService,
224
- collectionRegistry: collectionRegistry,
225
- admin,
226
-
227
- // Lifecycle
228
- async initialize() {
229
- // Connection is already established via the MongoClient constructor
230
- },
231
- async healthCheck(): Promise<HealthCheckResult> {
232
- const start = Date.now();
233
- try {
234
- await db.command({ ping: 1 });
235
- return { healthy: true,
236
- latencyMs: Date.now() - start };
237
- } catch {
238
- return { healthy: false,
239
- latencyMs: Date.now() - start };
240
- }
241
- },
242
- async destroy() {
243
- await client.close();
244
- },
245
-
246
- // MongoDB-specific accessors
247
- db,
248
- client,
249
- driver,
250
- dataService,
251
- realtimeService
252
- };
253
- }
254
-
255
- /**
256
- * Create a MongoDB DataDriver.
257
- *
258
- * This is a convenience function when you only need the DataDriver
259
- * without the full backend instance.
260
- *
261
- * @example
262
- * ```typescript
263
- * import { createMongoDelegate } from "@rebasepro/server-mongo";
264
- *
265
- * const delegate = createMongoDelegate(db);
266
- * ```
267
- */
268
- export function createMongoDelegate(
269
- db: Db,
270
- realtimeService?: MongoRealtimeService,
271
- historyService?: MongoHistoryService,
272
- registry?: CollectionRegistryInterface
273
- ): MongoDriver {
274
- const realtime = realtimeService ?? new MongoRealtimeService(db);
275
- const history = historyService ?? new MongoHistoryService(db);
276
- return new MongoDriver(db, realtime, history, registry);
277
- }
278
-
279
- /**
280
- * Create a RealtimeService for MongoDB.
281
- *
282
- * @example
283
- * ```typescript
284
- * import { createMongoRealtimeService } from "@rebasepro/server-mongo";
285
- *
286
- * const realtimeService = createMongoRealtimeService(db);
287
- * ```
288
- */
289
- export function createMongoRealtimeService(db: Db): MongoRealtimeService {
290
- return new MongoRealtimeService(db);
291
- }
292
-
293
- /**
294
- * Create a MongoDB row repository.
295
- *
296
- * @example
297
- * ```typescript
298
- * import { createMongoEntityRepository } from "@rebasepro/server-mongo";
299
- *
300
- * const repository = createMongoEntityRepository(db);
301
- * const users = await repository.fetchCollection("users", {});
302
- * ```
303
- */
304
- export function createMongoEntityRepository(db: Db): DataRepository {
305
- return new MongoDataService(db);
306
- }
307
-
308
- // =============================================================================
309
- // Type Guards
310
- // =============================================================================
311
-
312
- /**
313
- * Check if a backend config is for MongoDB.
314
- */
315
- export function isMongoBackendConfig(config: BackendConfig): config is MongoBackendConfig {
316
- return config.type === "mongodb" &&
317
- typeof (config as MongoBackendConfig).connection !== "undefined" &&
318
- typeof (config as MongoBackendConfig).client !== "undefined";
319
- }
320
-
321
- /**
322
- * Check if a driver config is for MongoDB.
323
- */
324
- export function isMongoDriverConfig(obj: unknown): obj is { type: "mongodb"; connection: Db; client: MongoClient } {
325
- return typeof obj === "object" &&
326
- obj !== null &&
327
- "type" in obj &&
328
- (obj as Record<string, unknown>).type === "mongodb" &&
329
- "connection" in obj &&
330
- "client" in obj;
331
- }
@@ -1,22 +0,0 @@
1
- import { Db } from "mongodb";
2
- import { logger } from "@rebasepro/server";
3
-
4
- export async function ensureHistoryCollectionExists(db: Db): Promise<void> {
5
- logger.info("🔍 Checking MongoDB history collection and indexes...");
6
-
7
- try {
8
- const history = db.collection("__rebase_history");
9
-
10
- // Index for finding history entries for a specific row
11
- await history.createIndex({ entity_id: 1,
12
- table_name: 1,
13
- updated_at: -1 });
14
-
15
- // Index for pruning by date
16
- await history.createIndex({ updated_at: 1 });
17
-
18
- logger.info("✅ MongoDB History collection ready");
19
- } catch (error) {
20
- logger.error("❌ Failed to set up MongoDB history collection", { error: error });
21
- }
22
- }
package/src/index.ts DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * @rebasepro/server-mongo
3
- *
4
- * MongoDB backend implementation for Rebase
5
- * This package provides a complete backend solution for Rebase applications
6
- * using MongoDB as the database.
7
- *
8
- * The package implements the abstract interfaces from @rebasepro/server
9
- * (DataRepository, RealtimeProvider, CollectionRegistryInterface, etc.)
10
- */
11
-
12
- // Connection
13
- export * from "./connection";
14
-
15
- // Factory functions
16
- export * from "./factory";
17
-
18
- // Database services
19
- export * from "./db/MongoDataService";
20
- export * from "./db/MongoConditionBuilder";
21
-
22
- // Services
23
- export * from "./services/MongoRealtimeService";
24
- export * from "./services/MongoDriver";
25
- export * from "./MongoBootstrapper";
@@ -1,159 +0,0 @@
1
- /**
2
- * Planning a collection change for MongoDB.
3
- *
4
- * Shorter than the Postgres one by the whole of its difficulty. A document
5
- * database has no table to alter: adding a property adds nothing, removing one
6
- * removes nothing, and a document written yesterday is still valid tomorrow. So
7
- * the plan is the commit, and there is no DDL to run, refuse or get wrong.
8
- *
9
- * ## Why this exists at all, rather than leaving Mongo unsupported
10
- *
11
- * Because "unsupported" was the wrong answer, and it was the answer only
12
- * because nothing had been written here. `isSchemaEditingAdmin` is a structural
13
- * check — a driver either offers `planSchemaChange` or it does not — so a Mongo
14
- * project got `SCHEMA_EDITING_UNSUPPORTED` and fell back to the source-only
15
- * editor, which is off in production. A schemaless database is the one place
16
- * where changing a collection against a *running* backend is completely safe,
17
- * and it was the one place it did not work.
18
- *
19
- * ## What a verdict means here
20
- *
21
- * Every change is `safe`, because none of them can fail. That is not the
22
- * classifier being lax: on Postgres a verdict answers "will the database end up
23
- * matching this configuration", and here it always does — there is nothing in
24
- * the database that describes the shape.
25
- *
26
- * What the changes still say is what happens to the **data**, because that is
27
- * the part a reader can be surprised by. Removing a property does not delete
28
- * the field from documents that have it; those documents keep it and the API
29
- * stops serving it. Saying so is the difference between a reader who knows the
30
- * data is still there and one who assumes it is gone.
31
- */
32
- import type {
33
- CollectionConfig,
34
- SchemaChange,
35
- SchemaChangePlan
36
- } from "@rebasepro/types";
37
-
38
- const bySlug = (collections: CollectionConfig[]): Map<string, CollectionConfig> => {
39
- const map = new Map<string, CollectionConfig>();
40
- for (const collection of collections) {
41
- if (collection.slug) map.set(collection.slug, collection);
42
- }
43
- return map;
44
- };
45
-
46
- const propertiesOf = (collection: CollectionConfig): Record<string, unknown> =>
47
- (collection.properties ?? {}) as Record<string, unknown>;
48
-
49
- /**
50
- * What changed, in the terms a document database makes true.
51
- *
52
- * Exported because it is the interesting half and worth testing without a
53
- * driver: the shape of the answer is the product, and the rest is plumbing.
54
- */
55
- export function classifyMongoChanges(
56
- before: CollectionConfig[],
57
- after: CollectionConfig[]
58
- ): SchemaChange[] {
59
- const previous = bySlug(before);
60
- const next = bySlug(after);
61
- const changes: SchemaChange[] = [];
62
-
63
- for (const [slug, collection] of next) {
64
- if (!previous.has(slug)) {
65
- changes.push({
66
- kind: "add-collection",
67
- verdict: "safe",
68
- collection: slug,
69
- detail: `New collection "${slug}". MongoDB creates it on the first write; nothing ` +
70
- "is created now."
71
- });
72
- continue;
73
- }
74
-
75
- const oldProps = propertiesOf(previous.get(slug)!);
76
- const newProps = propertiesOf(collection);
77
-
78
- for (const name of Object.keys(newProps)) {
79
- if (name in oldProps) continue;
80
- changes.push({
81
- kind: "add-property",
82
- verdict: "safe",
83
- collection: slug,
84
- property: name,
85
- detail: `New property "${name}". Existing documents do not have it and are not ` +
86
- "rewritten; reads return it as absent until something writes it."
87
- });
88
- }
89
-
90
- for (const name of Object.keys(oldProps)) {
91
- if (name in newProps) continue;
92
- changes.push({
93
- kind: "remove-property",
94
- verdict: "safe",
95
- collection: slug,
96
- property: name,
97
- // The one thing worth being explicit about. On Postgres this is
98
- // refused because it would drop a column; here nothing is
99
- // dropped, and a reader who assumes otherwise has it backwards.
100
- detail: `"${name}" is no longer served. Documents that have it keep it — MongoDB ` +
101
- "stores no schema, so nothing is removed from the data."
102
- });
103
- }
104
- }
105
-
106
- for (const [slug] of previous) {
107
- if (next.has(slug)) continue;
108
- changes.push({
109
- kind: "remove-collection",
110
- verdict: "safe",
111
- collection: slug,
112
- detail: `Collection "${slug}" is no longer served. The MongoDB collection and every ` +
113
- "document in it are left exactly as they are."
114
- });
115
- }
116
-
117
- return changes;
118
- }
119
-
120
- /** A commit message that says what changed rather than that something did. */
121
- function commitMessage(changes: SchemaChange[]): string {
122
- if (changes.length === 0) return "chore(schema): no change";
123
-
124
- const collections = [...new Set(changes.map(c => c.collection))].sort();
125
- const added = changes.filter(c => c.kind === "add-collection");
126
- const properties = changes.filter(c => c.kind === "add-property");
127
-
128
- const subject = added.length === 1 && changes.length === 1
129
- ? `add the ${added[0].collection} collection`
130
- : properties.length === 1 && changes.length === 1
131
- ? `add ${properties[0].property} to ${properties[0].collection}`
132
- : collections.length === 1
133
- ? `${changes.length} change(s) to ${collections[0]}`
134
- : `${changes.length} change(s) across ${collections.length} collections`;
135
-
136
- return `feat(schema): ${subject}\n\n${changes.map(c => `- ${c.detail}`).join("\n")}\n`;
137
- }
138
-
139
- /**
140
- * The plan: a commit, and nothing to run.
141
- *
142
- * `files` is empty here and filled by the caller with the rewritten collection
143
- * source. Postgres adds generated artifacts — a Drizzle schema, declarative
144
- * DDL — because a stale one breaks the next deploy. MongoDB generates none, so
145
- * the collection file is the whole of the change.
146
- */
147
- export async function planMongoSchemaChange(
148
- before: CollectionConfig[],
149
- after: CollectionConfig[]
150
- ): Promise<SchemaChangePlan> {
151
- const changes = classifyMongoChanges(before, after);
152
- return {
153
- files: [],
154
- statements: [],
155
- classified: { changes, verdict: "safe", applicable: true },
156
- message: commitMessage(changes),
157
- withheldConstraints: []
158
- };
159
- }