@flowerforce/flowerbase 1.7.5-beta.5 → 1.7.5

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.
@@ -1,5 +1,4 @@
1
1
  import { FastifyInstance } from 'fastify'
2
- import { ObjectId } from 'mongodb'
3
2
  import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
4
3
  import { StateManager } from '../../../state'
5
4
  import { GenerateContext } from '../../../utils/context'
@@ -31,10 +30,7 @@ export async function customFunctionController(app: FastifyInstance) {
31
30
  app.post<LoginDto>(
32
31
  AUTH_ENDPOINTS.LOGIN,
33
32
  {
34
- schema: LOGIN_SCHEMA,
35
- errorHandler: (_error, _request, reply) => {
36
- reply.code(500).send({ message: 'Internal Server Error' })
37
- }
33
+ schema: LOGIN_SCHEMA
38
34
  },
39
35
  async function (req, reply) {
40
36
  const customFunctionProvider = AUTH_CONFIG.authProviders?.['custom-function']
@@ -58,7 +54,7 @@ export async function customFunctionController(app: FastifyInstance) {
58
54
  id
59
55
  } = req
60
56
 
61
- type CustomFunctionAuthResult = { id?: string; email?: string }
57
+ type CustomFunctionAuthResult = { id?: string }
62
58
  const authResult = await GenerateContext({
63
59
  args: [
64
60
  req.body
@@ -81,35 +77,16 @@ export async function customFunctionController(app: FastifyInstance) {
81
77
  }
82
78
  }) as CustomFunctionAuthResult
83
79
 
80
+
84
81
  if (!authResult.id) {
85
82
  reply.code(401).send({ message: 'Unauthorized' })
86
83
  return
87
84
  }
88
85
 
89
- const email = authResult?.email || authResult.id
90
- let authUser = await db.collection(authCollection!).findOne({ email })
91
-
86
+ const authUser = await db.collection(authCollection!).findOne({ email: authResult.id })
92
87
  if (!authUser) {
93
- const authUserId = new ObjectId()
94
- await db.collection(authCollection!).insertOne({
95
- _id: authUserId,
96
- email,
97
- status: 'confirmed',
98
- createdAt: new Date(),
99
- custom_data: {},
100
- identities: [
101
- {
102
- id: authResult.id.toString(),
103
- provider_id: authResult.id.toString(),
104
- provider_type: 'custom-function',
105
- provider_data: { email }
106
- }
107
- ]
108
- })
109
- authUser = {
110
- _id: authUserId,
111
- email
112
- }
88
+ reply.code(401).send({ message: 'Unauthorized' })
89
+ return
113
90
  }
114
91
 
115
92
  const user =
@@ -130,7 +107,6 @@ export async function customFunctionController(app: FastifyInstance) {
130
107
  ...(user || {})
131
108
  }
132
109
  }
133
-
134
110
  const refreshToken = this.createRefreshToken(currentUserData)
135
111
  const refreshTokenHash = hashToken(refreshToken)
136
112
  await db.collection(refreshTokensCollection).insertOne({
@@ -140,15 +116,12 @@ export async function customFunctionController(app: FastifyInstance) {
140
116
  expiresAt: new Date(Date.now() + refreshTokenTtlMs),
141
117
  revokedAt: null
142
118
  })
143
- const accessToken = this.createAccessToken(currentUserData)
144
-
145
- const responsePayload = {
146
- access_token: accessToken,
119
+ return {
120
+ access_token: this.createAccessToken(currentUserData),
147
121
  refresh_token: refreshToken,
148
122
  device_id: '',
149
123
  user_id: authUser._id.toString()
150
124
  }
151
- reply.code(200).send(responsePayload)
152
125
  }
153
126
  )
154
127
 
@@ -277,17 +277,6 @@ const handleAuthenticationTrigger = async ({
277
277
  changeStream.on('error', (error) => {
278
278
  if (shouldIgnoreStreamError(error)) return
279
279
  console.error('Authentication trigger change stream error', error)
280
- emitTriggerEvent({
281
- status: 'error',
282
- triggerName,
283
- triggerType,
284
- functionName,
285
- meta: {
286
- ...baseMeta,
287
- event: 'CHANGE_STREAM'
288
- },
289
- error
290
- })
291
280
  })
292
281
  changeStream.on('change', async function (change) {
293
282
  const operationType = change['operationType' as keyof typeof change] as
@@ -376,6 +365,13 @@ const handleAuthenticationTrigger = async ({
376
365
  updateDescription
377
366
  }
378
367
  try {
368
+ emitTriggerEvent({
369
+ status: 'fired',
370
+ triggerName,
371
+ triggerType,
372
+ functionName,
373
+ meta: { ...baseMeta, event: 'LOGOUT' }
374
+ })
379
375
  await GenerateContext({
380
376
  args: [{ user: userData, ...op }],
381
377
  app,
@@ -387,13 +383,6 @@ const handleAuthenticationTrigger = async ({
387
383
  services,
388
384
  runAsSystem: true
389
385
  })
390
- emitTriggerEvent({
391
- status: 'fired',
392
- triggerName,
393
- triggerType,
394
- functionName,
395
- meta: { ...baseMeta, event: 'LOGOUT' }
396
- })
397
386
  } catch (error) {
398
387
  emitTriggerEvent({
399
388
  status: 'error',
@@ -428,6 +417,13 @@ const handleAuthenticationTrigger = async ({
428
417
  updateDescription
429
418
  }
430
419
  try {
420
+ emitTriggerEvent({
421
+ status: 'fired',
422
+ triggerName,
423
+ triggerType,
424
+ functionName,
425
+ meta: { ...baseMeta, event: 'DELETE' }
426
+ })
431
427
  await GenerateContext({
432
428
  args: isAutoTrigger ? [userData] : [{ user: userData, ...op }],
433
429
  app,
@@ -439,13 +435,6 @@ const handleAuthenticationTrigger = async ({
439
435
  services,
440
436
  runAsSystem: true
441
437
  })
442
- emitTriggerEvent({
443
- status: 'fired',
444
- triggerName,
445
- triggerType,
446
- functionName,
447
- meta: { ...baseMeta, event: 'DELETE' }
448
- })
449
438
  } catch (error) {
450
439
  emitTriggerEvent({
451
440
  status: 'error',
@@ -482,6 +471,13 @@ const handleAuthenticationTrigger = async ({
482
471
  updateDescription
483
472
  }
484
473
  try {
474
+ emitTriggerEvent({
475
+ status: 'fired',
476
+ triggerName,
477
+ triggerType,
478
+ functionName,
479
+ meta: { ...baseMeta, event: 'UPDATE' }
480
+ })
485
481
  await GenerateContext({
486
482
  args: isAutoTrigger ? [userData] : [{ user: userData, ...op }],
487
483
  app,
@@ -493,13 +489,6 @@ const handleAuthenticationTrigger = async ({
493
489
  services,
494
490
  runAsSystem: true
495
491
  })
496
- emitTriggerEvent({
497
- status: 'fired',
498
- triggerName,
499
- triggerType,
500
- functionName,
501
- meta: { ...baseMeta, event: 'UPDATE' }
502
- })
503
492
  } catch (error) {
504
493
  emitTriggerEvent({
505
494
  status: 'error',
@@ -586,6 +575,13 @@ const handleAuthenticationTrigger = async ({
586
575
  }
587
576
 
588
577
  try {
578
+ emitTriggerEvent({
579
+ status: 'fired',
580
+ triggerName,
581
+ triggerType,
582
+ functionName,
583
+ meta: { ...baseMeta, event: 'CREATE' }
584
+ })
589
585
  await GenerateContext({
590
586
  args: isAutoTrigger ? [userData] : [{ user: userData, ...op }],
591
587
  app,
@@ -597,13 +593,6 @@ const handleAuthenticationTrigger = async ({
597
593
  services,
598
594
  runAsSystem: true
599
595
  })
600
- emitTriggerEvent({
601
- status: 'fired',
602
- triggerName,
603
- triggerType,
604
- functionName,
605
- meta: { ...baseMeta, event: 'CREATE' }
606
- })
607
596
  } catch (error) {
608
597
  emitTriggerEvent({
609
598
  status: 'error',
@@ -0,0 +1,163 @@
1
+ import Fastify, { FastifyInstance } from 'fastify'
2
+ import { registerCollectionRoutes } from '../collections'
3
+ import { StateManager } from '../../../state'
4
+
5
+ jest.mock('../../../state', () => ({
6
+ StateManager: {
7
+ select: jest.fn()
8
+ }
9
+ }))
10
+
11
+ describe('monitoring collections routes', () => {
12
+ let app: FastifyInstance
13
+ let addCollectionHistory: jest.Mock
14
+ let selectMock: jest.Mock
15
+ let insertOne: jest.Mock
16
+ let insertMany: jest.Mock
17
+
18
+ beforeEach(async () => {
19
+ app = Fastify()
20
+ addCollectionHistory = jest.fn()
21
+ selectMock = StateManager.select as unknown as jest.Mock
22
+ insertOne = jest.fn()
23
+ insertMany = jest.fn()
24
+
25
+ const services = {
26
+ 'mongodb-atlas': jest.fn(() => ({
27
+ db: jest.fn(() => ({
28
+ collection: jest.fn(() => ({
29
+ insertOne,
30
+ insertMany
31
+ }))
32
+ }))
33
+ }))
34
+ }
35
+
36
+ selectMock.mockImplementation((key: string) => {
37
+ if (key === 'rules') return {}
38
+ if (key === 'services') return services
39
+ return {}
40
+ })
41
+
42
+ registerCollectionRoutes(app, {
43
+ prefix: '/monit',
44
+ collectionHistory: [],
45
+ maxCollectionHistory: 20,
46
+ addCollectionHistory
47
+ })
48
+ await app.ready()
49
+ })
50
+
51
+ afterEach(async () => {
52
+ await app.close()
53
+ jest.clearAllMocks()
54
+ })
55
+
56
+ it('POST /collections/insert should insert one document', async () => {
57
+ insertOne.mockResolvedValue({
58
+ acknowledged: true,
59
+ insertedId: 'id-1'
60
+ })
61
+
62
+ const response = await app.inject({
63
+ method: 'POST',
64
+ url: '/monit/api/collections/insert',
65
+ payload: {
66
+ collection: 'todos',
67
+ mode: 'insertOne',
68
+ document: { title: 'Task 1' },
69
+ runAsSystem: true
70
+ }
71
+ })
72
+
73
+ expect(response.statusCode).toBe(200)
74
+ expect(JSON.parse(response.body)).toEqual({
75
+ mode: 'insertOne',
76
+ acknowledged: true,
77
+ insertedId: 'id-1',
78
+ count: 1
79
+ })
80
+ expect(insertOne).toHaveBeenCalledWith({ title: 'Task 1' })
81
+ expect(addCollectionHistory).toHaveBeenCalledWith(expect.objectContaining({
82
+ collection: 'todos',
83
+ mode: 'insertOne',
84
+ document: { title: 'Task 1' },
85
+ runAsSystem: true,
86
+ page: 1
87
+ }))
88
+ })
89
+
90
+ it('POST /collections/insert should insert many documents', async () => {
91
+ insertMany.mockResolvedValue({
92
+ acknowledged: true,
93
+ insertedIds: {
94
+ 0: 'id-1',
95
+ 1: 'id-2'
96
+ }
97
+ })
98
+
99
+ const response = await app.inject({
100
+ method: 'POST',
101
+ url: '/monit/api/collections/insert',
102
+ payload: {
103
+ collection: 'todos',
104
+ mode: 'insertMany',
105
+ documents: [{ title: 'Task 1' }, { title: 'Task 2' }],
106
+ runAsSystem: false
107
+ }
108
+ })
109
+
110
+ expect(response.statusCode).toBe(200)
111
+ expect(JSON.parse(response.body)).toEqual({
112
+ mode: 'insertMany',
113
+ acknowledged: true,
114
+ insertedCount: 2,
115
+ insertedIds: ['id-1', 'id-2'],
116
+ count: 2
117
+ })
118
+ expect(insertMany).toHaveBeenCalledWith([{ title: 'Task 1' }, { title: 'Task 2' }])
119
+ expect(addCollectionHistory).toHaveBeenCalledWith(expect.objectContaining({
120
+ collection: 'todos',
121
+ mode: 'insertMany',
122
+ documents: [{ title: 'Task 1' }, { title: 'Task 2' }],
123
+ runAsSystem: false,
124
+ page: 1
125
+ }))
126
+ })
127
+
128
+ it('POST /collections/insert should validate insertOne payload', async () => {
129
+ const response = await app.inject({
130
+ method: 'POST',
131
+ url: '/monit/api/collections/insert',
132
+ payload: {
133
+ collection: 'todos',
134
+ mode: 'insertOne',
135
+ document: ['invalid']
136
+ }
137
+ })
138
+
139
+ expect(response.statusCode).toBe(400)
140
+ expect(JSON.parse(response.body)).toEqual({
141
+ error: 'Document must be an object'
142
+ })
143
+ expect(insertOne).not.toHaveBeenCalled()
144
+ })
145
+
146
+ it('POST /collections/insert should validate insertMany payload', async () => {
147
+ const response = await app.inject({
148
+ method: 'POST',
149
+ url: '/monit/api/collections/insert',
150
+ payload: {
151
+ collection: 'todos',
152
+ mode: 'insertMany',
153
+ documents: [{ title: 'Task 1' }, 'invalid']
154
+ }
155
+ })
156
+
157
+ expect(response.statusCode).toBe(400)
158
+ expect(JSON.parse(response.body)).toEqual({
159
+ error: 'Every document must be an object'
160
+ })
161
+ expect(insertMany).not.toHaveBeenCalled()
162
+ })
163
+ })
@@ -215,4 +215,103 @@ export const registerCollectionRoutes = (app: FastifyInstance, deps: CollectionR
215
215
  return { error: details.message, stack: details.stack }
216
216
  }
217
217
  })
218
+
219
+ app.post(`${prefix}/api/collections/insert`, async (req, reply) => {
220
+ const body = req.body as {
221
+ collection?: string
222
+ mode?: 'insertOne' | 'insertMany'
223
+ document?: unknown
224
+ documents?: unknown
225
+ recordHistory?: boolean
226
+ runAsSystem?: boolean
227
+ userId?: string
228
+ }
229
+ const collection = body?.collection
230
+ if (!collection) {
231
+ reply.code(400)
232
+ return { error: 'Missing collection name' }
233
+ }
234
+ const mode = body?.mode === 'insertMany' ? 'insertMany' : 'insertOne'
235
+ if (mode === 'insertOne') {
236
+ if (!isPlainObject(body?.document)) {
237
+ reply.code(400)
238
+ return { error: 'Document must be an object' }
239
+ }
240
+ } else {
241
+ if (!Array.isArray(body?.documents) || !body.documents.length) {
242
+ reply.code(400)
243
+ return { error: 'Documents must be a non-empty array' }
244
+ }
245
+ const invalid = body.documents.some((entry) => !isPlainObject(entry))
246
+ if (invalid) {
247
+ reply.code(400)
248
+ return { error: 'Every document must be an object' }
249
+ }
250
+ }
251
+
252
+ const rules = StateManager.select('rules') as Rules
253
+ const services = StateManager.select('services') as typeof coreServices
254
+ const resolvedUser = await resolveUserContext(app, body?.userId)
255
+ const runAsSystem = body?.runAsSystem !== false
256
+ const recordHistory = body?.recordHistory !== false
257
+
258
+ try {
259
+ const mongoService = services['mongodb-atlas'](app, {
260
+ rules,
261
+ user: resolvedUser ?? {},
262
+ run_as_system: runAsSystem
263
+ })
264
+ const collectionService = mongoService.db(DB_NAME).collection(collection)
265
+ if (mode === 'insertMany') {
266
+ const documents = body.documents as Record<string, unknown>[]
267
+ const result = await collectionService.insertMany(documents)
268
+ const insertedIds = Array.isArray(result?.insertedIds)
269
+ ? result.insertedIds
270
+ : Object.values(result?.insertedIds || {})
271
+ if (recordHistory) {
272
+ addCollectionHistory({
273
+ ts: Date.now(),
274
+ collection,
275
+ mode: 'insertMany',
276
+ documents: sanitize(documents),
277
+ runAsSystem,
278
+ user: getUserInfo(resolvedUser as Record<string, unknown> | undefined),
279
+ page: 1
280
+ })
281
+ }
282
+ return {
283
+ mode: 'insertMany',
284
+ acknowledged: !!result?.acknowledged,
285
+ insertedCount: insertedIds.length,
286
+ insertedIds: sanitize(insertedIds),
287
+ count: insertedIds.length
288
+ }
289
+ }
290
+
291
+ const document = body.document as Record<string, unknown>
292
+ const result = await collectionService.insertOne(document)
293
+ const insertedId = result?.insertedId
294
+ if (recordHistory) {
295
+ addCollectionHistory({
296
+ ts: Date.now(),
297
+ collection,
298
+ mode: 'insertOne',
299
+ document: sanitize(document),
300
+ runAsSystem,
301
+ user: getUserInfo(resolvedUser as Record<string, unknown> | undefined),
302
+ page: 1
303
+ })
304
+ }
305
+ return {
306
+ mode: 'insertOne',
307
+ acknowledged: !!result?.acknowledged,
308
+ insertedId: sanitize(insertedId),
309
+ count: insertedId ? 1 : 0
310
+ }
311
+ } catch (error) {
312
+ const details = getErrorDetails(error)
313
+ reply.code(500)
314
+ return { error: details.message, stack: details.stack }
315
+ }
316
+ })
218
317
  }
@@ -42,6 +42,10 @@
42
42
  dom.collectionQueryHighlight = document.getElementById('collectionQueryHighlight');
43
43
  dom.collectionAggregate = document.getElementById('collectionAggregate');
44
44
  dom.collectionAggregateHighlight = document.getElementById('collectionAggregateHighlight');
45
+ dom.collectionInsertOne = document.getElementById('collectionInsertOne');
46
+ dom.collectionInsertOneHighlight = document.getElementById('collectionInsertOneHighlight');
47
+ dom.collectionInsertMany = document.getElementById('collectionInsertMany');
48
+ dom.collectionInsertManyHighlight = document.getElementById('collectionInsertManyHighlight');
45
49
  dom.runCollectionQuery = document.getElementById('runCollectionQuery');
46
50
  dom.collectionResult = document.getElementById('collectionResult');
47
51
  dom.collectionPrev = document.getElementById('collectionPrev');
@@ -71,6 +75,10 @@
71
75
  collectionQueryHighlight,
72
76
  collectionAggregate,
73
77
  collectionAggregateHighlight,
78
+ collectionInsertOne,
79
+ collectionInsertOneHighlight,
80
+ collectionInsertMany,
81
+ collectionInsertManyHighlight,
74
82
  runCollectionQuery,
75
83
  collectionResult,
76
84
  collectionPrev,
@@ -237,6 +245,22 @@
237
245
  collectionAggregateHighlight.scrollLeft = collectionAggregate.scrollLeft;
238
246
  };
239
247
 
248
+ const updateCollectionInsertOneHighlight = () => {
249
+ if (!collectionInsertOne || !collectionInsertOneHighlight) return;
250
+ const text = collectionInsertOne.value || '';
251
+ collectionInsertOneHighlight.innerHTML = highlightJson(text || '');
252
+ collectionInsertOneHighlight.scrollTop = collectionInsertOne.scrollTop;
253
+ collectionInsertOneHighlight.scrollLeft = collectionInsertOne.scrollLeft;
254
+ };
255
+
256
+ const updateCollectionInsertManyHighlight = () => {
257
+ if (!collectionInsertMany || !collectionInsertManyHighlight) return;
258
+ const text = collectionInsertMany.value || '';
259
+ collectionInsertManyHighlight.innerHTML = highlightJson(text || '');
260
+ collectionInsertManyHighlight.scrollTop = collectionInsertMany.scrollTop;
261
+ collectionInsertManyHighlight.scrollLeft = collectionInsertMany.scrollLeft;
262
+ };
263
+
240
264
  const formatCellValue = (value) => {
241
265
  if (value === null || value === undefined) return '';
242
266
  if (typeof value === 'string') {
@@ -483,6 +507,14 @@
483
507
  collectionAggregate.value = entry.pipeline ? JSON.stringify(entry.pipeline, null, 2) : '';
484
508
  updateCollectionAggregateHighlight();
485
509
  }
510
+ if (collectionInsertOne) {
511
+ collectionInsertOne.value = entry.document ? JSON.stringify(entry.document, null, 2) : '';
512
+ updateCollectionInsertOneHighlight();
513
+ }
514
+ if (collectionInsertMany) {
515
+ collectionInsertMany.value = entry.documents ? JSON.stringify(entry.documents, null, 2) : '';
516
+ updateCollectionInsertManyHighlight();
517
+ }
486
518
  if (entry.user && entry.user.id) {
487
519
  if (collectionUserInput) collectionUserInput.value = entry.user.id;
488
520
  setSelectedCollectionUser({
@@ -549,14 +581,16 @@
549
581
  setCollectionResult('Select a collection first', false);
550
582
  return;
551
583
  }
552
- const keepPage = options && options.keepPage;
584
+ const mode = collectionMode ? collectionMode.value : 'query';
585
+ const supportsPaging = mode === 'query' || mode === 'aggregate';
586
+ const keepPage = supportsPaging && options && options.keepPage;
553
587
  const recordHistory = !keepPage;
554
588
  if (!keepPage) {
555
589
  state.collectionPage = 1;
556
590
  }
557
- const shouldRefreshTotals = !keepPage || !state.collectionTotal;
591
+ const shouldRefreshTotals = supportsPaging && (!keepPage || !state.collectionTotal);
558
592
  state.collectionHasMore = false;
559
- if (shouldRefreshTotals) {
593
+ if (shouldRefreshTotals || !supportsPaging) {
560
594
  state.collectionTotal = 0;
561
595
  }
562
596
  state.collectionLoading = true;
@@ -568,11 +602,10 @@
568
602
  const userId = selectedUser
569
603
  ? String(selectedUser.id || (selectedUser.auth && selectedUser.auth._id) || '')
570
604
  : fallbackUserId;
571
- const mode = collectionMode ? collectionMode.value : 'query';
572
605
  try {
573
- const sortRaw = collectionSort ? collectionSort.value.trim() : '';
574
- const sort = parseJsonObject(sortRaw, 'Sort');
575
606
  if (mode === 'aggregate') {
607
+ const sortRaw = collectionSort ? collectionSort.value.trim() : '';
608
+ const sort = parseJsonObject(sortRaw, 'Sort');
576
609
  const raw = collectionAggregate ? collectionAggregate.value.trim() : '';
577
610
  const pipeline = raw ? JSON.parse(raw) : [];
578
611
  if (!Array.isArray(pipeline)) {
@@ -607,7 +640,58 @@
607
640
  updateCollectionPager();
608
641
  setCollectionTab('query');
609
642
  setCollectionResult(data, true);
643
+ } else if (mode === 'insertOne') {
644
+ const raw = collectionInsertOne ? collectionInsertOne.value.trim() : '';
645
+ const document = parseJsonObject(raw, 'Document');
646
+ if (!document) {
647
+ throw new Error('Document is required for insert one');
648
+ }
649
+ const data = await api('/collections/insert', {
650
+ method: 'POST',
651
+ body: JSON.stringify({
652
+ collection: name,
653
+ mode,
654
+ document,
655
+ runAsSystem,
656
+ userId: userId || undefined,
657
+ recordHistory
658
+ })
659
+ });
660
+ state.collectionHasMore = false;
661
+ state.collectionPage = 1;
662
+ state.collectionTotal = typeof data.count === 'number' ? data.count : 1;
663
+ updateCollectionPager();
664
+ setCollectionTab('query');
665
+ setCollectionResult(data, true);
666
+ } else if (mode === 'insertMany') {
667
+ const raw = collectionInsertMany ? collectionInsertMany.value.trim() : '';
668
+ const documents = raw ? JSON.parse(raw) : [];
669
+ if (!Array.isArray(documents)) {
670
+ throw new Error('Documents must be a JSON array for insert many');
671
+ }
672
+ if (!documents.length) {
673
+ throw new Error('Documents are required for insert many');
674
+ }
675
+ const data = await api('/collections/insert', {
676
+ method: 'POST',
677
+ body: JSON.stringify({
678
+ collection: name,
679
+ mode,
680
+ documents,
681
+ runAsSystem,
682
+ userId: userId || undefined,
683
+ recordHistory
684
+ })
685
+ });
686
+ state.collectionHasMore = false;
687
+ state.collectionPage = 1;
688
+ state.collectionTotal = typeof data.count === 'number' ? data.count : documents.length;
689
+ updateCollectionPager();
690
+ setCollectionTab('query');
691
+ setCollectionResult(data, true);
610
692
  } else {
693
+ const sortRaw = collectionSort ? collectionSort.value.trim() : '';
694
+ const sort = parseJsonObject(sortRaw, 'Sort');
611
695
  const raw = collectionQuery ? collectionQuery.value.trim() : '';
612
696
  const query = parseJsonObject(raw, 'Query') || {};
613
697
  const data = await api('/collections/query', {
@@ -806,6 +890,14 @@
806
890
  collectionAggregate.addEventListener('input', updateCollectionAggregateHighlight);
807
891
  collectionAggregate.addEventListener('scroll', updateCollectionAggregateHighlight);
808
892
  }
893
+ if (collectionInsertOne) {
894
+ collectionInsertOne.addEventListener('input', updateCollectionInsertOneHighlight);
895
+ collectionInsertOne.addEventListener('scroll', updateCollectionInsertOneHighlight);
896
+ }
897
+ if (collectionInsertMany) {
898
+ collectionInsertMany.addEventListener('input', updateCollectionInsertManyHighlight);
899
+ collectionInsertMany.addEventListener('scroll', updateCollectionInsertManyHighlight);
900
+ }
809
901
 
810
902
  if (collectionList) {
811
903
  collectionList.addEventListener('click', (event) => {
@@ -916,6 +1008,8 @@
916
1008
  setCollectionResultView('json');
917
1009
  updateCollectionQueryHighlight();
918
1010
  updateCollectionAggregateHighlight();
1011
+ updateCollectionInsertOneHighlight();
1012
+ updateCollectionInsertManyHighlight();
919
1013
  setRulesTabEnabled(false);
920
1014
  };
921
1015
 
@@ -929,12 +929,17 @@ button.small {
929
929
  align-items: stretch;
930
930
  }
931
931
 
932
- .collection-io[data-mode="query"] [data-collection-mode="aggregate"] {
932
+ .collection-io [data-collection-mode] {
933
933
  display: none;
934
+ min-height: 0;
934
935
  }
935
936
 
936
- .collection-io[data-mode="aggregate"] [data-collection-mode="query"] {
937
- display: none;
937
+ .collection-io[data-mode="query"] [data-collection-mode="query"],
938
+ .collection-io[data-mode="aggregate"] [data-collection-mode="aggregate"],
939
+ .collection-io[data-mode="insertOne"] [data-collection-mode="insertOne"],
940
+ .collection-io[data-mode="insertMany"] [data-collection-mode="insertMany"] {
941
+ display: flex;
942
+ flex-direction: column;
938
943
  }
939
944
 
940
945
  .collection-io {
@@ -1039,6 +1044,7 @@ button.small {
1039
1044
  border-right: 1px solid #2b2b2b;
1040
1045
  user-select: none;
1041
1046
  white-space: pre;
1047
+ overflow: hidden;
1042
1048
  }
1043
1049
 
1044
1050
  .code-surface {
@@ -78,7 +78,8 @@
78
78
  functionHighlight.innerHTML = highlightCode(code);
79
79
  }
80
80
  if (functionGutter) {
81
- const lines = Math.max(1, code.split('\n').length);
81
+ const lineBreaks = code.match(/\r\n|\r|\n/g);
82
+ const lines = Math.max(1, (lineBreaks ? lineBreaks.length : 0) + 1);
82
83
  let out = '';
83
84
  for (let i = 1; i <= lines; i += 1) {
84
85
  out += i + (i === lines ? '' : '\n');