@flowerforce/flowerbase 1.7.4 → 1.7.5-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +14 -4
- package/dist/auth/plugins/jwt.d.ts.map +1 -1
- package/dist/auth/plugins/jwt.js +8 -5
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +126 -28
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +102 -34
- package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
- package/dist/utils/initializer/exposeRoutes.js +2 -0
- package/package.json +1 -1
- package/src/auth/controller.ts +17 -6
- package/src/auth/plugins/jwt.test.ts +1 -1
- package/src/auth/plugins/jwt.ts +5 -8
- package/src/features/functions/controller.ts +149 -31
- package/src/services/mongodb-atlas/index.ts +247 -142
- package/src/utils/initializer/exposeRoutes.ts +2 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import type { ServerResponse } from 'http'
|
|
1
2
|
import { EJSON, ObjectId } from 'bson'
|
|
2
3
|
import type { FastifyRequest } from 'fastify'
|
|
3
|
-
import {
|
|
4
|
+
import type { Document } from 'mongodb'
|
|
4
5
|
import { services } from '../../services'
|
|
5
|
-
import { StateManager } from '../../state'
|
|
6
6
|
import { GenerateContext } from '../../utils/context'
|
|
7
7
|
import { Base64Function, FunctionCallBase64Dto, FunctionCallDto } from './dtos'
|
|
8
8
|
import { FunctionController } from './interface'
|
|
@@ -51,6 +51,44 @@ const isReturnedError = (value: unknown): value is { message: string; name: stri
|
|
|
51
51
|
const serializeEjson = (value: unknown) =>
|
|
52
52
|
JSON.stringify(EJSON.serialize(value, { relaxed: false }))
|
|
53
53
|
|
|
54
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
55
|
+
!!value && typeof value === 'object' && !Array.isArray(value)
|
|
56
|
+
|
|
57
|
+
type WatchSubscriber = {
|
|
58
|
+
id: string
|
|
59
|
+
user: Record<string, any>
|
|
60
|
+
response: ServerResponse
|
|
61
|
+
extraFilter?: Document
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type SharedWatchStream = {
|
|
65
|
+
database: string
|
|
66
|
+
collection: string
|
|
67
|
+
stream: {
|
|
68
|
+
on: (event: 'change' | 'error', listener: (payload: any) => void) => void
|
|
69
|
+
off: (event: 'change' | 'error', listener: (payload: any) => void) => void
|
|
70
|
+
close: () => Promise<void> | void
|
|
71
|
+
}
|
|
72
|
+
subscribers: Map<string, WatchSubscriber>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sharedWatchStreams = new Map<string, SharedWatchStream>()
|
|
76
|
+
let watchSubscriberCounter = 0
|
|
77
|
+
|
|
78
|
+
const parseWatchFilter = (args: unknown): Document | undefined => {
|
|
79
|
+
if (!isRecord(args)) return undefined
|
|
80
|
+
const candidate =
|
|
81
|
+
(isRecord(args.filter) ? args.filter : undefined) ??
|
|
82
|
+
(isRecord(args.query) ? args.query : undefined)
|
|
83
|
+
return candidate ? (candidate as Document) : undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const isReadableDocumentResult = (value: unknown) =>
|
|
87
|
+
!!value &&
|
|
88
|
+
typeof value === 'object' &&
|
|
89
|
+
!Array.isArray(value) &&
|
|
90
|
+
Object.keys(value as Record<string, unknown>).length > 0
|
|
91
|
+
|
|
54
92
|
/**
|
|
55
93
|
* > Creates a pre handler for every query
|
|
56
94
|
* @param app -> the fastify instance
|
|
@@ -63,8 +101,6 @@ export const functionsController: FunctionController = async (
|
|
|
63
101
|
) => {
|
|
64
102
|
app.addHook('preHandler', app.jwtAuthentication)
|
|
65
103
|
|
|
66
|
-
const streams = {} as Record<string, ChangeStream<Document, Document>>
|
|
67
|
-
|
|
68
104
|
app.post<{ Body: FunctionCallDto }>('/call', {
|
|
69
105
|
schema: {
|
|
70
106
|
tags: ['Functions']
|
|
@@ -170,13 +206,12 @@ export const functionsController: FunctionController = async (
|
|
|
170
206
|
}
|
|
171
207
|
const { baas_request, stitch_request } = query
|
|
172
208
|
|
|
173
|
-
const
|
|
209
|
+
const decodedConfig = JSON.parse(
|
|
174
210
|
Buffer.from(baas_request || stitch_request || '', 'base64').toString('utf8')
|
|
175
211
|
)
|
|
212
|
+
const config = EJSON.deserialize(decodedConfig) as Base64Function
|
|
176
213
|
|
|
177
|
-
const [{ database, collection }] = config.arguments
|
|
178
|
-
const app = StateManager.select('app')
|
|
179
|
-
const services = StateManager.select('services')
|
|
214
|
+
const [{ database, collection, ...watchArgs }] = config.arguments
|
|
180
215
|
|
|
181
216
|
const headers = {
|
|
182
217
|
'Content-Type': 'text/event-stream',
|
|
@@ -190,36 +225,119 @@ export const functionsController: FunctionController = async (
|
|
|
190
225
|
res.raw.writeHead(200, headers)
|
|
191
226
|
res.raw.flushHeaders();
|
|
192
227
|
|
|
193
|
-
const
|
|
228
|
+
const streamKey = `${database}::${collection}`
|
|
229
|
+
const subscriberId = `${Date.now()}-${watchSubscriberCounter++}`
|
|
230
|
+
const extraFilter = parseWatchFilter(watchArgs)
|
|
231
|
+
const mongoClient = app.mongo.client as unknown as {
|
|
232
|
+
db: (name: string) => { collection: (name: string) => { watch: (...args: any[]) => any } }
|
|
233
|
+
}
|
|
194
234
|
|
|
195
|
-
|
|
235
|
+
let hub = sharedWatchStreams.get(streamKey)
|
|
236
|
+
if (!hub) {
|
|
237
|
+
const stream = mongoClient.db(database).collection(collection).watch([], {
|
|
238
|
+
fullDocument: 'whenAvailable'
|
|
239
|
+
})
|
|
240
|
+
hub = {
|
|
241
|
+
database,
|
|
242
|
+
collection,
|
|
243
|
+
stream,
|
|
244
|
+
subscribers: new Map<string, WatchSubscriber>()
|
|
245
|
+
}
|
|
246
|
+
sharedWatchStreams.set(streamKey, hub)
|
|
247
|
+
}
|
|
196
248
|
|
|
197
|
-
const
|
|
249
|
+
const ensureHubListeners = (currentHub: SharedWatchStream) => {
|
|
250
|
+
if ((currentHub as SharedWatchStream & { listenersBound?: boolean }).listenersBound) {
|
|
251
|
+
return
|
|
252
|
+
}
|
|
198
253
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
254
|
+
const closeHub = async () => {
|
|
255
|
+
currentHub.stream.off('change', onHubChange)
|
|
256
|
+
currentHub.stream.off('error', onHubError)
|
|
257
|
+
sharedWatchStreams.delete(streamKey)
|
|
258
|
+
try {
|
|
259
|
+
await currentHub.stream.close()
|
|
260
|
+
} catch {
|
|
261
|
+
// Ignore stream close errors.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const onHubChange = async (change: Document) => {
|
|
266
|
+
const subscribers = Array.from(currentHub.subscribers.values())
|
|
267
|
+
await Promise.all(subscribers.map(async (subscriber) => {
|
|
268
|
+
const subscriberRes = subscriber.response
|
|
269
|
+
if (subscriberRes.writableEnded || subscriberRes.destroyed) {
|
|
270
|
+
currentHub.subscribers.delete(subscriber.id)
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const docId =
|
|
275
|
+
(change as { documentKey?: { _id?: unknown } })?.documentKey?._id ??
|
|
276
|
+
(change as { fullDocument?: { _id?: unknown } })?.fullDocument?._id
|
|
277
|
+
if (typeof docId === 'undefined') return
|
|
278
|
+
|
|
279
|
+
const readQuery = subscriber.extraFilter
|
|
280
|
+
? ({ $and: [subscriber.extraFilter, { _id: docId }] } as Document)
|
|
281
|
+
: ({ _id: docId } as Document)
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const readableDoc = await services['mongodb-atlas'](app, {
|
|
285
|
+
user: subscriber.user,
|
|
286
|
+
rules
|
|
287
|
+
})
|
|
288
|
+
.db(currentHub.database)
|
|
289
|
+
.collection(currentHub.collection)
|
|
290
|
+
.findOne(readQuery)
|
|
291
|
+
|
|
292
|
+
if (!isReadableDocumentResult(readableDoc)) return
|
|
293
|
+
subscriberRes.write(`data: ${serializeEjson(change)}\n\n`)
|
|
294
|
+
} catch (error) {
|
|
295
|
+
subscriberRes.write(`event: error\ndata: ${formatFunctionExecutionError(error)}\n\n`)
|
|
296
|
+
subscriberRes.end()
|
|
297
|
+
currentHub.subscribers.delete(subscriber.id)
|
|
298
|
+
}
|
|
299
|
+
}))
|
|
300
|
+
|
|
301
|
+
if (!currentHub.subscribers.size) {
|
|
302
|
+
await closeHub()
|
|
303
|
+
}
|
|
304
|
+
}
|
|
203
305
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
306
|
+
const onHubError = async (error: unknown) => {
|
|
307
|
+
for (const subscriber of currentHub.subscribers.values()) {
|
|
308
|
+
const subscriberRes = subscriber.response
|
|
309
|
+
if (!subscriberRes.writableEnded && !subscriberRes.destroyed) {
|
|
310
|
+
subscriberRes.write(`event: error\ndata: ${formatFunctionExecutionError(error)}\n\n`)
|
|
311
|
+
subscriberRes.end()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
currentHub.subscribers.clear()
|
|
315
|
+
await closeHub()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
currentHub.stream.on('change', onHubChange)
|
|
319
|
+
currentHub.stream.on('error', onHubError)
|
|
320
|
+
; (currentHub as SharedWatchStream & { listenersBound?: boolean }).listenersBound = true
|
|
210
321
|
}
|
|
211
322
|
|
|
212
|
-
|
|
213
|
-
user,
|
|
214
|
-
rules
|
|
215
|
-
})
|
|
216
|
-
.db(database)
|
|
217
|
-
.collection(collection)
|
|
218
|
-
.watch([], { fullDocument: 'whenAvailable' });
|
|
323
|
+
ensureHubListeners(hub)
|
|
219
324
|
|
|
325
|
+
const subscriber: WatchSubscriber = {
|
|
326
|
+
id: subscriberId,
|
|
327
|
+
user,
|
|
328
|
+
response: res.raw,
|
|
329
|
+
extraFilter
|
|
330
|
+
}
|
|
331
|
+
hub.subscribers.set(subscriberId, subscriber)
|
|
220
332
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
333
|
+
req.raw.on('close', () => {
|
|
334
|
+
const currentHub = sharedWatchStreams.get(streamKey)
|
|
335
|
+
if (!currentHub) return
|
|
336
|
+
currentHub.subscribers.delete(subscriberId)
|
|
337
|
+
if (!currentHub.subscribers.size) {
|
|
338
|
+
void currentHub.stream.close()
|
|
339
|
+
sharedWatchStreams.delete(streamKey)
|
|
340
|
+
}
|
|
341
|
+
})
|
|
224
342
|
})
|
|
225
343
|
}
|