@emulators/mongoatlas 0.4.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/dist/index.js ADDED
@@ -0,0 +1,969 @@
1
+ // src/store.ts
2
+ function getMongoAtlasStore(store) {
3
+ return {
4
+ clusters: store.collection("mongoatlas.clusters", ["cluster_id", "name"]),
5
+ databases: store.collection("mongoatlas.databases", ["cluster_id", "name"]),
6
+ collections: store.collection("mongoatlas.collections", ["cluster_id", "database", "name"]),
7
+ documents: store.collection("mongoatlas.documents", ["cluster_id", "doc_id"]),
8
+ projects: store.collection("mongoatlas.projects", ["group_id"]),
9
+ users: store.collection("mongoatlas.users", ["user_id", "username"])
10
+ };
11
+ }
12
+
13
+ // src/helpers.ts
14
+ import { randomBytes } from "crypto";
15
+ function generateObjectId() {
16
+ const timestamp = Math.floor(Date.now() / 1e3).toString(16).padStart(8, "0");
17
+ const random = randomBytes(8).toString("hex").slice(0, 16);
18
+ return (timestamp + random).slice(0, 24);
19
+ }
20
+ function generateClusterId() {
21
+ return randomBytes(12).toString("hex");
22
+ }
23
+ function generateGroupId() {
24
+ return randomBytes(12).toString("hex");
25
+ }
26
+ function generateUserId() {
27
+ return randomBytes(12).toString("hex");
28
+ }
29
+ function mongoOk(c, data, status = 200) {
30
+ return c.json(data, status);
31
+ }
32
+ function mongoError(c, errorCode, detail, status = 400) {
33
+ return c.json({ error: status, errorCode, detail }, status);
34
+ }
35
+
36
+ // src/routes/data-api.ts
37
+ function dataApiRoutes(ctx) {
38
+ const { app, store } = ctx;
39
+ const ms = () => getMongoAtlasStore(store);
40
+ app.post("/app/data-api/v1/action/findOne", async (c) => {
41
+ const body = await c.req.json();
42
+ if (!body.dataSource || !body.database || !body.collection) {
43
+ return mongoError(c, "InvalidParameter", "dataSource, database, and collection are required");
44
+ }
45
+ const cluster = ms().clusters.findOneBy("name", body.dataSource);
46
+ if (!cluster) {
47
+ return mongoError(c, "ClusterNotFound", `Cluster '${body.dataSource}' not found`, 404);
48
+ }
49
+ const docs = ms().documents.all().filter(
50
+ (d) => d.cluster_id === cluster.cluster_id && d.database === body.database && d.collection === body.collection
51
+ );
52
+ const matched = matchFilter(docs, body.filter) ?? docs;
53
+ const doc = matched[0] ?? null;
54
+ const projected = doc ? applyProjection(doc.data, body.projection) : null;
55
+ return mongoOk(c, { document: projected });
56
+ });
57
+ app.post("/app/data-api/v1/action/find", async (c) => {
58
+ const body = await c.req.json();
59
+ if (!body.dataSource || !body.database || !body.collection) {
60
+ return mongoError(c, "InvalidParameter", "dataSource, database, and collection are required");
61
+ }
62
+ const cluster = ms().clusters.findOneBy("name", body.dataSource);
63
+ if (!cluster) {
64
+ return mongoError(c, "ClusterNotFound", `Cluster '${body.dataSource}' not found`, 404);
65
+ }
66
+ let docs = ms().documents.all().filter(
67
+ (d) => d.cluster_id === cluster.cluster_id && d.database === body.database && d.collection === body.collection
68
+ );
69
+ docs = matchFilter(docs, body.filter) ?? docs;
70
+ if (body.sort) {
71
+ docs = sortBySpec(docs, body.sort, (d) => d.data);
72
+ }
73
+ if (body.skip) {
74
+ docs = docs.slice(body.skip);
75
+ }
76
+ if (body.limit) {
77
+ docs = docs.slice(0, body.limit);
78
+ }
79
+ const documents = docs.map((d) => applyProjection(d.data, body.projection));
80
+ return mongoOk(c, { documents });
81
+ });
82
+ app.post("/app/data-api/v1/action/insertOne", async (c) => {
83
+ const body = await c.req.json();
84
+ if (!body.dataSource || !body.database || !body.collection || !body.document) {
85
+ return mongoError(c, "InvalidParameter", "dataSource, database, collection, and document are required");
86
+ }
87
+ const cluster = ms().clusters.findOneBy("name", body.dataSource);
88
+ if (!cluster) {
89
+ return mongoError(c, "ClusterNotFound", `Cluster '${body.dataSource}' not found`, 404);
90
+ }
91
+ ensureCollectionExists(ms, cluster.cluster_id, body.database, body.collection);
92
+ const docId = body.document._id ?? generateObjectId();
93
+ const data = { ...body.document, _id: docId };
94
+ ms().documents.insert({
95
+ cluster_id: cluster.cluster_id,
96
+ database: body.database,
97
+ collection: body.collection,
98
+ doc_id: docId,
99
+ data
100
+ });
101
+ return mongoOk(c, { insertedId: docId }, 201);
102
+ });
103
+ app.post("/app/data-api/v1/action/insertMany", async (c) => {
104
+ const body = await c.req.json();
105
+ if (!body.dataSource || !body.database || !body.collection || !body.documents) {
106
+ return mongoError(c, "InvalidParameter", "dataSource, database, collection, and documents are required");
107
+ }
108
+ const cluster = ms().clusters.findOneBy("name", body.dataSource);
109
+ if (!cluster) {
110
+ return mongoError(c, "ClusterNotFound", `Cluster '${body.dataSource}' not found`, 404);
111
+ }
112
+ ensureCollectionExists(ms, cluster.cluster_id, body.database, body.collection);
113
+ const insertedIds = [];
114
+ for (const doc of body.documents) {
115
+ const docId = doc._id ?? generateObjectId();
116
+ const data = { ...doc, _id: docId };
117
+ ms().documents.insert({
118
+ cluster_id: cluster.cluster_id,
119
+ database: body.database,
120
+ collection: body.collection,
121
+ doc_id: docId,
122
+ data
123
+ });
124
+ insertedIds.push(docId);
125
+ }
126
+ return mongoOk(c, { insertedIds }, 201);
127
+ });
128
+ app.post("/app/data-api/v1/action/updateOne", async (c) => {
129
+ const body = await c.req.json();
130
+ if (!body.dataSource || !body.database || !body.collection || !body.update) {
131
+ return mongoError(c, "InvalidParameter", "dataSource, database, collection, and update are required");
132
+ }
133
+ const cluster = ms().clusters.findOneBy("name", body.dataSource);
134
+ if (!cluster) {
135
+ return mongoError(c, "ClusterNotFound", `Cluster '${body.dataSource}' not found`, 404);
136
+ }
137
+ const docs = ms().documents.all().filter(
138
+ (d) => d.cluster_id === cluster.cluster_id && d.database === body.database && d.collection === body.collection
139
+ );
140
+ const matched = matchFilter(docs, body.filter) ?? docs;
141
+ const doc = matched[0];
142
+ if (doc) {
143
+ const updatedData = applyUpdate(doc.data, body.update);
144
+ ms().documents.update(doc.id, { data: updatedData });
145
+ return mongoOk(c, { matchedCount: 1, modifiedCount: 1 });
146
+ }
147
+ if (body.upsert) {
148
+ ensureCollectionExists(ms, cluster.cluster_id, body.database, body.collection);
149
+ const docId = generateObjectId();
150
+ const baseDoc = extractEqualityFields(body.filter ?? {});
151
+ const data = applyUpdate({ _id: docId, ...baseDoc }, body.update);
152
+ ms().documents.insert({
153
+ cluster_id: cluster.cluster_id,
154
+ database: body.database,
155
+ collection: body.collection,
156
+ doc_id: docId,
157
+ data
158
+ });
159
+ return mongoOk(c, { matchedCount: 0, modifiedCount: 0, upsertedId: docId });
160
+ }
161
+ return mongoOk(c, { matchedCount: 0, modifiedCount: 0 });
162
+ });
163
+ app.post("/app/data-api/v1/action/updateMany", async (c) => {
164
+ const body = await c.req.json();
165
+ if (!body.dataSource || !body.database || !body.collection || !body.update) {
166
+ return mongoError(c, "InvalidParameter", "dataSource, database, collection, and update are required");
167
+ }
168
+ const cluster = ms().clusters.findOneBy("name", body.dataSource);
169
+ if (!cluster) {
170
+ return mongoError(c, "ClusterNotFound", `Cluster '${body.dataSource}' not found`, 404);
171
+ }
172
+ const docs = ms().documents.all().filter(
173
+ (d) => d.cluster_id === cluster.cluster_id && d.database === body.database && d.collection === body.collection
174
+ );
175
+ const matched = matchFilter(docs, body.filter) ?? docs;
176
+ let modifiedCount = 0;
177
+ for (const doc of matched) {
178
+ const updatedData = applyUpdate(doc.data, body.update);
179
+ ms().documents.update(doc.id, { data: updatedData });
180
+ modifiedCount++;
181
+ }
182
+ if (matched.length === 0 && body.upsert) {
183
+ ensureCollectionExists(ms, cluster.cluster_id, body.database, body.collection);
184
+ const docId = generateObjectId();
185
+ const baseDoc = extractEqualityFields(body.filter ?? {});
186
+ const data = applyUpdate({ _id: docId, ...baseDoc }, body.update);
187
+ ms().documents.insert({
188
+ cluster_id: cluster.cluster_id,
189
+ database: body.database,
190
+ collection: body.collection,
191
+ doc_id: docId,
192
+ data
193
+ });
194
+ return mongoOk(c, { matchedCount: 0, modifiedCount: 0, upsertedId: docId });
195
+ }
196
+ return mongoOk(c, { matchedCount: matched.length, modifiedCount });
197
+ });
198
+ app.post("/app/data-api/v1/action/deleteOne", async (c) => {
199
+ const body = await c.req.json();
200
+ if (!body.dataSource || !body.database || !body.collection) {
201
+ return mongoError(c, "InvalidParameter", "dataSource, database, and collection are required");
202
+ }
203
+ const cluster = ms().clusters.findOneBy("name", body.dataSource);
204
+ if (!cluster) {
205
+ return mongoError(c, "ClusterNotFound", `Cluster '${body.dataSource}' not found`, 404);
206
+ }
207
+ const docs = ms().documents.all().filter(
208
+ (d) => d.cluster_id === cluster.cluster_id && d.database === body.database && d.collection === body.collection
209
+ );
210
+ const matched = matchFilter(docs, body.filter) ?? docs;
211
+ const doc = matched[0];
212
+ if (doc) {
213
+ ms().documents.delete(doc.id);
214
+ return mongoOk(c, { deletedCount: 1 });
215
+ }
216
+ return mongoOk(c, { deletedCount: 0 });
217
+ });
218
+ app.post("/app/data-api/v1/action/deleteMany", async (c) => {
219
+ const body = await c.req.json();
220
+ if (!body.dataSource || !body.database || !body.collection) {
221
+ return mongoError(c, "InvalidParameter", "dataSource, database, and collection are required");
222
+ }
223
+ const cluster = ms().clusters.findOneBy("name", body.dataSource);
224
+ if (!cluster) {
225
+ return mongoError(c, "ClusterNotFound", `Cluster '${body.dataSource}' not found`, 404);
226
+ }
227
+ const docs = ms().documents.all().filter(
228
+ (d) => d.cluster_id === cluster.cluster_id && d.database === body.database && d.collection === body.collection
229
+ );
230
+ const matched = matchFilter(docs, body.filter) ?? docs;
231
+ let deletedCount = 0;
232
+ for (const doc of matched) {
233
+ ms().documents.delete(doc.id);
234
+ deletedCount++;
235
+ }
236
+ return mongoOk(c, { deletedCount });
237
+ });
238
+ app.post("/app/data-api/v1/action/aggregate", async (c) => {
239
+ const body = await c.req.json();
240
+ if (!body.dataSource || !body.database || !body.collection) {
241
+ return mongoError(c, "InvalidParameter", "dataSource, database, and collection are required");
242
+ }
243
+ const cluster = ms().clusters.findOneBy("name", body.dataSource);
244
+ if (!cluster) {
245
+ return mongoError(c, "ClusterNotFound", `Cluster '${body.dataSource}' not found`, 404);
246
+ }
247
+ let docs = ms().documents.all().filter(
248
+ (d) => d.cluster_id === cluster.cluster_id && d.database === body.database && d.collection === body.collection
249
+ );
250
+ const pipeline = body.pipeline ?? [];
251
+ let results = docs.map((d) => d.data);
252
+ for (const stage of pipeline) {
253
+ if ("$match" in stage) {
254
+ const filter = stage.$match;
255
+ results = results.filter((d) => matchesFilter(d, filter));
256
+ } else if ("$limit" in stage) {
257
+ results = results.slice(0, stage.$limit);
258
+ } else if ("$skip" in stage) {
259
+ results = results.slice(stage.$skip);
260
+ } else if ("$sort" in stage) {
261
+ const sortSpec = stage.$sort;
262
+ results = sortBySpec(results, sortSpec, (d) => d);
263
+ } else if ("$project" in stage) {
264
+ const projection = stage.$project;
265
+ results = results.map((d) => applyProjection(d, projection));
266
+ } else if ("$count" in stage) {
267
+ const fieldName = stage.$count;
268
+ results = [{ [fieldName]: results.length }];
269
+ }
270
+ }
271
+ return mongoOk(c, { documents: results });
272
+ });
273
+ }
274
+ function ensureCollectionExists(ms, clusterId, database, collection) {
275
+ const existing = ms().collections.all().find(
276
+ (col) => col.cluster_id === clusterId && col.database === database && col.name === collection
277
+ );
278
+ if (!existing) {
279
+ const dbExists = ms().databases.all().find(
280
+ (db) => db.cluster_id === clusterId && db.name === database
281
+ );
282
+ if (!dbExists) {
283
+ ms().databases.insert({ cluster_id: clusterId, name: database });
284
+ }
285
+ ms().collections.insert({ cluster_id: clusterId, database, name: collection });
286
+ }
287
+ }
288
+ function matchesFilter(data, filter) {
289
+ for (const [key, value] of Object.entries(filter)) {
290
+ if (key === "$and") {
291
+ const conditions = value;
292
+ if (!conditions.every((cond) => matchesFilter(data, cond))) return false;
293
+ continue;
294
+ }
295
+ if (key === "$or") {
296
+ const conditions = value;
297
+ if (!conditions.some((cond) => matchesFilter(data, cond))) return false;
298
+ continue;
299
+ }
300
+ if (key === "$nor") {
301
+ const conditions = value;
302
+ if (conditions.some((cond) => matchesFilter(data, cond))) return false;
303
+ continue;
304
+ }
305
+ const docValue = getNestedValue(data, key);
306
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
307
+ const ops = value;
308
+ for (const [op, opVal] of Object.entries(ops)) {
309
+ switch (op) {
310
+ case "$eq":
311
+ if (docValue !== opVal) return false;
312
+ break;
313
+ case "$ne":
314
+ if (docValue === opVal) return false;
315
+ break;
316
+ case "$gt":
317
+ if (typeof docValue !== "number" || typeof opVal !== "number" || docValue <= opVal) return false;
318
+ break;
319
+ case "$gte":
320
+ if (typeof docValue !== "number" || typeof opVal !== "number" || docValue < opVal) return false;
321
+ break;
322
+ case "$lt":
323
+ if (typeof docValue !== "number" || typeof opVal !== "number" || docValue >= opVal) return false;
324
+ break;
325
+ case "$lte":
326
+ if (typeof docValue !== "number" || typeof opVal !== "number" || docValue > opVal) return false;
327
+ break;
328
+ case "$in":
329
+ if (!Array.isArray(opVal) || !opVal.includes(docValue)) return false;
330
+ break;
331
+ case "$nin":
332
+ if (!Array.isArray(opVal) || opVal.includes(docValue)) return false;
333
+ break;
334
+ case "$exists":
335
+ if (opVal && docValue === void 0) return false;
336
+ if (!opVal && docValue !== void 0) return false;
337
+ break;
338
+ case "$regex": {
339
+ const pattern = opVal;
340
+ const flags = ops.$options ?? "";
341
+ try {
342
+ if (pattern.length > 1e3) return false;
343
+ const re = new RegExp(pattern, flags);
344
+ if (typeof docValue !== "string" || !re.test(docValue)) return false;
345
+ } catch {
346
+ return false;
347
+ }
348
+ break;
349
+ }
350
+ }
351
+ }
352
+ } else {
353
+ if (docValue !== value) return false;
354
+ }
355
+ }
356
+ return true;
357
+ }
358
+ function getNestedValue(obj, path) {
359
+ const parts = path.split(".");
360
+ if (hasDangerousKey(parts)) return void 0;
361
+ let current = obj;
362
+ for (const part of parts) {
363
+ if (current === null || current === void 0 || typeof current !== "object") return void 0;
364
+ current = current[part];
365
+ }
366
+ return current;
367
+ }
368
+ function matchFilter(docs, filter) {
369
+ if (!filter || Object.keys(filter).length === 0) return null;
370
+ return docs.filter((d) => matchesFilter(d.data, filter));
371
+ }
372
+ function applyProjection(data, projection) {
373
+ if (!projection || Object.keys(projection).length === 0) return data;
374
+ const hasInclusions = Object.values(projection).some((v) => v === 1 || v === true);
375
+ if (hasInclusions) {
376
+ const result2 = {};
377
+ if (projection._id !== 0 && projection._id !== false) {
378
+ result2._id = data._id;
379
+ }
380
+ for (const [key, val] of Object.entries(projection)) {
381
+ if (key === "_id") continue;
382
+ if (val === 1 || val === true) {
383
+ result2[key] = data[key];
384
+ }
385
+ }
386
+ return result2;
387
+ }
388
+ const result = { ...data };
389
+ for (const [key, val] of Object.entries(projection)) {
390
+ if (val === 0 || val === false) {
391
+ delete result[key];
392
+ }
393
+ }
394
+ return result;
395
+ }
396
+ function applyUpdate(data, update) {
397
+ const result = { ...data };
398
+ if ("$set" in update) {
399
+ const setFields = update.$set;
400
+ for (const [key, value] of Object.entries(setFields)) {
401
+ setNestedValue(result, key, value);
402
+ }
403
+ }
404
+ if ("$unset" in update) {
405
+ const unsetFields = update.$unset;
406
+ for (const key of Object.keys(unsetFields)) {
407
+ const parts = key.split(".");
408
+ if (hasDangerousKey(parts)) continue;
409
+ if (parts.length === 1) {
410
+ delete result[key];
411
+ } else {
412
+ let current = result;
413
+ for (let i = 0; i < parts.length - 1; i++) {
414
+ if (current[parts[i]] === null || current[parts[i]] === void 0 || typeof current[parts[i]] !== "object") break;
415
+ current = current[parts[i]];
416
+ }
417
+ delete current[parts[parts.length - 1]];
418
+ }
419
+ }
420
+ }
421
+ if ("$inc" in update) {
422
+ const incFields = update.$inc;
423
+ for (const [key, value] of Object.entries(incFields)) {
424
+ const current = getNestedValue(result, key) ?? 0;
425
+ setNestedValue(result, key, current + value);
426
+ }
427
+ }
428
+ if ("$push" in update) {
429
+ const pushFields = update.$push;
430
+ for (const [key, value] of Object.entries(pushFields)) {
431
+ const current = getNestedValue(result, key);
432
+ if (!Array.isArray(current)) {
433
+ setNestedValue(result, key, [value]);
434
+ } else {
435
+ setNestedValue(result, key, [...current, value]);
436
+ }
437
+ }
438
+ }
439
+ if ("$pull" in update) {
440
+ const pullFields = update.$pull;
441
+ for (const [key, value] of Object.entries(pullFields)) {
442
+ const current = getNestedValue(result, key);
443
+ if (Array.isArray(current)) {
444
+ setNestedValue(result, key, current.filter((item) => item !== value));
445
+ }
446
+ }
447
+ }
448
+ if ("$rename" in update) {
449
+ const renameFields = update.$rename;
450
+ for (const [oldKey, newKey] of Object.entries(renameFields)) {
451
+ const oldParts = oldKey.split(".");
452
+ const newParts = newKey.split(".");
453
+ if (hasDangerousKey(oldParts) || hasDangerousKey(newParts)) continue;
454
+ const value = getNestedValue(result, oldKey);
455
+ if (value !== void 0) {
456
+ setNestedValue(result, newKey, value);
457
+ if (oldParts.length === 1) {
458
+ delete result[oldKey];
459
+ } else {
460
+ let current = result;
461
+ for (let i = 0; i < oldParts.length - 1; i++) {
462
+ if (typeof current[oldParts[i]] !== "object" || current[oldParts[i]] === null) break;
463
+ current = current[oldParts[i]];
464
+ }
465
+ delete current[oldParts[oldParts.length - 1]];
466
+ }
467
+ }
468
+ }
469
+ }
470
+ const hasOperators = Object.keys(update).some((k) => k.startsWith("$"));
471
+ if (!hasOperators) {
472
+ const id = result._id;
473
+ for (const key of Object.keys(result)) {
474
+ delete result[key];
475
+ }
476
+ result._id = id;
477
+ Object.assign(result, update);
478
+ }
479
+ return result;
480
+ }
481
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
482
+ function hasDangerousKey(parts) {
483
+ return parts.some((p) => DANGEROUS_KEYS.has(p));
484
+ }
485
+ function setNestedValue(obj, path, value) {
486
+ const parts = path.split(".");
487
+ if (hasDangerousKey(parts)) return;
488
+ let current = obj;
489
+ for (let i = 0; i < parts.length - 1; i++) {
490
+ if (!(parts[i] in current) || typeof current[parts[i]] !== "object") {
491
+ current[parts[i]] = {};
492
+ }
493
+ current = current[parts[i]];
494
+ }
495
+ current[parts[parts.length - 1]] = value;
496
+ }
497
+ function sortBySpec(docs, sortSpec, accessor) {
498
+ return [...docs].sort((a, b) => {
499
+ for (const [key, direction] of Object.entries(sortSpec)) {
500
+ const aVal = getNestedValue(accessor(a), key);
501
+ const bVal = getNestedValue(accessor(b), key);
502
+ if (aVal === bVal) continue;
503
+ if (aVal === void 0) return direction;
504
+ if (bVal === void 0) return -direction;
505
+ if (aVal < bVal) return -direction;
506
+ if (aVal > bVal) return direction;
507
+ }
508
+ return 0;
509
+ });
510
+ }
511
+ function extractEqualityFields(filter) {
512
+ const result = {};
513
+ for (const [key, value] of Object.entries(filter)) {
514
+ if (key.startsWith("$")) continue;
515
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
516
+ const ops = value;
517
+ if (Object.keys(ops).some((k) => k.startsWith("$"))) continue;
518
+ }
519
+ result[key] = value;
520
+ }
521
+ return result;
522
+ }
523
+
524
+ // src/routes/admin.ts
525
+ function adminRoutes(ctx) {
526
+ const { app, store } = ctx;
527
+ const ms = () => getMongoAtlasStore(store);
528
+ app.get("/api/atlas/v2/groups", (c) => {
529
+ const projects = ms().projects.all();
530
+ return mongoOk(c, {
531
+ results: projects.map(formatProject),
532
+ totalCount: projects.length
533
+ });
534
+ });
535
+ app.get("/api/atlas/v2/groups/:groupId", (c) => {
536
+ const groupId = c.req.param("groupId");
537
+ const project = ms().projects.findOneBy("group_id", groupId);
538
+ if (!project) {
539
+ return mongoError(c, "GROUP_NOT_FOUND", `Group '${groupId}' not found.`, 404);
540
+ }
541
+ return mongoOk(c, formatProject(project));
542
+ });
543
+ app.post("/api/atlas/v2/groups", async (c) => {
544
+ const body = await c.req.json();
545
+ if (!body.name?.trim()) {
546
+ return mongoError(c, "INVALID_PARAMETER", "name is required");
547
+ }
548
+ const existing = ms().projects.all().find((p) => p.name === body.name);
549
+ if (existing) {
550
+ return mongoError(c, "DUPLICATE_GROUP_NAME", `Group name '${body.name}' already exists.`, 409);
551
+ }
552
+ const groupId = generateGroupId();
553
+ const project = ms().projects.insert({
554
+ group_id: groupId,
555
+ name: body.name,
556
+ org_id: body.orgId ?? "default_org",
557
+ cluster_count: 0
558
+ });
559
+ return mongoOk(c, formatProject(project), 201);
560
+ });
561
+ app.delete("/api/atlas/v2/groups/:groupId", (c) => {
562
+ const groupId = c.req.param("groupId");
563
+ const project = ms().projects.findOneBy("group_id", groupId);
564
+ if (!project) {
565
+ return mongoError(c, "GROUP_NOT_FOUND", `Group '${groupId}' not found.`, 404);
566
+ }
567
+ const clusters = ms().clusters.all().filter((cl) => cl.group_id === groupId);
568
+ for (const cluster of clusters) {
569
+ deleteClusterData(ms, cluster.cluster_id);
570
+ ms().clusters.delete(cluster.id);
571
+ }
572
+ ms().projects.delete(project.id);
573
+ return c.body(null, 204);
574
+ });
575
+ app.get("/api/atlas/v2/groups/:groupId/clusters", (c) => {
576
+ const groupId = c.req.param("groupId");
577
+ const project = ms().projects.findOneBy("group_id", groupId);
578
+ if (!project) {
579
+ return mongoError(c, "GROUP_NOT_FOUND", `Group '${groupId}' not found.`, 404);
580
+ }
581
+ const clusters = ms().clusters.all().filter((cl) => cl.group_id === groupId);
582
+ return mongoOk(c, {
583
+ results: clusters.map(formatCluster),
584
+ totalCount: clusters.length
585
+ });
586
+ });
587
+ app.get("/api/atlas/v2/groups/:groupId/clusters/:clusterName", (c) => {
588
+ const groupId = c.req.param("groupId");
589
+ const clusterName = c.req.param("clusterName");
590
+ const cluster = ms().clusters.all().find(
591
+ (cl) => cl.group_id === groupId && cl.name === clusterName
592
+ );
593
+ if (!cluster) {
594
+ return mongoError(c, "CLUSTER_NOT_FOUND", `Cluster '${clusterName}' not found.`, 404);
595
+ }
596
+ return mongoOk(c, formatCluster(cluster));
597
+ });
598
+ app.post("/api/atlas/v2/groups/:groupId/clusters", async (c) => {
599
+ const groupId = c.req.param("groupId");
600
+ const project = ms().projects.findOneBy("group_id", groupId);
601
+ if (!project) {
602
+ return mongoError(c, "GROUP_NOT_FOUND", `Group '${groupId}' not found.`, 404);
603
+ }
604
+ const body = await c.req.json();
605
+ if (!body.name?.trim()) {
606
+ return mongoError(c, "INVALID_PARAMETER", "name is required");
607
+ }
608
+ const existing = ms().clusters.all().find(
609
+ (cl) => cl.group_id === groupId && cl.name === body.name
610
+ );
611
+ if (existing) {
612
+ return mongoError(c, "DUPLICATE_CLUSTER_NAME", `Cluster '${body.name}' already exists.`, 409);
613
+ }
614
+ const clusterId = generateClusterId();
615
+ const cluster = ms().clusters.insert({
616
+ cluster_id: clusterId,
617
+ name: body.name,
618
+ group_id: groupId,
619
+ state: "IDLE",
620
+ mongo_uri: `mongodb+srv://${body.name}.emulate.mongodb.net`,
621
+ connection_strings: {
622
+ standard: `mongodb://${body.name}.emulate.mongodb.net:27017`,
623
+ standard_srv: `mongodb+srv://${body.name}.emulate.mongodb.net`
624
+ },
625
+ provider_settings: {
626
+ provider_name: body.providerSettings?.providerName ?? "AWS",
627
+ instance_size_name: body.providerSettings?.instanceSizeName ?? "M10",
628
+ region_name: body.providerSettings?.regionName ?? "US_EAST_1"
629
+ },
630
+ cluster_type: body.clusterType ?? "REPLICASET",
631
+ disk_size_gb: body.diskSizeGB ?? 10,
632
+ mongodb_version: body.mongoDBMajorVersion ?? "8.0"
633
+ });
634
+ ms().projects.update(project.id, { cluster_count: project.cluster_count + 1 });
635
+ return mongoOk(c, formatCluster(cluster), 201);
636
+ });
637
+ app.patch("/api/atlas/v2/groups/:groupId/clusters/:clusterName", async (c) => {
638
+ const groupId = c.req.param("groupId");
639
+ const clusterName = c.req.param("clusterName");
640
+ const cluster = ms().clusters.all().find(
641
+ (cl) => cl.group_id === groupId && cl.name === clusterName
642
+ );
643
+ if (!cluster) {
644
+ return mongoError(c, "CLUSTER_NOT_FOUND", `Cluster '${clusterName}' not found.`, 404);
645
+ }
646
+ const body = await c.req.json();
647
+ const updates = {};
648
+ if (body.providerSettings) {
649
+ updates.provider_settings = {
650
+ provider_name: cluster.provider_settings.provider_name,
651
+ instance_size_name: body.providerSettings.instanceSizeName ?? cluster.provider_settings.instance_size_name,
652
+ region_name: body.providerSettings.regionName ?? cluster.provider_settings.region_name
653
+ };
654
+ }
655
+ if (body.diskSizeGB !== void 0) {
656
+ updates.disk_size_gb = body.diskSizeGB;
657
+ }
658
+ const updated = ms().clusters.update(cluster.id, updates);
659
+ return mongoOk(c, formatCluster(updated));
660
+ });
661
+ app.delete("/api/atlas/v2/groups/:groupId/clusters/:clusterName", (c) => {
662
+ const groupId = c.req.param("groupId");
663
+ const clusterName = c.req.param("clusterName");
664
+ const cluster = ms().clusters.all().find(
665
+ (cl) => cl.group_id === groupId && cl.name === clusterName
666
+ );
667
+ if (!cluster) {
668
+ return mongoError(c, "CLUSTER_NOT_FOUND", `Cluster '${clusterName}' not found.`, 404);
669
+ }
670
+ deleteClusterData(ms, cluster.cluster_id);
671
+ ms().clusters.delete(cluster.id);
672
+ const project = ms().projects.findOneBy("group_id", groupId);
673
+ if (project) {
674
+ ms().projects.update(project.id, { cluster_count: Math.max(0, project.cluster_count - 1) });
675
+ }
676
+ return c.body(null, 204);
677
+ });
678
+ app.get("/api/atlas/v2/groups/:groupId/databaseUsers", (c) => {
679
+ const groupId = c.req.param("groupId");
680
+ const users = ms().users.all().filter((u) => u.group_id === groupId);
681
+ return mongoOk(c, {
682
+ results: users.map(formatUser),
683
+ totalCount: users.length
684
+ });
685
+ });
686
+ app.get("/api/atlas/v2/groups/:groupId/databaseUsers/admin/:username", (c) => {
687
+ const groupId = c.req.param("groupId");
688
+ const username = c.req.param("username");
689
+ const user = ms().users.all().find((u) => u.group_id === groupId && u.username === username);
690
+ if (!user) {
691
+ return mongoError(c, "USER_NOT_FOUND", `Database user '${username}' not found.`, 404);
692
+ }
693
+ return mongoOk(c, formatUser(user));
694
+ });
695
+ app.post("/api/atlas/v2/groups/:groupId/databaseUsers", async (c) => {
696
+ const groupId = c.req.param("groupId");
697
+ const body = await c.req.json();
698
+ if (!body.username?.trim()) {
699
+ return mongoError(c, "INVALID_PARAMETER", "username is required");
700
+ }
701
+ const existing = ms().users.all().find((u) => u.group_id === groupId && u.username === body.username);
702
+ if (existing) {
703
+ return mongoError(c, "DUPLICATE_USER", `User '${body.username}' already exists.`, 409);
704
+ }
705
+ const userId = generateUserId();
706
+ const user = ms().users.insert({
707
+ user_id: userId,
708
+ username: body.username,
709
+ group_id: groupId,
710
+ roles: (body.roles ?? []).map((r) => ({ database_name: r.databaseName, role_name: r.roleName }))
711
+ });
712
+ return mongoOk(c, formatUser(user), 201);
713
+ });
714
+ app.delete("/api/atlas/v2/groups/:groupId/databaseUsers/admin/:username", (c) => {
715
+ const groupId = c.req.param("groupId");
716
+ const username = c.req.param("username");
717
+ const user = ms().users.all().find((u) => u.group_id === groupId && u.username === username);
718
+ if (!user) {
719
+ return mongoError(c, "USER_NOT_FOUND", `Database user '${username}' not found.`, 404);
720
+ }
721
+ ms().users.delete(user.id);
722
+ return c.body(null, 204);
723
+ });
724
+ app.get("/api/atlas/v2/groups/:groupId/clusters/:clusterName/databases", (c) => {
725
+ const groupId = c.req.param("groupId");
726
+ const clusterName = c.req.param("clusterName");
727
+ const cluster = ms().clusters.all().find(
728
+ (cl) => cl.group_id === groupId && cl.name === clusterName
729
+ );
730
+ if (!cluster) {
731
+ return mongoError(c, "CLUSTER_NOT_FOUND", `Cluster '${clusterName}' not found.`, 404);
732
+ }
733
+ const databases = ms().databases.all().filter((db) => db.cluster_id === cluster.cluster_id);
734
+ return mongoOk(c, {
735
+ results: databases.map((db) => ({ databaseName: db.name })),
736
+ totalCount: databases.length
737
+ });
738
+ });
739
+ app.get("/api/atlas/v2/groups/:groupId/clusters/:clusterName/databases/:databaseName/collections", (c) => {
740
+ const groupId = c.req.param("groupId");
741
+ const clusterName = c.req.param("clusterName");
742
+ const databaseName = c.req.param("databaseName");
743
+ const cluster = ms().clusters.all().find(
744
+ (cl) => cl.group_id === groupId && cl.name === clusterName
745
+ );
746
+ if (!cluster) {
747
+ return mongoError(c, "CLUSTER_NOT_FOUND", `Cluster '${clusterName}' not found.`, 404);
748
+ }
749
+ const collections = ms().collections.all().filter(
750
+ (col) => col.cluster_id === cluster.cluster_id && col.database === databaseName
751
+ );
752
+ return mongoOk(c, {
753
+ results: collections.map((col) => ({ collectionName: col.name, databaseName })),
754
+ totalCount: collections.length
755
+ });
756
+ });
757
+ }
758
+ function deleteClusterData(ms, clusterId) {
759
+ const docs = ms().documents.all().filter((d) => d.cluster_id === clusterId);
760
+ for (const doc of docs) ms().documents.delete(doc.id);
761
+ const cols = ms().collections.all().filter((col) => col.cluster_id === clusterId);
762
+ for (const col of cols) ms().collections.delete(col.id);
763
+ const dbs = ms().databases.all().filter((db) => db.cluster_id === clusterId);
764
+ for (const db of dbs) ms().databases.delete(db.id);
765
+ }
766
+ function formatProject(p) {
767
+ return {
768
+ id: p.group_id,
769
+ name: p.name,
770
+ orgId: p.org_id,
771
+ clusterCount: p.cluster_count,
772
+ created: p.created_at
773
+ };
774
+ }
775
+ function formatCluster(cl) {
776
+ return {
777
+ id: cl.cluster_id,
778
+ name: cl.name,
779
+ groupId: cl.group_id,
780
+ stateName: cl.state,
781
+ mongoURI: cl.mongo_uri,
782
+ connectionStrings: {
783
+ standard: cl.connection_strings.standard,
784
+ standardSrv: cl.connection_strings.standard_srv
785
+ },
786
+ providerSettings: {
787
+ providerName: cl.provider_settings.provider_name,
788
+ instanceSizeName: cl.provider_settings.instance_size_name,
789
+ regionName: cl.provider_settings.region_name
790
+ },
791
+ clusterType: cl.cluster_type,
792
+ diskSizeGB: cl.disk_size_gb,
793
+ mongoDBVersion: cl.mongodb_version,
794
+ created: cl.created_at
795
+ };
796
+ }
797
+ function formatUser(u) {
798
+ return {
799
+ username: u.username,
800
+ groupId: u.group_id,
801
+ databaseName: "admin",
802
+ roles: u.roles.map((r) => ({ databaseName: r.database_name, roleName: r.role_name }))
803
+ };
804
+ }
805
+
806
+ // src/index.ts
807
+ function seedDefaults(store, _baseUrl) {
808
+ const ms = getMongoAtlasStore(store);
809
+ const groupId = generateGroupId();
810
+ ms.projects.insert({
811
+ group_id: groupId,
812
+ name: "Project0",
813
+ org_id: "default_org",
814
+ cluster_count: 1
815
+ });
816
+ const clusterId = generateClusterId();
817
+ ms.clusters.insert({
818
+ cluster_id: clusterId,
819
+ name: "Cluster0",
820
+ group_id: groupId,
821
+ state: "IDLE",
822
+ mongo_uri: "mongodb+srv://Cluster0.emulate.mongodb.net",
823
+ connection_strings: {
824
+ standard: "mongodb://Cluster0.emulate.mongodb.net:27017",
825
+ standard_srv: "mongodb+srv://Cluster0.emulate.mongodb.net"
826
+ },
827
+ provider_settings: {
828
+ provider_name: "AWS",
829
+ instance_size_name: "M10",
830
+ region_name: "US_EAST_1"
831
+ },
832
+ cluster_type: "REPLICASET",
833
+ disk_size_gb: 10,
834
+ mongodb_version: "8.0"
835
+ });
836
+ ms.users.insert({
837
+ user_id: generateUserId(),
838
+ username: "admin",
839
+ group_id: groupId,
840
+ roles: [{ database_name: "admin", role_name: "atlasAdmin" }]
841
+ });
842
+ ms.databases.insert({ cluster_id: clusterId, name: "test" });
843
+ ms.collections.insert({ cluster_id: clusterId, database: "test", name: "items" });
844
+ }
845
+ function seedFromConfig(store, _baseUrl, config) {
846
+ const ms = getMongoAtlasStore(store);
847
+ const projectIdMap = /* @__PURE__ */ new Map();
848
+ if (config.projects) {
849
+ for (const p of config.projects) {
850
+ const existing = ms.projects.all().find((ep) => ep.name === p.name);
851
+ if (existing) {
852
+ projectIdMap.set(p.name, existing.group_id);
853
+ continue;
854
+ }
855
+ const groupId = generateGroupId();
856
+ ms.projects.insert({
857
+ group_id: groupId,
858
+ name: p.name,
859
+ org_id: p.org_id ?? "default_org",
860
+ cluster_count: 0
861
+ });
862
+ projectIdMap.set(p.name, groupId);
863
+ }
864
+ }
865
+ const defaultProject = ms.projects.all()[0];
866
+ if (defaultProject) {
867
+ projectIdMap.set(defaultProject.name, defaultProject.group_id);
868
+ }
869
+ const clusterIdMap = /* @__PURE__ */ new Map();
870
+ if (config.clusters) {
871
+ for (const cl of config.clusters) {
872
+ const groupId = projectIdMap.get(cl.project);
873
+ if (!groupId) continue;
874
+ const existing = ms.clusters.all().find(
875
+ (ec) => ec.group_id === groupId && ec.name === cl.name
876
+ );
877
+ if (existing) {
878
+ clusterIdMap.set(cl.name, existing.cluster_id);
879
+ continue;
880
+ }
881
+ const clusterId = generateClusterId();
882
+ ms.clusters.insert({
883
+ cluster_id: clusterId,
884
+ name: cl.name,
885
+ group_id: groupId,
886
+ state: "IDLE",
887
+ mongo_uri: `mongodb+srv://${cl.name}.emulate.mongodb.net`,
888
+ connection_strings: {
889
+ standard: `mongodb://${cl.name}.emulate.mongodb.net:27017`,
890
+ standard_srv: `mongodb+srv://${cl.name}.emulate.mongodb.net`
891
+ },
892
+ provider_settings: {
893
+ provider_name: cl.provider ?? "AWS",
894
+ instance_size_name: cl.instance_size ?? "M10",
895
+ region_name: cl.region ?? "US_EAST_1"
896
+ },
897
+ cluster_type: "REPLICASET",
898
+ disk_size_gb: cl.disk_size_gb ?? 10,
899
+ mongodb_version: cl.mongodb_version ?? "8.0"
900
+ });
901
+ clusterIdMap.set(cl.name, clusterId);
902
+ const project = ms.projects.findOneBy("group_id", groupId);
903
+ if (project) {
904
+ ms.projects.update(project.id, { cluster_count: project.cluster_count + 1 });
905
+ }
906
+ }
907
+ }
908
+ const defaultCluster = ms.clusters.all()[0];
909
+ if (defaultCluster) {
910
+ clusterIdMap.set(defaultCluster.name, defaultCluster.cluster_id);
911
+ }
912
+ if (config.database_users) {
913
+ for (const u of config.database_users) {
914
+ const groupId = projectIdMap.get(u.project);
915
+ if (!groupId) continue;
916
+ const existing = ms.users.all().find(
917
+ (eu) => eu.group_id === groupId && eu.username === u.username
918
+ );
919
+ if (existing) continue;
920
+ ms.users.insert({
921
+ user_id: generateUserId(),
922
+ username: u.username,
923
+ group_id: groupId,
924
+ roles: u.roles ?? [{ database_name: "admin", role_name: "readWriteAnyDatabase" }]
925
+ });
926
+ }
927
+ }
928
+ if (config.databases) {
929
+ for (const db of config.databases) {
930
+ const clusterId = clusterIdMap.get(db.cluster);
931
+ if (!clusterId) continue;
932
+ const existingDb = ms.databases.all().find(
933
+ (edb) => edb.cluster_id === clusterId && edb.name === db.name
934
+ );
935
+ if (!existingDb) {
936
+ ms.databases.insert({ cluster_id: clusterId, name: db.name });
937
+ }
938
+ if (db.collections) {
939
+ for (const colName of db.collections) {
940
+ const existingCol = ms.collections.all().find(
941
+ (ec) => ec.cluster_id === clusterId && ec.database === db.name && ec.name === colName
942
+ );
943
+ if (!existingCol) {
944
+ ms.collections.insert({ cluster_id: clusterId, database: db.name, name: colName });
945
+ }
946
+ }
947
+ }
948
+ }
949
+ }
950
+ }
951
+ var mongoatlasPlugin = {
952
+ name: "mongoatlas",
953
+ register(app, store, webhooks, baseUrl, tokenMap) {
954
+ const ctx = { app, store, webhooks, baseUrl, tokenMap };
955
+ adminRoutes(ctx);
956
+ dataApiRoutes(ctx);
957
+ },
958
+ seed(store, baseUrl) {
959
+ seedDefaults(store, baseUrl);
960
+ }
961
+ };
962
+ var index_default = mongoatlasPlugin;
963
+ export {
964
+ index_default as default,
965
+ getMongoAtlasStore,
966
+ mongoatlasPlugin,
967
+ seedFromConfig
968
+ };
969
+ //# sourceMappingURL=index.js.map