Repository navigation
Expand file tree
/
Copy pathdata.jsx
More file actions
963 lines (935 loc) · 51.1 KB
/
Copy pathdata.jsx
File metadata and controls
963 lines (935 loc) · 51.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
/* DBLingo — content data
* Six worlds × multiple levels. Each level declares its `kind`:
* - "shape" → drag fields into a JSON document shape
* - "blocks" → drag operator/value tokens into snippet slots
* - "reorder" → drag pipeline stages into the right order
* - "index" → drag index types onto fields of a collection
* - "fill" → click-to-fill blanks from a choice bank
* Each level also carries its prompt, the source for the live preview pane,
* a "why this works" concept blurb, and the correct-answer key.
*/
const HINTS = /*EDITMODE-BEGIN*/{
"$match": "Filters docs against a condition — like a WHERE clause.",
"$group": "Groups docs by a key and accumulates totals.",
"$project": "Reshapes each doc: include, rename, or compute fields.",
"$lookup": "Joins another collection by matching a foreign key.",
"$unwind": "Explodes an array field into one doc per element.",
"$sort": "Orders results by one or more fields.",
"$limit": "Returns only the first N docs after sort.",
"$search": "Atlas Search stage for full-text search inside aggregate().",
"$eq": "Matches exactly equal values.",
"$gt": "Greater than. ($gte for inclusive.)",
"$lt": "Less than. ($lte for inclusive.)",
"$in": "Matches any value in an array.",
"$text": "Full-text search across indexed string fields.",
"$vectorSearch": "Atlas-only stage: k-NN search over a vector index.",
"find": "Returns matching documents from a collection.",
"insertOne": "Inserts a single document, returns its _id.",
"updateOne": "Updates the first matching doc with a `$set` patch.",
"deleteOne": "Deletes the first matching doc.",
"explain": "Shows how MongoDB ran a query: which index it used (if any) and how many docs it read.",
"executionStats": "explain() mode that actually runs the query and reports docs/keys examined and time taken.",
"IXSCAN": "Index scan: MongoDB walks an index to jump straight to matching docs. Fast.",
"COLLSCAN": "Collection scan: no usable index, so MongoDB reads every document. Slow on big collections.",
"createIndex": "Builds an index on one or more fields. Reads get fast.",
"watch": "Opens a change stream cursor for inserts, updates, deletes, and replaces.",
"$source": "Atlas Stream Processing stage that reads from a streaming source.",
"$merge": "Writes aggregation or stream output into a target collection.",
"TTL": "Time-To-Live: docs expire automatically after N seconds.",
"ESR": "Equality → Sort → Range. Compound-index field order rule.",
"covered": "All projected fields live in the index — no fetch needed.",
"compound": "Index across multiple fields. Order matters (ESR)."
}/*HINTS-END*/;
const LEVEL_HINTS = {
d1: [
'Match each field to the BSON/JSON shape the document expects: string, number, array, or embedded document.',
'Strings need quotes; numbers do not. Arrays use [ ], and embedded documents use { }.'
],
d2: [
'Find the collection name first, then choose the MongoDB write operation.',
'insertOne() writes exactly one document object and returns an inserted _id.'
],
d3: [
'Use $set when you want to patch fields without replacing the whole document.',
'The first argument finds the document; the second argument describes the update.'
],
d4: [
'Use deleteOne when the task asks to remove exactly one matching document.',
'If a sort is present, it decides which matching document is removed first.'
],
q1: [
'Equality queries match a field directly to one value.',
'The filter shape is { field: value }; string values need quotes.'
],
q2: [
'$match is the aggregation stage that filters documents.',
'Use $gt for “greater than”; keep numeric thresholds as numbers, not strings.'
],
q3: [
'$in expects an array of possible values.',
'Use $in when one field can match any value from a list.'
],
q4: [
'MongoDB implicitly ANDs conditions listed in the same filter object.',
'Use range operators like $gte/$lte or $gt/$lt inside the field condition.'
],
a1: [
'Filter early to reduce how much data later stages process.',
'Group before sorting when the sort depends on an aggregated value.'
],
a2: [
'$project controls the output shape returned by the pipeline.',
'Use _id: 0 to hide MongoDB’s default _id field when the UI does not need it.'
],
a3: [
'$lookup attaches matching documents from another collection as an array.',
'Use $unwind after $lookup when you want one joined object instead of an array.'
],
a4: [
'$sort chooses the order; $limit chooses how many results remain.',
'For newest or highest first, descending sort usually uses -1.'
],
i1: [
'Pick the index based on the whole query pattern, not just the field name.',
'Text search belongs to Atlas Search; low-cardinality booleans often should not be indexed alone.'
],
i2: [
'ESR means Equality fields first, then Sort fields, then Range fields.',
'Range predicates like $gt/$lt usually come after equality and sort keys in the compound index.'
],
i3: [
'TTL indexes require a date field and expireAfterSeconds.',
'Convert days or hours into seconds before filling the TTL value.'
],
i4: [
'A covered query projects only fields that exist in the index.',
'Exclude _id with _id: 0 unless _id is also part of the index.'
],
v1: [
'$vectorSearch needs the indexed vector path and a numeric limit.',
'The query vector comes from an embedding model; Atlas returns nearest neighbors.'
],
v2: [
'Atlas Search runs inside aggregate() as a $search stage.',
'Use the text operator with a path and query for full-text matching.'
],
v3: [
'A trigger watches one collection for specific operation types.',
'The function name should be a string reference to the server-side handler.'
],
v4: [
'For RAG, retrieve relevant records first, then trim the payload for the LLM.',
'Use filters to keep retrieval scoped to the right tenant, store, org, or domain.'
],
s1: [
'watch() opens a change stream; filter operationType to only react to inserts, updates, deletes, or replaces.',
'Change stream events include operationType plus fullDocument when configured for inserts and updates.'
],
s2: [
'Filter change events on fullDocument fields so only meaningful business events flow downstream.',
'Use numeric thresholds as numbers, not strings.'
],
s3: [
'Stream processors usually read from a source, filter early, project the event shape, then write or route the result.',
'Keep only the fields the downstream alert, dashboard, or workflow needs.'
],
s4: [
'Route processed events into an operational collection or handler that downstream apps can consume.',
'Real-time pipelines should be idempotent because events can be retried.'
]
};
/* Official-docs "Learn more" links shown in each level's "Why does this work?"
* card. Keyed by the shared level IDs so every pack inherits them; a level can
* override with its own `docs: [{ label, url }]`. */
const MDB_DOCS = 'https://www.mongodb.com/docs';
const LEVEL_DOCS = {
d1: [{ label: 'Documents', url: MDB_DOCS + '/manual/core/document/' }],
d2: [{ label: 'insertOne()', url: MDB_DOCS + '/manual/reference/method/db.collection.insertone/' }],
d3: [
{ label: 'updateOne()', url: MDB_DOCS + '/manual/reference/method/db.collection.updateone/' },
{ label: '$set', url: MDB_DOCS + '/manual/reference/operator/update/set/' }
],
d4: [{ label: 'deleteOne()', url: MDB_DOCS + '/manual/reference/method/db.collection.deleteone/' }],
q1: [{ label: 'Query documents', url: MDB_DOCS + '/manual/tutorial/query-documents/' }],
q2: [
{ label: '$match', url: MDB_DOCS + '/manual/reference/operator/aggregation/match/' },
{ label: '$gt', url: MDB_DOCS + '/manual/reference/operator/query/gt/' }
],
q3: [{ label: '$in', url: MDB_DOCS + '/manual/reference/operator/query/in/' }],
q4: [{ label: '$and', url: MDB_DOCS + '/manual/reference/operator/query/and/' }],
a1: [
{ label: 'Aggregation pipeline', url: MDB_DOCS + '/manual/core/aggregation-pipeline/' },
{ label: '$group', url: MDB_DOCS + '/manual/reference/operator/aggregation/group/' }
],
a2: [{ label: '$project', url: MDB_DOCS + '/manual/reference/operator/aggregation/project/' }],
a3: [
{ label: '$lookup', url: MDB_DOCS + '/manual/reference/operator/aggregation/lookup/' },
{ label: '$unwind', url: MDB_DOCS + '/manual/reference/operator/aggregation/unwind/' }
],
a4: [
{ label: '$sort', url: MDB_DOCS + '/manual/reference/operator/aggregation/sort/' },
{ label: '$limit', url: MDB_DOCS + '/manual/reference/operator/aggregation/limit/' }
],
i1: [
{ label: 'Indexes', url: MDB_DOCS + '/manual/indexes/' },
{ label: 'Explain results (IXSCAN, COLLSCAN)', url: MDB_DOCS + '/manual/reference/explain-results/' }
],
i2: [
{ label: 'The ESR guideline', url: MDB_DOCS + '/manual/tutorial/equality-sort-range-guideline/' },
{ label: 'Compound indexes', url: MDB_DOCS + '/manual/core/indexes/index-types/index-compound/' }
],
i3: [{ label: 'TTL indexes', url: MDB_DOCS + '/manual/core/index-ttl/' }],
i4: [
{ label: 'explain()', url: MDB_DOCS + '/manual/reference/method/cursor.explain/' },
{ label: 'Reading explain results', url: MDB_DOCS + '/manual/reference/explain-results/' },
{ label: 'Covered queries', url: MDB_DOCS + '/manual/core/query-optimization/' }
],
v1: [{ label: '$vectorSearch', url: MDB_DOCS + '/vector-search/query/aggregation-stages/vector-search-stage/' }],
v2: [{ label: '$search', url: MDB_DOCS + '/search/query/aggregation-stages/search/' }],
v3: [{ label: 'Database triggers', url: MDB_DOCS + '/atlas/atlas-ui/triggers/database-triggers/' }],
v4: [{ label: 'RAG with Vector Search', url: MDB_DOCS + '/vector-search/tutorials/rag/' }],
s1: [{ label: 'Change streams', url: MDB_DOCS + '/manual/changestreams/' }],
s2: [{ label: 'Change streams', url: MDB_DOCS + '/manual/changestreams/' }],
s3: [
{ label: 'Atlas Stream Processing', url: MDB_DOCS + '/atlas/atlas-stream-processing/' },
{ label: '$source', url: MDB_DOCS + '/atlas/atlas-stream-processing/sp-agg-source/' }
],
s4: [{ label: '$merge (stream processing)', url: MDB_DOCS + '/atlas/atlas-stream-processing/sp-agg-merge/' }]
};
/* Optional next steps shown when a learner completes every level in a world,
* and re-openable from the world banner on the map afterwards.
* Keyed by the shared world IDs so every learning pack (which clones WORLDS)
* gets the same topical recommendations. `type` defaults to 'Skill Badge';
* set it for anything else (e.g. a full course) so the UI labels it honestly. */
const SKILL_BADGES_HOME = 'https://learn.mongodb.com/skills';
const GENAI_PATH = 'https://learn.mongodb.com/learning-paths/mongodb-genai-developer';
const WORLD_SKILL_BADGES = {
docs: [
{ title: 'Relational to Document Model', url: 'https://learn.mongodb.com/courses/relational-to-document-model' },
{ title: 'Schema Design Patterns and Anti-patterns', url: 'https://learn.mongodb.com/courses/schema-design-patterns-and-antipatterns' },
{ title: 'Advanced Schema Patterns and Anti-patterns', url: 'https://learn.mongodb.com/courses/advanced-schema-patterns-and-antipatterns' },
{ title: 'Schema Design Optimization', url: 'https://learn.mongodb.com/courses/schema-design-optimization' }
],
query: [
{ title: 'CRUD Operations', url: 'https://learn.mongodb.com/courses/crud-operations-in-mongodb' }
],
agg: [
{ title: 'Fundamentals of Data Transformation', url: 'https://learn.mongodb.com/courses/fundamentals-of-data-transformation' }
],
idx: [
{ title: 'Query Optimization', url: 'https://learn.mongodb.com/courses/query-optimization' }
],
atlas: [
{ title: 'AI Data Strategy with MongoDB', url: 'https://learn.mongodb.com/courses/ai-data-strategy-with-mongodb' },
{ title: 'Building an App with Code Agents and MongoDB', url: 'https://learn.mongodb.com/courses/building-an-app-with-code-agents-and-mongodb' },
// No public per-badge URL found for these two; they live in the GenAI Developer path.
{ title: 'Vector Search Fundamentals', url: GENAI_PATH },
{ title: 'RAG with MongoDB', url: GENAI_PATH },
{ title: 'AI Agents with MongoDB', url: 'https://learn.mongodb.com/courses/ai-agents-with-mongodb' },
{ title: 'Memory for AI Applications with MongoDB', url: SKILL_BADGES_HOME }
],
streams: [
{ title: 'Atlas Stream Processing', url: 'https://learn.mongodb.com/courses/atlas-stream-processing', type: 'Course' }
]
};
const WORLDS = [
/* ============================================================ WORLD 1 */
{
id: 'docs',
icon: 'leaf',
name: 'Documents & Collections',
tagline: 'Shape your data',
blurb: 'JSON-shaped documents, organized into collections. Where MongoDB begins.',
tint: 'rgba(0,237,100,0.18)',
tintSolid: '#00ED64',
levels: [
{
id: 'd1', title: 'Build a user document', kind: 'shape',
prompt: 'Drag the right values into the user document for Ada Lovelace.',
sub: 'Documents can combine strings and numbers with arrays and embedded documents.',
why: 'A MongoDB document is BSON — JSON with extra types (ObjectId, Date, Decimal128). Related values can live together as arrays and embedded documents, matching the application object without joins.',
skeleton: [
{ key: '_id', type: 'oid', value: 'ObjectId("64e...")' },
{ key: 'name', type: 'slot', slot: 'name' },
{ key: 'age', type: 'slot', slot: 'age' },
{ key: 'skills', type: 'slot', slot: 'skills' },
{ key: 'preferences', type: 'slot', slot: 'preferences' }
],
bank: [
{ id: 'name', label: '"Ada Lovelace"', kind: 'value' },
{ id: 'age', label: '36', kind: 'value' },
{ id: 'skills', label: '["math", "programming"]', kind: 'value' },
{ id: 'preferences', label: '{ theme: "dark", digest: "weekly" }', kind: 'value' },
/* distractors */
{ id: 'd1', label: '"36"', kind: 'value' },
{ id: 'd2', label: 'Ada Lovelace', kind: 'value' },
{ id: 'd3', label: '[math, "programming"]', kind: 'value' }
],
answer: { name: 'name', age: 'age', skills: 'skills', preferences: 'preferences' }
},
{
id: 'd2', title: 'insertOne()', kind: 'blocks',
prompt: 'Insert a new product into the `products` collection. Drag the missing pieces.',
sub: 'insertOne() takes a single document and returns its generated _id.',
why: '`insertOne()` is the simplest write. For many docs, use `insertMany()`. MongoDB auto-generates an `_id` if you don\'t provide one.',
snippet: [
'db.', { slot: 'col' }, '.', { slot: 'op' }, '({',
'\n name: ', { slot: 'val' }, ',',
'\n price: 24.99,',
'\n inStock: ', { slot: 'bool' },
'\n})'
],
bank: [
{ id: 'op', label: 'insertOne', kind: 'op', answer: 'op' },
{ id: 'col', label: 'products', kind: 'field', answer: 'col' },
{ id: 'val', label: '"Espresso"', kind: 'value', answer: 'val' },
{ id: 'bool', label: 'true', kind: 'value', answer: 'bool' },
/* distractors */
{ id: 'x1', label: 'addOne', kind: 'op' },
{ id: 'x2', label: 'inventory', kind: 'field' },
{ id: 'x3', label: 'Espresso', kind: 'value' }
]
},
{
id: 'd3', title: 'updateOne()', kind: 'fill',
prompt: 'Mark order #1042 as shipped.',
sub: 'Use the $set operator to change one or more fields in place.',
why: 'Without `$set` you would *replace* the entire doc. `$set` patches only the listed fields and leaves the rest alone.',
snippet: [
'db.orders.updateOne(',
'\n { orderId: 1042 },',
'\n { ', { blank: 'op' }, ': { ', { blank: 'field' }, ': ', { blank: 'val' }, ' } }',
'\n)'
],
choices: {
op: { options: ['$set', '$push', '$inc', '$replace'], answer: '$set' },
field: { options: ['status', 'state', 'shipped', 'flag'], answer: 'status' },
val: { options: ['"shipped"', 'shipped', 'true', '1'], answer: '"shipped"' }
}
},
{
id: 'd4', title: 'deleteOne()', kind: 'fill',
prompt: 'Remove the cancelled order with the highest id.',
sub: 'deleteOne() removes the first doc that matches — no second chances.',
why: 'Filter precision matters: a too-broad filter on `deleteMany()` is the classic MongoDB foot-gun. Always preview with `find()` first.',
snippet: [
'db.orders.', { blank: 'op' }, '(',
'\n { status: ', { blank: 'val' }, ' },',
'\n { sort: { orderId: ', { blank: 'dir' }, ' } }',
'\n)'
],
choices: {
op: { options: ['deleteOne', 'removeOne', 'dropOne'], answer: 'deleteOne' },
val: { options: ['"cancelled"', 'cancelled', 'null'], answer: '"cancelled"' },
dir: { options: ['-1', '1', '"desc"'], answer: '-1' }
}
}
]
},
/* ============================================================ WORLD 2 */
{
id: 'query',
icon: 'magnify',
name: 'Querying',
tagline: 'Ask the database questions',
blurb: 'find() and the operator family: $eq, $gt, $lt, $in, $and, $or.',
tint: 'rgba(1,107,248,0.22)',
tintSolid: '#4DA3FF',
levels: [
{
id: 'q1', title: 'Equality with find()', kind: 'blocks',
prompt: 'Find every user whose role is "admin".',
sub: 'For equality, you can write the value directly — $eq is implicit.',
why: 'When you write `{ field: value }`, Mongo treats it as `$eq`. Explicit `$eq` is only required inside larger expressions.',
snippet: [
'db.users.find({ ',
{ slot: 'field' }, ': ', { slot: 'val' },
' })'
],
bank: [
{ id: 'field', label: 'role', kind: 'field', answer: 'field' },
{ id: 'val', label: '"admin"', kind: 'value', answer: 'val' },
/* distractors */
{ id: 'x1', label: 'roles', kind: 'field' },
{ id: 'x2', label: 'admin', kind: 'value' },
{ id: 'x3', label: 'isAdmin', kind: 'field' }
]
},
{
id: 'q2', title: '$gt with $match', kind: 'blocks',
prompt: 'Find orders over $100. Drag the operator and value.',
sub: 'In aggregation, you wrap conditions in $match. Operators start with $.',
why: '$match early in a pipeline lets the planner push the filter down to an index — same speed as a regular `find()`.',
snippet: [
'db.orders.aggregate([',
'\n { ', { slot: 'stage' }, ': { total: { ', { slot: 'op' }, ': ', { slot: 'val' }, ' } } }',
'\n])'
],
bank: [
{ id: 'stage', label: '$match', kind: 'stage', answer: 'stage' },
{ id: 'op', label: '$gt', kind: 'op', answer: 'op' },
{ id: 'val', label: '100', kind: 'value', answer: 'val' },
/* distractors */
{ id: 'x1', label: '$where', kind: 'stage' },
{ id: 'x2', label: '$eq', kind: 'op' },
{ id: 'x3', label: '$lt', kind: 'op' },
{ id: 'x4', label: '"100"', kind: 'value' }
]
},
{
id: 'q3', title: '$in — multi-value match', kind: 'fill',
prompt: 'Find products in the "coffee" or "tea" categories.',
sub: '$in matches if a field equals any value in the array.',
why: '`$in` is shorthand for an `$or` of equality checks. It uses indexes efficiently — prefer it over a string of `$or`s.',
snippet: [
'db.products.find({',
'\n category: { ', { blank: 'op' }, ': ', { blank: 'arr' }, ' }',
'\n})'
],
choices: {
op: { options: ['$in', '$any', '$or', '$contains'], answer: '$in' },
arr: {
options: ['["coffee", "tea"]', '"coffee, tea"', '{ coffee, tea }', '[coffee, tea]'],
answer: '["coffee", "tea"]'
}
}
},
{
id: 'q4', title: 'Compound filter', kind: 'fill',
prompt: 'Find active users aged 18–25.',
sub: 'Combine equality and range operators in one filter object.',
why: 'When all conditions are on different fields, just list them — Mongo ANDs them implicitly. No need for explicit `$and`.',
snippet: [
'db.users.find({',
'\n active: true,',
'\n age: { ', { blank: 'gte' }, ': 18, ', { blank: 'lte' }, ': 25 }',
'\n})'
],
choices: {
gte: { options: ['$gte', '$gt', '$geq', '>='], answer: '$gte' },
lte: { options: ['$lte', '$lt', '$leq', '<='], answer: '$lte' }
}
}
]
},
/* ============================================================ WORLD 3 */
{
id: 'agg',
icon: 'diagram',
name: 'Aggregation Pipeline',
tagline: 'Compose data transformations',
blurb: 'Stack stages like a UNIX pipe. Each stage feeds the next.',
tint: 'rgba(255,192,16,0.20)',
tintSolid: '#FFC76A',
levels: [
{
id: 'a1', title: 'Order matters', kind: 'reorder',
collection: 'orders', // shown in the solved-query preview
prompt: 'Total revenue per customer in 2024 — re-order the stages.',
sub: 'Filter early, group, sort, then limit. The ESR of pipelines.',
why: 'Put `$match` first so the planner can use indexes and shrink the dataset before the expensive `$group`. Sort after grouping; limit last.',
stages: [
{ id: 'm', code: '$match: { year: 2024 }', sub: 'filter to 2024 docs', correct: 0 },
{ id: 'g', code: '$group: { _id: "$customer", total: { $sum: "$amount" } }', sub: 'sum amounts per customer', correct: 1 },
{ id: 's', code: '$sort: { total: -1 }', sub: 'highest spenders first', correct: 2 },
{ id: 'l', code: '$limit: 5', sub: 'top 5 only', correct: 3 }
],
// initial scramble
initial: ['s', 'l', 'm', 'g']
},
{
id: 'a2', title: '$project — reshape output', kind: 'fill',
prompt: 'Return just the customer name and order total.',
sub: '$project picks (and renames or computes) fields per doc.',
why: '`1` includes a field, `0` excludes it. `_id` is the only field included by default — disable it explicitly when you don\'t want it.',
snippet: [
'db.orders.aggregate([',
'\n { $project: {',
'\n _id: ', { blank: 'id' }, ',',
'\n customer: ', { blank: 'one' }, ',',
'\n total: ', { blank: 'one2' },
'\n } }',
'\n])'
],
choices: {
id: { options: ['0', '1', 'false', 'null'], answer: '0' },
one: { options: ['1', '0', 'true', '"yes"'], answer: '1' },
one2: { options: ['1', '0', '"$total"', 'yes'], answer: '1' }
}
},
{
id: 'a3', title: '$lookup — join collections', kind: 'reorder',
collection: 'orders', // shown in the solved-query preview
prompt: 'Attach each order\'s customer doc, then keep only the name.',
sub: '$lookup adds an array of matches. $unwind flattens it. $project finishes the shape.',
why: '`$lookup` is your JOIN. It returns an array — even for single matches — so `$unwind` is almost always next.',
stages: [
{ id: 'lo', code: '$lookup: { from: "customers", localField: "customerId", foreignField: "_id", as: "cust" }', sub: 'attach customer docs as array', correct: 0 },
{ id: 'un', code: '$unwind: "$cust"', sub: 'one doc per match', correct: 1 },
{ id: 'pr', code: '$project: { _id: 0, name: "$cust.name" }', sub: 'keep only the name', correct: 2 }
],
initial: ['pr', 'lo', 'un']
},
{
id: 'a4', title: '$sort + $limit', kind: 'blocks',
prompt: 'Show the three most recent posts.',
sub: 'Sort descending by createdAt, then keep three.',
why: 'When `$sort` is followed by `$limit`, the engine streams just enough docs through the sort — much cheaper than sorting the whole collection.',
snippet: [
'db.posts.aggregate([',
'\n { ', { slot: 'sort' }, ': { createdAt: ', { slot: 'dir' }, ' } },',
'\n { ', { slot: 'limit' }, ': ', { slot: 'n' }, ' }',
'\n])'
],
bank: [
{ id: 'sort', label: '$sort', kind: 'stage', answer: 'sort' },
{ id: 'limit', label: '$limit', kind: 'stage', answer: 'limit' },
{ id: 'dir', label: '-1', kind: 'value', answer: 'dir' },
{ id: 'n', label: '3', kind: 'value', answer: 'n' },
/* distractors */
{ id: 'x1', label: '$top', kind: 'stage' },
{ id: 'x2', label: '$first', kind: 'stage' },
{ id: 'x3', label: '1', kind: 'value' },
{ id: 'x4', label: '"3"', kind: 'value' }
]
}
]
},
/* ============================================================ WORLD 4 */
{
id: 'idx',
icon: 'database',
name: 'Indexes & Performance',
tagline: 'Make queries fast',
blurb: 'The ESR rule, covered queries, and TTL indexes.',
tint: 'rgba(94,12,158,0.30)',
tintSolid: '#C390DF',
levels: [
{
id: 'i1', title: 'Classify index choices', kind: 'index',
prompt: 'The `events` collection has four query shapes. Pick the best index strategy for each one.',
sub: 'Classify the full query shape: one equality field, equality+sort fields, Atlas Search text, or no index.',
why: 'Indexes trade write cost for read speed. A compound index must include every field needed by that query shape — for example `{ userId: 1, createdAt: -1 }` supports filtering by userId and sorting by createdAt. In `explain()` output, a good index shows up as `IXSCAN` (index scan); a missing one as `COLLSCAN` (MongoDB reads every document).',
collection: 'events',
fields: [
{ name: 'userId', type: 'ObjectId', need: 'single', used: 'db.events.find({ userId })' },
{ name: 'userId + createdAt', type: 'ObjectId + Date', need: 'compound', used: 'db.events.find({ userId }).sort({ createdAt: -1 })' },
{ name: 'title', type: 'String', need: 'search', used: 'Atlas Search text query on event titles' },
{ name: 'isInternal', type: 'Bool', need: 'none', used: 'low cardinality flag — usually do not index alone' }
],
bank: [
{ id: 'single', label: 'Single-field', kind: 'index' },
{ id: 'compound', label: 'Compound', kind: 'index' },
{ id: 'search', label: 'Atlas Search', kind: 'index' },
{ id: 'none', label: 'No index', kind: 'index' }
]
},
{
id: 'i2', title: 'The ESR rule', kind: 'reorder',
prompt: 'Put the fields of this compound index in ESR order: Equality, Sort, Range.',
sub: 'For: db.orders.find({ status: "paid", amount: { $gt: 50 } }).sort({ createdAt: -1 })',
why: 'ESR: index Equality fields first (smallest sub-tree), then the Sort key (so the index returns rows pre-sorted), then Range (which forces a scan inside the bucket).',
stages: [
{ id: 'e', code: 'status: 1', sub: 'filter: { status: "paid" }', correct: 0 },
{ id: 's', code: 'createdAt: -1', sub: '.sort({ createdAt: -1 })', correct: 1 },
{ id: 'r', code: 'amount: 1', sub: 'filter: { amount: { $gt: 50 } }', correct: 2 }
],
initial: ['r', 's', 'e']
},
{
id: 'i3', title: 'TTL index', kind: 'fill',
prompt: 'Expire session docs 24 hours after `lastSeen`.',
sub: 'Add expireAfterSeconds to a date-typed field index.',
why: 'TTL indexes are checked by a background thread once per minute. Don\'t use them for precise timing — expect drift of up to 60s.',
snippet: [
'db.sessions.', { blank: 'op' }, '(',
'\n { lastSeen: 1 },',
'\n { ', { blank: 'key' }, ': ', { blank: 'sec' }, ' }',
'\n)'
],
choices: {
op: { options: ['createIndex', 'addIndex', 'index', 'ensureIndex'], answer: 'createIndex' },
key: { options: ['expireAfterSeconds', 'ttl', 'expires', 'maxAge'], answer: 'expireAfterSeconds' },
sec: { options: ['86400', '24', '1440', '3600'], answer: '86400' }
}
},
{
id: 'i4', title: 'Covered query', kind: 'fill',
prompt: 'Project only fields that live inside the index { sku: 1, price: 1 }.',
sub: 'When every projected field is in the index, MongoDB skips the document fetch.',
why: 'A *covered* query returns from the index alone — no disk seek. `explain()` reports how MongoDB ran a query; in `"executionStats"` mode it also counts what it read. `totalDocsExamined: 0` means the answer came straight from the index without touching a single document.',
snippet: [
'db.products.find(',
'\n { sku: ', { blank: 'val' }, ' },',
'\n { _id: ', { blank: 'id' }, ', sku: 1, price: 1 }',
'\n).', { blank: 'verb' }, '("executionStats")'
],
choices: {
val: { options: ['"ESPRESSO-01"', 'ESPRESSO-01', '*', '{}'], answer: '"ESPRESSO-01"' },
id: { options: ['0', '1', 'null', 'true'], answer: '0' },
verb: { options: ['explain', 'plan', 'analyze', 'trace'], answer: 'explain' }
}
}
]
},
/* ============================================================ WORLD 5 */
{
id: 'atlas',
icon: 'cloud',
name: 'Atlas Superpowers',
tagline: 'Vector, search, triggers',
blurb: 'The pieces that turn MongoDB into a modern AI data platform.',
tint: 'rgba(0,237,100,0.22)',
tintSolid: '#71F6BA',
levels: [
{
id: 'v1', title: '$vectorSearch', kind: 'fill',
prompt: 'Find the 5 nearest products to a query embedding.',
sub: 'Atlas Vector Search runs k-NN against an HNSW index you created on `embedding`.',
why: 'Vector search powers RAG. Generate the embedding from your LLM, pass it as `queryVector`, and Atlas returns the closest docs by cosine distance.',
snippet: [
'db.products.aggregate([{',
'\n $vectorSearch: {',
'\n index: "embed_idx",',
'\n path: ', { blank: 'path' }, ',',
'\n queryVector: queryEmbedding,',
'\n numCandidates: 150,',
'\n ', { blank: 'limit' }, ': 5',
'\n }',
'\n}])'
],
choices: {
path: { options: ['"embedding"', '"vector"', 'embedding', '*'], answer: '"embedding"' },
limit: { options: ['limit', 'numResults', 'topK', 'k'], answer: 'limit' }
}
},
{
id: 'v2', title: '$search — Atlas Search', kind: 'blocks',
prompt: 'Use Atlas Search to find blog posts mentioning "aggregation pipeline".',
sub: 'Atlas Search runs as a `$search` stage inside `aggregate()`, not as `find({ $text: ... })`.',
why: 'Atlas Search uses a dedicated search index and the `$search` aggregation stage. It supports relevance scoring, analyzers, fuzzy matching, highlighting, autocomplete, and richer production search patterns than legacy `$text`.',
snippet: [
'db.posts.aggregate([',
'\n { ', { slot: 'stage' }, ': {',
'\n ', { slot: 'operator' }, ': {',
'\n path: ', { slot: 'path' }, ',',
'\n query: ', { slot: 'query' },
'\n }',
'\n } }',
'\n])'
],
bank: [
{ id: 'stage', label: '$search', kind: 'stage', answer: 'stage' },
{ id: 'operator', label: 'text', kind: 'op', answer: 'operator' },
{ id: 'path', label: '"body"', kind: 'field', answer: 'path' },
{ id: 'query', label: '"aggregation pipeline"', kind: 'value', answer: 'query' },
/* distractors */
{ id: 'x1', label: '$text', kind: 'op' },
{ id: 'x2', label: 'find', kind: 'op' },
{ id: 'x3', label: 'aggregation pipeline', kind: 'value' }
]
},
{
id: 'v3', title: 'Atlas Trigger', kind: 'fill',
prompt: 'Send a welcome email whenever a user is inserted.',
sub: 'Triggers run server-side on change events. Pick the event type and the collection.',
why: 'Triggers attach to a change stream. Keep handlers small (<60s) and idempotent — they can re-fire.',
snippet: [
'// Atlas Trigger config',
'\n{',
'\n type: "DATABASE",',
'\n database: "app",',
'\n collection: ', { blank: 'col' }, ',',
'\n operationTypes: [', { blank: 'evt' }, '],',
'\n function: ', { blank: 'fn' },
'\n}'
],
choices: {
col: { options: ['"users"', 'users', 'User', '*'], answer: '"users"' },
evt: { options: ['"insert"', '"create"', '"write"', '"new"'], answer: '"insert"' },
fn: { options: ['"sendWelcomeEmail"', 'sendWelcomeEmail', 'fn()', 'send()'], answer: '"sendWelcomeEmail"' }
}
},
{
id: 'v4', title: 'RAG: hybrid retrieval', kind: 'reorder',
collection: 'documents', // shown in the solved-query preview
prompt: 'Pipeline for retrieval-augmented chat: filter by tenant, vector-search, then trim payload.',
sub: '$match early (tenant scope) → $vectorSearch (semantic) → $project (keep only what the LLM needs).',
why: 'Tenant filters belong before semantic search so you only rank documents the user is allowed to see. Always trim the payload — every token costs.',
stages: [
{ id: 'vs', code: '$vectorSearch: { index: "embed", path: "embedding", queryVector: q, limit: 8, filter: { tenantId } }', sub: 'k-NN with tenant prefilter', correct: 0 },
{ id: 'ms', code: '$match: { score: { $gt: 0.78 } }', sub: 'drop weak matches', correct: 1 },
{ id: 'pj', code: '$project: { _id: 0, title: 1, chunk: 1, score: { $meta: "vectorSearchScore" } }', sub: 'trim for the LLM', correct: 2 }
],
initial: ['pj', 'ms', 'vs']
}
]
},
/* ============================================================ WORLD 6 */
{
id: 'streams',
icon: 'sparkle',
name: 'Real-Time Streams',
tagline: 'React as data changes',
blurb: 'Change Streams and Atlas Stream Processing turn operational writes into live workflows.',
tint: 'rgba(113,246,186,0.16)',
tintSolid: '#00ED64',
levels: [
{
id: 's1', title: 'Change Stream — watch inserts', kind: 'blocks',
prompt: 'Watch for new operational events as they are inserted.',
sub: 'Change streams let apps react to writes without polling.',
why: 'Change streams expose MongoDB writes as an ordered event stream. Apps can subscribe once and react to inserts, updates, replaces, and deletes in real time.',
snippet: [
'db.', { slot: 'col' }, '.watch([',
'\n { ', { slot: 'stage' }, ': { operationType: ', { slot: 'evt' }, ' } }',
'\n])'
],
bank: [
{ id: 'col', label: 'events', kind: 'field', answer: 'col' },
{ id: 'stage', label: '$match', kind: 'stage', answer: 'stage' },
{ id: 'evt', label: '"insert"', kind: 'value', answer: 'evt' },
{ id: 'x1', label: 'listen', kind: 'op' },
{ id: 'x2', label: '$project', kind: 'stage' },
{ id: 'x3', label: 'insert', kind: 'value' }
]
},
{
id: 's2', title: 'Change Stream — filter critical events', kind: 'fill',
prompt: 'Only react when the incoming event crosses the critical threshold.',
sub: 'Filter on fullDocument fields so low-priority changes never reach the handler.',
why: 'Filtering inside the change stream keeps downstream functions, queues, and dashboards focused on events that actually require action.',
snippet: [
'db.events.watch([',
'\n { $match: { "fullDocument.priorityScore": { ', { blank: 'op' }, ': ', { blank: 'threshold' }, ' } } }',
'\n])'
],
choices: {
op: { options: ['$gte', '$gt', '$lte', '$eq'], answer: '$gte' },
threshold: { options: ['90', '"90"', '9', 'true'], answer: '90' }
}
},
{
id: 's3', title: 'Atlas Stream Processing — route alerts', kind: 'reorder',
prompt: 'Build a stream processor: read events, filter critical ones, shape alerts, then write them.',
sub: '$source → $match → $project → $merge keeps the stream useful and app-ready.',
why: 'Atlas Stream Processing lets teams continuously transform and route operational events without building a separate stream-processing stack.',
stages: [
{ id: 'src', code: '$source: { connectionName: "atlas", db: "app", coll: "events" }', sub: 'read the live event source', correct: 0 },
{ id: 'mat', code: '$match: { priorityScore: { $gte: 90 } }', sub: 'keep critical events only', correct: 1 },
{ id: 'prj', code: '$project: { _id: 0, eventId: 1, priorityScore: 1, alertType: 1, observedAt: "$$NOW" }', sub: 'shape alert payloads', correct: 2 },
{ id: 'mrg', code: '$merge: { into: { db: "app", coll: "alerts" } }', sub: 'write routed alerts', correct: 3 }
],
initial: ['mrg', 'prj', 'src', 'mat']
},
{
id: 's4', title: 'Stream output — alert collection', kind: 'fill',
prompt: 'Write processed stream alerts into the alerts collection for apps and dashboards.',
sub: '$merge routes processed events back into MongoDB as operational alerts.',
why: 'Writing stream output into MongoDB keeps the real-time alert state queryable by dashboards, APIs, and downstream automation.',
snippet: [
'{ ', { blank: 'stage' }, ': { into: { db: "app", coll: ', { blank: 'coll' }, ' }, whenMatched: ', { blank: 'mode' }, ' } }'
],
choices: {
stage: { options: ['$merge', '$out', '$insert', '$route'], answer: '$merge' },
coll: { options: ['"alerts"', 'alerts', '"events"', '"archive"'], answer: '"alerts"' },
mode: { options: ['"replace"', 'replace', '"drop"', '"ignore"'], answer: '"replace"' }
}
}
]
}
];
/* ============================================================
* Industry pack registry & helpers
* ============================================================ */
window.DBLINGO_INDUSTRIES = {};
function registerDBLingoIndustry(pack) {
window.DBLINGO_INDUSTRIES[pack.id] = pack;
return pack;
}
function createIndustryWorlds(profile) {
const worlds = JSON.parse(JSON.stringify(WORLDS));
const nouns = profile.nouns || {};
const aha = profile.aha || [];
const levelOverrides = profile.levels || {};
worlds.forEach((world, wi) => {
world.levels.forEach((level, li) => {
const idx = wi * 4 + li;
const moment = aha[idx % Math.max(aha.length, 1)] || {
title: 'AHA: ' + profile.name + ' runs on connected data',
message: profile.promise
};
level.aha = moment;
// Apply full level override if provided
if (levelOverrides[level.id]) {
const override = levelOverrides[level.id];
Object.keys(override).forEach(function(key) {
level[key] = override[key];
});
}
});
});
const [docs, query, agg, idxWorld, atlas, streams] = worlds;
const stream = Object.assign({
eventCollection: nouns.itemCollection || nouns.transactionPlural || 'events',
alertCollection: 'alerts',
eventPlural: nouns.transactionPlural || nouns.itemPlural || 'events',
criticalLabel: 'critical events',
scoreField: 'priorityScore',
threshold: 90,
alertType: 'critical_event',
sourceDb: 'app',
handler: 'routeCriticalEvent',
outcome: 'alert dashboards and downstream automation'
}, profile.stream || {});
// Only apply generic text substitutions for levels that were NOT overridden
if (!levelOverrides['d1']) {
docs.levels[0].title = 'Model a ' + (nouns.profile || 'profile') + ' document';
docs.levels[0].prompt = 'Drag the right values into a ' + profile.name + ' ' + (nouns.profile || 'profile') + ' document.';
docs.levels[0].why = profile.name + ' teams move fast when operational data fits the way the business thinks. MongoDB documents keep rich ' + (nouns.profile || 'profile') + ' context together without forcing every new attribute through a rigid migration.';
}
if (!levelOverrides['d2']) {
// Name the exact collection: learners can't guess it from a noun like "catalog".
const d2Col = (docs.levels[1].bank.find(function(b) { return b.answer === 'col'; }) || {}).label || 'products';
docs.levels[1].prompt = 'Insert a new ' + (nouns.item || 'record') + ' into the `' + d2Col + '` collection. Drag the missing pieces.';
docs.levels[1].why = 'MongoDB makes it natural to capture new ' + (nouns.itemPlural || 'records') + ' as JSON-shaped events, products, assets, or cases while preserving room for industry-specific fields.';
}
docs.name = (profile.shortName || profile.name) + ' Documents';
docs.tagline = 'Model the domain';
docs.blurb = profile.promise;
query.name = (profile.shortName || profile.name) + ' Queries';
query.tagline = 'Ask operational questions';
query.blurb = 'Find and filter ' + profile.name + ' data in the shapes applications actually need.';
if (!levelOverrides['q1']) {
query.levels[0].prompt = 'Find every ' + (nouns.user || 'user') + ' whose segment is "priority".';
query.levels[0].snippet = ['db.', { slot: 'field' }, '.find({ segment: ', { slot: 'val' }, ' })'];
query.levels[0].bank = [
{ id: 'field', label: nouns.profileCollection || 'customers', kind: 'field', answer: 'field' },
{ id: 'val', label: '"priority"', kind: 'value', answer: 'val' },
{ id: 'x1', label: nouns.itemCollection || 'events', kind: 'field' },
{ id: 'x2', label: 'priority', kind: 'value' },
{ id: 'x3', label: 'tier', kind: 'field' }
];
query.levels[0].why = 'MongoDB indexes make high-volume ' + profile.name + ' lookups fast while keeping each ' + (nouns.profile || 'profile') + ' flexible enough for changing business signals.';
}
if (!levelOverrides['q2']) {
query.levels[1].prompt = 'Find ' + (nouns.transactionPlural || 'events') + ' over the important threshold. Drag the operator and value.';
query.levels[1].why = 'Aggregation lets ' + profile.name + ' teams filter early, transform in place, and feed dashboards, apps, and AI workflows from the same operational data.';
}
agg.name = (profile.shortName || profile.name) + ' Pipelines';
agg.tagline = 'Transform in place';
agg.blurb = 'Turn ' + profile.name + ' events into ranked, joined, and shaped outcomes.';
if (!levelOverrides['a1']) {
agg.levels[0].prompt = profile.name + ': rank ' + (nouns.profilePlural || 'customers') + ' by 2024 value — re-order the stages.';
agg.levels[0].why = 'The aggregation pipeline is MongoDB\'s in-database transformation engine: filter, group, rank, and reshape ' + profile.name + ' data without exporting it to a separate ETL tier.';
}
if (!levelOverrides['a3']) {
agg.levels[2].prompt = 'Attach each ' + (nouns.transaction || 'event') + ' to its ' + (nouns.profile || 'profile') + ' record, then keep only the fields the app needs.';
agg.levels[2].why = 'MongoDB supports embedding when data is accessed together and $lookup when independent entities need to meet at query time — a practical fit for ' + profile.name + '.';
}
idxWorld.name = (profile.shortName || profile.name) + ' Performance';
idxWorld.tagline = 'Make reads fast';
idxWorld.blurb = 'Index the ' + profile.name + ' access patterns that matter.';
if (!levelOverrides['i1']) {
idxWorld.levels[0].prompt = 'Classify the best index strategy for common ' + profile.name + ' query shapes.';
idxWorld.levels[0].why = 'MongoDB performance tuning maps directly to how ' + profile.name + ' applications read data: equality lookups, compound filter+sort patterns, Atlas Search, and low-cardinality fields that should not be indexed alone.';
}
if (!levelOverrides['i2']) {
idxWorld.levels[1].prompt = 'Put this ' + profile.name + ' compound index in ESR order: Equality, Sort, Range.';
}
atlas.name = (profile.shortName || profile.name) + ' Atlas AI';
atlas.tagline = 'Search, vectors, triggers';
atlas.blurb = 'Use Atlas Search and Vector Search to build smarter ' + profile.name + ' experiences.';
if (!levelOverrides['v1']) {
atlas.levels[0].prompt = 'Find the 5 nearest ' + (nouns.itemPlural || 'records') + ' to an embedding.';
atlas.levels[0].why = 'Vector Search lets ' + profile.name + ' teams build semantic search and RAG over operational data already living in MongoDB Atlas.';
}
if (!levelOverrides['v2']) {
atlas.levels[1].prompt = 'Use Atlas Search to find ' + (nouns.documentPlural || 'documents') + ' mentioning "' + (profile.searchPhrase || 'aggregation pipeline') + '".';
atlas.levels[1].bank = atlas.levels[1].bank.map(function(b) { return b.id === 'query' ? Object.assign({}, b, { label: '"' + (profile.searchPhrase || 'aggregation pipeline') + '"' }) : b; });
atlas.levels[1].why = 'Atlas Search gives ' + profile.name + ' applications relevance scoring, analyzers, fuzzy matching, autocomplete, and highlighting without bolting on a separate search cluster.';
}
if (!levelOverrides['v3']) {
atlas.levels[2] = atlas.levels[2]; // no change needed
}
if (!levelOverrides['v4']) {
atlas.levels[3].prompt = profile.name + ' RAG pipeline: scope by tenant or domain, retrieve semantically, then trim payload.';
atlas.levels[3].why = 'MongoDB Atlas brings operational data, search, vector search, and app services together so ' + profile.name + ' teams can build AI features with fewer moving parts.';
}
streams.name = (profile.shortName || profile.name) + ' Streams';
streams.tagline = 'React in real time';
streams.blurb = 'Use Change Streams and Atlas Stream Processing to turn ' + stream.eventPlural + ' into live ' + stream.outcome + '.';
if (!levelOverrides['s1']) {
streams.levels[0].prompt = 'Watch for new ' + stream.eventPlural + ' as they are inserted.';
streams.levels[0].sub = 'Change streams let ' + profile.name + ' apps react to writes without polling.';
streams.levels[0].why = 'In ' + profile.name + ', teams need to react the moment important operational data lands. Change streams expose inserts, updates, replaces, and deletes as an ordered event stream.';
streams.levels[0].snippet = [
'db.', { slot: 'col' }, '.watch([',
'\n { ', { slot: 'stage' }, ': { operationType: ', { slot: 'evt' }, ' } }',
'\n])'
];
streams.levels[0].bank = [
{ id: 'col', label: stream.eventCollection, kind: 'field', answer: 'col' },
{ id: 'stage', label: '$match', kind: 'stage', answer: 'stage' },
{ id: 'evt', label: '"insert"', kind: 'value', answer: 'evt' },
{ id: 'x1', label: 'listen', kind: 'op' },
{ id: 'x2', label: '$project', kind: 'stage' },
{ id: 'x3', label: 'insert', kind: 'value' }
];
}
if (!levelOverrides['s2']) {
streams.levels[1].prompt = 'Only react when incoming ' + stream.eventPlural + ' become ' + stream.criticalLabel + '.';
streams.levels[1].snippet = [
'db.' + stream.eventCollection + '.watch([',
'\n { $match: { "fullDocument.' + stream.scoreField + '": { ', { blank: 'op' }, ': ', { blank: 'threshold' }, ' } } }',
'\n])'
];
streams.levels[1].choices.threshold = { options: [String(stream.threshold), '"' + stream.threshold + '"', String(Math.max(1, Math.floor(stream.threshold / 10))), 'true'], answer: String(stream.threshold) };
streams.levels[1].why = 'Filtering on `fullDocument.' + stream.scoreField + '` inside the change stream keeps ' + profile.name + ' handlers focused on ' + stream.criticalLabel + ' instead of processing every write.';
}
if (!levelOverrides['s3']) {
streams.levels[2].prompt = 'Build a stream processor: read ' + stream.eventPlural + ', filter ' + stream.criticalLabel + ', shape alerts, then write them.';
streams.levels[2].stages = [
{ id: 'src', code: '$source: { connectionName: "atlas", db: "' + stream.sourceDb + '", coll: "' + stream.eventCollection + '" }', sub: 'read the live ' + stream.eventCollection + ' source', correct: 0 },
{ id: 'mat', code: '$match: { ' + stream.scoreField + ': { $gte: ' + stream.threshold + ' } }', sub: 'keep ' + stream.criticalLabel + ' only', correct: 1 },
{ id: 'prj', code: '$project: { _id: 0, eventId: 1, ' + stream.scoreField + ': 1, alertType: "' + stream.alertType + '", observedAt: "$$NOW" }', sub: 'shape alert payloads', correct: 2 },
{ id: 'mrg', code: '$merge: { into: { db: "' + stream.sourceDb + '", coll: "' + stream.alertCollection + '" } }', sub: 'write routed alerts', correct: 3 }
];
streams.levels[2].why = 'Atlas Stream Processing continuously transforms ' + profile.name + ' events into operational outputs without a separate stream-processing tier.';
}
if (!levelOverrides['s4']) {
streams.levels[3].prompt = 'Write processed stream alerts into the ' + stream.alertCollection + ' collection for ' + stream.outcome + '.';
streams.levels[3].snippet = [
'{ ', { blank: 'stage' }, ': { into: { db: "' + stream.sourceDb + '", coll: ', { blank: 'coll' }, ' }, whenMatched: ', { blank: 'mode' }, ' } }'
];
streams.levels[3].choices.coll = { options: ['"' + stream.alertCollection + '"', stream.alertCollection, '"' + stream.eventCollection + '"', '"archive"'], answer: '"' + stream.alertCollection + '"' };
streams.levels[3].why = 'Routing processed events into `' + stream.alertCollection + '` keeps ' + profile.name + ' real-time state queryable by dashboards, APIs, and downstream automation.';
}
return worlds;
}
function createDBLingoIndustryPack(profile) {
return {
id: profile.id,
name: profile.name,
shortName: profile.shortName || profile.name,
description: profile.description || profile.promise,
promise: profile.promise,
hints: Object.assign({}, HINTS, profile.hints || {}),
worlds: createIndustryWorlds(profile)
};
}
/* Register the general/default pack */
registerDBLingoIndustry({
id: 'general',
name: 'General MongoDB Foundations',
shortName: 'General',
description: 'A broad MongoDB learning path from documents to Atlas Search and Vector Search.',
promise: 'MongoDB fits modern applications because documents, indexes, aggregation, search, and vectors work together in one developer data platform.',
hints: HINTS,
worlds: WORLDS
});
window.WORLDS = WORLDS;
window.HINTS = HINTS;
window.LEVEL_HINTS = LEVEL_HINTS;
window.LEVEL_DOCS = LEVEL_DOCS;
window.WORLD_SKILL_BADGES = WORLD_SKILL_BADGES;