@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.
- package/CHANGELOG.md +21 -0
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +6 -31
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +28 -36
- package/dist/monitoring/routes/collections.d.ts.map +1 -1
- package/dist/monitoring/routes/collections.js +89 -0
- package/dist/monitoring/utils.d.ts +3 -1
- package/dist/monitoring/utils.d.ts.map +1 -1
- package/dist/services/api/index.d.ts +0 -4
- package/dist/services/api/index.d.ts.map +1 -1
- package/dist/services/api/utils.d.ts +0 -1
- package/dist/services/api/utils.d.ts.map +1 -1
- package/dist/services/index.d.ts +0 -4
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +0 -14
- package/dist/utils/context/helpers.d.ts +0 -12
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/auth/providers/custom-function/controller.ts +8 -35
- package/src/features/triggers/utils.ts +28 -39
- package/src/monitoring/routes/__tests__/collections.test.ts +163 -0
- package/src/monitoring/routes/collections.ts +99 -0
- package/src/monitoring/ui.collections.js +100 -6
- package/src/monitoring/ui.css +9 -3
- package/src/monitoring/ui.functions.js +2 -1
- package/src/monitoring/ui.html +16 -0
- package/src/monitoring/utils.ts +3 -1
- package/src/services/mongodb-atlas/index.ts +108 -123
|
@@ -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
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/src/monitoring/ui.css
CHANGED
|
@@ -929,12 +929,17 @@ button.small {
|
|
|
929
929
|
align-items: stretch;
|
|
930
930
|
}
|
|
931
931
|
|
|
932
|
-
.collection-io
|
|
932
|
+
.collection-io [data-collection-mode] {
|
|
933
933
|
display: none;
|
|
934
|
+
min-height: 0;
|
|
934
935
|
}
|
|
935
936
|
|
|
936
|
-
.collection-io[data-mode="
|
|
937
|
-
|
|
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
|
|
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');
|