@flowerforce/flowerbase 1.8.2 → 1.8.4-beta.1
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 +7 -0
- package/README.md +64 -19
- package/dist/features/functions/utils.d.ts +1 -1
- package/dist/features/functions/utils.d.ts.map +1 -1
- package/dist/features/functions/utils.js +1 -1
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +28 -18
- package/package.json +1 -1
- package/src/features/functions/__tests__/utils.test.ts +33 -0
- package/src/features/functions/utils.ts +6 -1
- package/src/features/triggers/__tests__/utils.test.ts +112 -0
- package/src/features/triggers/utils.ts +30 -18
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -616,35 +616,80 @@ Once deployed, you'll receive a public URL (e.g. https://your-app-name.up.exampl
|
|
|
616
616
|
|
|
617
617
|
>This URL should be used as the base URL in your frontend application, as explained in the next section.
|
|
618
618
|
|
|
619
|
-
## 🌐 Frontend Setup –
|
|
619
|
+
## 🌐 Frontend Setup – `@flowerforce/flowerbase-client` (Recommended)
|
|
620
620
|
|
|
621
|
-
|
|
622
|
-
This serves as a sample setup — similar logic can be applied using other official Realm SDKs **(e.g. React Native, Node, or Flutter)**.
|
|
623
|
-
|
|
624
|
-
### 📦 Install Realm SDK
|
|
621
|
+
For frontend and mobile projects, you can use the dedicated Flowerbase client:
|
|
625
622
|
|
|
626
623
|
```bash
|
|
627
|
-
npm install
|
|
624
|
+
npm install @flowerforce/flowerbase-client
|
|
628
625
|
```
|
|
629
626
|
|
|
630
|
-
### ⚙️ Configure
|
|
631
|
-
|
|
632
|
-
Create a file to initialize and export the Realm App instance:
|
|
627
|
+
### ⚙️ Configure client app
|
|
633
628
|
|
|
634
629
|
```ts
|
|
635
|
-
|
|
630
|
+
import { App, Credentials } from '@flowerforce/flowerbase-client'
|
|
636
631
|
|
|
637
|
-
|
|
632
|
+
const app = new App({
|
|
633
|
+
id: 'your-app-id',
|
|
634
|
+
baseUrl: 'https://your-deployed-backend-url.com',
|
|
635
|
+
timeout: 10000
|
|
636
|
+
})
|
|
638
637
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
638
|
+
await app.logIn(Credentials.emailPassword('user@example.com', 'secret'))
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### 📦 Common client operations
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
const user = app.currentUser
|
|
645
|
+
if (!user) throw new Error('User not logged in')
|
|
646
|
+
|
|
647
|
+
const profile = await user.functions.getProfile()
|
|
644
648
|
|
|
645
|
-
|
|
649
|
+
const todos = user.mongoClient('mongodb-atlas')
|
|
650
|
+
.db('my-db')
|
|
651
|
+
.collection('todos')
|
|
646
652
|
|
|
653
|
+
await todos.insertOne({ title: 'Ship docs update', done: false })
|
|
654
|
+
const openTodos = await todos.find({ done: false })
|
|
647
655
|
```
|
|
648
656
|
|
|
649
|
-
|
|
650
|
-
|
|
657
|
+
`@flowerforce/flowerbase-client` supports:
|
|
658
|
+
- local-userpass / anon-user / custom-function authentication
|
|
659
|
+
- function calls (`user.functions.<name>(...)`)
|
|
660
|
+
- MongoDB operations via `user.mongoClient('mongodb-atlas')`
|
|
661
|
+
- change streams with `watch()` async iterator
|
|
662
|
+
- BSON/EJSON interoperability (`ObjectId`, `Date`, etc.)
|
|
663
|
+
|
|
664
|
+
## 💡 Use Cases by Feature
|
|
665
|
+
|
|
666
|
+
### 🔐 Authentication
|
|
667
|
+
- Registration and login flows for SaaS dashboards using `local-userpass`.
|
|
668
|
+
- Guest sessions for trial users with `anon-user`, then account upgrade with full registration.
|
|
669
|
+
- Delegated enterprise login with `custom-function` auth when credentials must be validated by external identity logic.
|
|
670
|
+
|
|
671
|
+
### 🔒 Rules
|
|
672
|
+
- Multi-tenant isolation where each user can only read/write documents of their own workspace.
|
|
673
|
+
- Field-level protection to hide private fields (for example billing or internal notes) from non-admin users.
|
|
674
|
+
|
|
675
|
+
### ⚙️ Functions
|
|
676
|
+
- Centralized business logic (pricing, counters, workflows) called from web and mobile clients.
|
|
677
|
+
- Privileged server-side tasks invoked with `run_as_system` to perform safe internal operations.
|
|
678
|
+
|
|
679
|
+
### 🔔 Triggers
|
|
680
|
+
- Audit logging on insert/update/delete events into an activity collection.
|
|
681
|
+
- Scheduled jobs (for example nightly cleanup, reminder generation, data aggregation).
|
|
682
|
+
- Auth lifecycle reactions (welcome email on user creation, cleanup on user deletion).
|
|
683
|
+
|
|
684
|
+
### 🌐 HTTP Endpoints
|
|
685
|
+
- Public webhook ingestion from third-party systems.
|
|
686
|
+
- Protected custom APIs for backoffice actions not exposed as direct database operations.
|
|
687
|
+
|
|
688
|
+
### 📡 `flowerbase-client`
|
|
689
|
+
- Real-time UI updates in task boards using `collection.watch()` change streams.
|
|
690
|
+
- Frontend data access with Realm-style API surface to minimize integration complexity.
|
|
691
|
+
- Shared client usage across web and React Native projects with consistent auth/session behavior.
|
|
692
|
+
|
|
693
|
+
### 🖥 Monitoring UI
|
|
694
|
+
- Live inspection of function invocations, endpoint calls, and trigger executions in staging/production.
|
|
695
|
+
- Fast troubleshooting with event stream filters and user/session search tools.
|
|
@@ -18,7 +18,7 @@ export declare const executeQuery: ({ currentMethod, query, update, filter, proj
|
|
|
18
18
|
countDocuments: () => Promise<number>;
|
|
19
19
|
deleteOne: () => Promise<import("mongodb").DeleteResult>;
|
|
20
20
|
insertOne: () => Promise<import("mongodb").InsertOneResult<Document>>;
|
|
21
|
-
updateOne: () => Promise<
|
|
21
|
+
updateOne: () => Promise<import("mongodb").UpdateResult<Document>>;
|
|
22
22
|
findOneAndUpdate: () => Promise<Document | null>;
|
|
23
23
|
aggregate: () => Promise<Document[]>;
|
|
24
24
|
insertMany: () => Promise<import("mongodb").InsertManyResult<Document>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/features/functions/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAGlC,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE3D;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAU,gBAAuB,KAAG,OAAO,CAAC,SAAS,CAwB9E,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAU,2HAYhC,kBAAkB;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/features/functions/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAGlC,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE3D;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAU,gBAAuB,KAAG,OAAO,CAAC,SAAS,CAwB9E,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAU,2HAYhC,kBAAkB;;;;;;;;;;;;;EAsGpB,CAAA"}
|
|
@@ -100,7 +100,7 @@ const executeQuery = (_a) => __awaiter(void 0, [_a], void 0, function* ({ curren
|
|
|
100
100
|
countDocuments: () => currentMethod(bson_1.EJSON.deserialize(resolvedQuery), parsedOptions),
|
|
101
101
|
deleteOne: () => currentMethod(bson_1.EJSON.deserialize(resolvedQuery), parsedOptions),
|
|
102
102
|
insertOne: () => currentMethod(bson_1.EJSON.deserialize(document)),
|
|
103
|
-
updateOne: () => currentMethod(bson_1.EJSON.deserialize(resolvedQuery), bson_1.EJSON.deserialize(resolvedUpdate)),
|
|
103
|
+
updateOne: () => currentMethod(bson_1.EJSON.deserialize(resolvedQuery), bson_1.EJSON.deserialize(resolvedUpdate), parsedOptions),
|
|
104
104
|
findOneAndUpdate: () => currentMethod(bson_1.EJSON.deserialize(resolvedQuery), bson_1.EJSON.deserialize(resolvedUpdate), parsedOptions),
|
|
105
105
|
aggregate: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
106
106
|
return (yield currentMethod(bson_1.EJSON.deserialize(pipeline), {}, // TODO -> ADD OPTIONS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/features/triggers/utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/features/triggers/utils.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,aAAa,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAA;AA6E9D;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAU,gBAAuB,KAAG,OAAO,CAAC,QAAQ,CAkB5E,CAAA;AAmrBD,eAAO,MAAM,gBAAgB;kHA5pB1B,aAAa;iHAmkBb,aAAa;uHAhdb,aAAa;CA6iBf,CAAA"}
|
|
@@ -27,11 +27,13 @@ exports.TRIGGER_HANDLERS = exports.loadTriggers = void 0;
|
|
|
27
27
|
const fs_1 = __importDefault(require("fs"));
|
|
28
28
|
const node_path_1 = __importDefault(require("node:path"));
|
|
29
29
|
const node_cron_1 = __importDefault(require("node-cron"));
|
|
30
|
+
const bson_1 = require("bson");
|
|
30
31
|
const constants_1 = require("../../constants");
|
|
31
32
|
const utils_1 = require("../../monitoring/utils");
|
|
32
33
|
const state_1 = require("../../state");
|
|
33
34
|
const utils_2 = require("../../utils");
|
|
34
35
|
const context_1 = require("../../utils/context");
|
|
36
|
+
const normalizeTriggerPayload = (value) => bson_1.EJSON.deserialize(bson_1.EJSON.serialize(value, { relaxed: false }));
|
|
35
37
|
const registerOnClose = (app, handler, label) => {
|
|
36
38
|
if (app.server) {
|
|
37
39
|
app.server.once('close', () => {
|
|
@@ -148,7 +150,8 @@ const handleCronTrigger = (_a) => __awaiter(void 0, [_a], void 0, function* ({ c
|
|
|
148
150
|
currentFunction: triggerHandler,
|
|
149
151
|
functionName,
|
|
150
152
|
functionsList,
|
|
151
|
-
services
|
|
153
|
+
services,
|
|
154
|
+
deserializeArgs: false
|
|
152
155
|
});
|
|
153
156
|
}
|
|
154
157
|
catch (error) {
|
|
@@ -332,7 +335,7 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
332
335
|
meta: Object.assign(Object.assign({}, baseMeta), { event: 'LOGOUT' })
|
|
333
336
|
});
|
|
334
337
|
yield (0, context_1.GenerateContext)({
|
|
335
|
-
args: [Object.assign({ user: userData }, op)],
|
|
338
|
+
args: [normalizeTriggerPayload(Object.assign({ user: userData }, op))],
|
|
336
339
|
app,
|
|
337
340
|
rules: state_1.StateManager.select("rules"),
|
|
338
341
|
user: {}, // TODO from currentUser ??
|
|
@@ -340,7 +343,8 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
340
343
|
functionName,
|
|
341
344
|
functionsList,
|
|
342
345
|
services,
|
|
343
|
-
runAsSystem: true
|
|
346
|
+
runAsSystem: true,
|
|
347
|
+
deserializeArgs: false
|
|
344
348
|
});
|
|
345
349
|
}
|
|
346
350
|
catch (error) {
|
|
@@ -352,7 +356,6 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
352
356
|
meta: Object.assign(Object.assign({}, baseMeta), { event: 'LOGOUT' }),
|
|
353
357
|
error
|
|
354
358
|
});
|
|
355
|
-
console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error);
|
|
356
359
|
}
|
|
357
360
|
return;
|
|
358
361
|
}
|
|
@@ -389,7 +392,7 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
389
392
|
meta: Object.assign(Object.assign({}, baseMeta), { event: 'LOGIN' })
|
|
390
393
|
});
|
|
391
394
|
yield (0, context_1.GenerateContext)({
|
|
392
|
-
args: [Object.assign({ user: userData }, op)],
|
|
395
|
+
args: [normalizeTriggerPayload(Object.assign({ user: userData }, op))],
|
|
393
396
|
app,
|
|
394
397
|
rules: state_1.StateManager.select("rules"),
|
|
395
398
|
user: {}, // TODO from currentUser ??
|
|
@@ -397,7 +400,8 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
397
400
|
functionName,
|
|
398
401
|
functionsList,
|
|
399
402
|
services,
|
|
400
|
-
runAsSystem: true
|
|
403
|
+
runAsSystem: true,
|
|
404
|
+
deserializeArgs: false
|
|
401
405
|
});
|
|
402
406
|
}
|
|
403
407
|
catch (error) {
|
|
@@ -409,7 +413,6 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
409
413
|
meta: Object.assign(Object.assign({}, baseMeta), { event: 'LOGIN' }),
|
|
410
414
|
error
|
|
411
415
|
});
|
|
412
|
-
console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error);
|
|
413
416
|
}
|
|
414
417
|
return;
|
|
415
418
|
}
|
|
@@ -441,7 +444,9 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
441
444
|
meta: Object.assign(Object.assign({}, baseMeta), { event: 'DELETE' })
|
|
442
445
|
});
|
|
443
446
|
yield (0, context_1.GenerateContext)({
|
|
444
|
-
args: isAutoTrigger
|
|
447
|
+
args: isAutoTrigger
|
|
448
|
+
? [normalizeTriggerPayload(userData)]
|
|
449
|
+
: [normalizeTriggerPayload(Object.assign({ user: userData }, op))],
|
|
445
450
|
app,
|
|
446
451
|
rules: state_1.StateManager.select("rules"),
|
|
447
452
|
user: {}, // TODO from currentUser ??
|
|
@@ -449,7 +454,8 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
449
454
|
functionName,
|
|
450
455
|
functionsList,
|
|
451
456
|
services,
|
|
452
|
-
runAsSystem: true
|
|
457
|
+
runAsSystem: true,
|
|
458
|
+
deserializeArgs: false
|
|
453
459
|
});
|
|
454
460
|
}
|
|
455
461
|
catch (error) {
|
|
@@ -461,7 +467,6 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
461
467
|
meta: Object.assign(Object.assign({}, baseMeta), { event: 'DELETE' }),
|
|
462
468
|
error
|
|
463
469
|
});
|
|
464
|
-
console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error);
|
|
465
470
|
}
|
|
466
471
|
return;
|
|
467
472
|
}
|
|
@@ -495,7 +500,9 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
495
500
|
meta: Object.assign(Object.assign({}, baseMeta), { event: 'UPDATE' })
|
|
496
501
|
});
|
|
497
502
|
yield (0, context_1.GenerateContext)({
|
|
498
|
-
args: isAutoTrigger
|
|
503
|
+
args: isAutoTrigger
|
|
504
|
+
? [normalizeTriggerPayload(userData)]
|
|
505
|
+
: [normalizeTriggerPayload(Object.assign({ user: userData }, op))],
|
|
499
506
|
app,
|
|
500
507
|
rules: state_1.StateManager.select("rules"),
|
|
501
508
|
user: {}, // TODO from currentUser ??
|
|
@@ -503,7 +510,8 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
503
510
|
functionName,
|
|
504
511
|
functionsList,
|
|
505
512
|
services,
|
|
506
|
-
runAsSystem: true
|
|
513
|
+
runAsSystem: true,
|
|
514
|
+
deserializeArgs: false
|
|
507
515
|
});
|
|
508
516
|
}
|
|
509
517
|
catch (error) {
|
|
@@ -515,7 +523,6 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
515
523
|
meta: Object.assign(Object.assign({}, baseMeta), { event: 'UPDATE' }),
|
|
516
524
|
error
|
|
517
525
|
});
|
|
518
|
-
console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error);
|
|
519
526
|
}
|
|
520
527
|
return;
|
|
521
528
|
}
|
|
@@ -586,7 +593,9 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
586
593
|
meta: Object.assign(Object.assign({}, baseMeta), { event: 'CREATE' })
|
|
587
594
|
});
|
|
588
595
|
yield (0, context_1.GenerateContext)({
|
|
589
|
-
args: isAutoTrigger
|
|
596
|
+
args: isAutoTrigger
|
|
597
|
+
? [normalizeTriggerPayload(userData)]
|
|
598
|
+
: [normalizeTriggerPayload(Object.assign({ user: userData }, op))],
|
|
590
599
|
app,
|
|
591
600
|
rules: state_1.StateManager.select("rules"),
|
|
592
601
|
user: {}, // TODO from currentUser ??
|
|
@@ -594,7 +603,8 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
594
603
|
functionName,
|
|
595
604
|
functionsList,
|
|
596
605
|
services,
|
|
597
|
-
runAsSystem: true
|
|
606
|
+
runAsSystem: true,
|
|
607
|
+
deserializeArgs: false
|
|
598
608
|
});
|
|
599
609
|
}
|
|
600
610
|
catch (error) {
|
|
@@ -606,7 +616,6 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
|
|
|
606
616
|
meta: Object.assign(Object.assign({}, baseMeta), { event: 'CREATE' }),
|
|
607
617
|
error
|
|
608
618
|
});
|
|
609
|
-
console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error);
|
|
610
619
|
}
|
|
611
620
|
});
|
|
612
621
|
});
|
|
@@ -677,14 +686,15 @@ const handleDataBaseTrigger = (_a) => __awaiter(void 0, [_a], void 0, function*
|
|
|
677
686
|
});
|
|
678
687
|
try {
|
|
679
688
|
yield (0, context_1.GenerateContext)({
|
|
680
|
-
args: [change],
|
|
689
|
+
args: [normalizeTriggerPayload(change)],
|
|
681
690
|
app,
|
|
682
691
|
rules: state_1.StateManager.select("rules"),
|
|
683
692
|
user: {}, // TODO add from?
|
|
684
693
|
currentFunction: triggerHandler,
|
|
685
694
|
functionName,
|
|
686
695
|
functionsList,
|
|
687
|
-
services
|
|
696
|
+
services,
|
|
697
|
+
deserializeArgs: false
|
|
688
698
|
});
|
|
689
699
|
}
|
|
690
700
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { executeQuery } from '../utils'
|
|
2
|
+
|
|
3
|
+
describe('executeQuery', () => {
|
|
4
|
+
it('passes parsed options to updateOne', async () => {
|
|
5
|
+
const currentMethod = jest.fn().mockResolvedValue({
|
|
6
|
+
acknowledged: true,
|
|
7
|
+
matchedCount: 0,
|
|
8
|
+
modifiedCount: 0,
|
|
9
|
+
upsertedCount: 1
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const operators = await executeQuery({
|
|
13
|
+
currentMethod,
|
|
14
|
+
query: { ownerUserId: 'user-1' },
|
|
15
|
+
update: {
|
|
16
|
+
$set: { locale: 'it-IT' },
|
|
17
|
+
$setOnInsert: { scope: 'workspace' }
|
|
18
|
+
},
|
|
19
|
+
options: { upsert: true }
|
|
20
|
+
} as any)
|
|
21
|
+
|
|
22
|
+
await operators.updateOne()
|
|
23
|
+
|
|
24
|
+
expect(currentMethod).toHaveBeenCalledWith(
|
|
25
|
+
{ ownerUserId: 'user-1' },
|
|
26
|
+
{
|
|
27
|
+
$set: { locale: 'it-IT' },
|
|
28
|
+
$setOnInsert: { scope: 'workspace' }
|
|
29
|
+
},
|
|
30
|
+
{ upsert: true }
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -122,7 +122,12 @@ export const executeQuery = async ({
|
|
|
122
122
|
(currentMethod as ReturnType<GetOperatorsFunction>['insertOne'])(
|
|
123
123
|
EJSON.deserialize(document)
|
|
124
124
|
),
|
|
125
|
-
updateOne: () =>
|
|
125
|
+
updateOne: () =>
|
|
126
|
+
(currentMethod as ReturnType<GetOperatorsFunction>['updateOne'])(
|
|
127
|
+
EJSON.deserialize(resolvedQuery),
|
|
128
|
+
EJSON.deserialize(resolvedUpdate),
|
|
129
|
+
parsedOptions
|
|
130
|
+
),
|
|
126
131
|
findOneAndUpdate: () =>
|
|
127
132
|
(currentMethod as ReturnType<GetOperatorsFunction>['findOneAndUpdate'])(
|
|
128
133
|
EJSON.deserialize(resolvedQuery),
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ObjectId } from 'bson'
|
|
2
|
+
import { GenerateContext } from '../../../utils/context'
|
|
3
|
+
import { StateManager } from '../../../state'
|
|
4
|
+
import { TRIGGER_HANDLERS } from '../utils'
|
|
5
|
+
|
|
6
|
+
jest.mock('../../../utils/context', () => ({
|
|
7
|
+
GenerateContext: jest.fn()
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
jest.mock('../../../state', () => ({
|
|
11
|
+
StateManager: {
|
|
12
|
+
select: jest.fn()
|
|
13
|
+
}
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const mockedGenerateContext = jest.mocked(GenerateContext)
|
|
17
|
+
const mockedStateSelect = StateManager.select as jest.Mock
|
|
18
|
+
|
|
19
|
+
describe('TRIGGER_HANDLERS.DATABASE', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockedGenerateContext.mockReset()
|
|
22
|
+
mockedGenerateContext.mockResolvedValue(undefined)
|
|
23
|
+
mockedStateSelect.mockReset()
|
|
24
|
+
mockedStateSelect.mockReturnValue(undefined)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('preserves BSON ObjectId values in fullDocument before invoking the trigger function', async () => {
|
|
28
|
+
const changeListeners: Record<string, (...args: any[]) => unknown> = {}
|
|
29
|
+
const close = jest.fn(async () => undefined)
|
|
30
|
+
const watch = jest.fn(() => ({
|
|
31
|
+
on: jest.fn((event: string, listener: (...args: any[]) => unknown) => {
|
|
32
|
+
changeListeners[event] = listener
|
|
33
|
+
}),
|
|
34
|
+
close
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
const collection = { watch }
|
|
38
|
+
const db = jest.fn(() => ({ collection: jest.fn(() => collection) }))
|
|
39
|
+
const client = { db }
|
|
40
|
+
|
|
41
|
+
const app = {
|
|
42
|
+
mongo: {
|
|
43
|
+
changestream: { client }
|
|
44
|
+
},
|
|
45
|
+
server: {
|
|
46
|
+
once: jest.fn()
|
|
47
|
+
}
|
|
48
|
+
} as any
|
|
49
|
+
|
|
50
|
+
await TRIGGER_HANDLERS.DATABASE({
|
|
51
|
+
config: {
|
|
52
|
+
database: 'flowerbase-test',
|
|
53
|
+
collection: 'activityLogs',
|
|
54
|
+
operation_types: ['INSERT'],
|
|
55
|
+
full_document: true,
|
|
56
|
+
full_document_before_change: false,
|
|
57
|
+
match: {},
|
|
58
|
+
project: {}
|
|
59
|
+
} as any,
|
|
60
|
+
triggerHandler: { code: 'module.exports = async function () {}' } as any,
|
|
61
|
+
functionsList: {
|
|
62
|
+
logTriggerEvent: { code: 'module.exports = async function () {}' }
|
|
63
|
+
} as any,
|
|
64
|
+
services: {} as any,
|
|
65
|
+
app,
|
|
66
|
+
triggerName: 'log-trigger-event',
|
|
67
|
+
triggerType: 'DATABASE',
|
|
68
|
+
functionName: 'logTriggerEvent'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const documentId = new ObjectId('507f1f77bcf86cd799439011')
|
|
72
|
+
const ownerId = new ObjectId('507f191e810c19729de860ea')
|
|
73
|
+
|
|
74
|
+
await changeListeners.change({
|
|
75
|
+
clusterTime: new Date(),
|
|
76
|
+
operationType: 'insert',
|
|
77
|
+
ns: { db: 'flowerbase-test', coll: 'activityLogs' },
|
|
78
|
+
documentKey: { _id: documentId },
|
|
79
|
+
fullDocument: {
|
|
80
|
+
_id: documentId,
|
|
81
|
+
ownerId
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(mockedGenerateContext).toHaveBeenCalledTimes(1)
|
|
86
|
+
expect(mockedGenerateContext).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
deserializeArgs: false,
|
|
89
|
+
args: [
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
documentKey: expect.objectContaining({
|
|
92
|
+
_id: expect.any(ObjectId)
|
|
93
|
+
}),
|
|
94
|
+
fullDocument: expect.objectContaining({
|
|
95
|
+
_id: expect.any(ObjectId),
|
|
96
|
+
ownerId: expect.any(ObjectId)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
]
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const payload = mockedGenerateContext.mock.calls[0][0].args[0] as {
|
|
104
|
+
documentKey: { _id: ObjectId }
|
|
105
|
+
fullDocument: { _id: ObjectId; ownerId: ObjectId }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
expect(payload.documentKey._id.toHexString()).toBe(documentId.toHexString())
|
|
109
|
+
expect(payload.fullDocument._id.toHexString()).toBe(documentId.toHexString())
|
|
110
|
+
expect(payload.fullDocument.ownerId.toHexString()).toBe(ownerId.toHexString())
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import cron from 'node-cron'
|
|
4
|
+
import { EJSON } from 'bson'
|
|
4
5
|
import { AUTH_CONFIG, AUTH_DB_NAME, DB_NAME, CHANGESTREAM } from '../../constants'
|
|
5
6
|
import { createEventId, sanitize } from '../../monitoring/utils'
|
|
6
7
|
import { StateManager } from '../../state'
|
|
@@ -8,6 +9,9 @@ import { readJsonContent } from '../../utils'
|
|
|
8
9
|
import { GenerateContext } from '../../utils/context'
|
|
9
10
|
import { HandlerParams, Trigger, Triggers } from './interface'
|
|
10
11
|
|
|
12
|
+
const normalizeTriggerPayload = <T>(value: T): T =>
|
|
13
|
+
EJSON.deserialize(EJSON.serialize(value, { relaxed: false })) as T
|
|
14
|
+
|
|
11
15
|
const registerOnClose = (
|
|
12
16
|
app: HandlerParams['app'],
|
|
13
17
|
handler: () => Promise<void> | void,
|
|
@@ -150,7 +154,8 @@ const handleCronTrigger = async ({
|
|
|
150
154
|
currentFunction: triggerHandler,
|
|
151
155
|
functionName,
|
|
152
156
|
functionsList,
|
|
153
|
-
services
|
|
157
|
+
services,
|
|
158
|
+
deserializeArgs: false
|
|
154
159
|
})
|
|
155
160
|
} catch (error) {
|
|
156
161
|
emitTriggerEvent({
|
|
@@ -376,7 +381,7 @@ const handleAuthenticationTrigger = async ({
|
|
|
376
381
|
meta: { ...baseMeta, event: 'LOGOUT' }
|
|
377
382
|
})
|
|
378
383
|
await GenerateContext({
|
|
379
|
-
args: [{ user: userData, ...op }],
|
|
384
|
+
args: [normalizeTriggerPayload({ user: userData, ...op })],
|
|
380
385
|
app,
|
|
381
386
|
rules: StateManager.select("rules"),
|
|
382
387
|
user: {}, // TODO from currentUser ??
|
|
@@ -384,7 +389,8 @@ const handleAuthenticationTrigger = async ({
|
|
|
384
389
|
functionName,
|
|
385
390
|
functionsList,
|
|
386
391
|
services,
|
|
387
|
-
runAsSystem: true
|
|
392
|
+
runAsSystem: true,
|
|
393
|
+
deserializeArgs: false
|
|
388
394
|
})
|
|
389
395
|
} catch (error) {
|
|
390
396
|
emitTriggerEvent({
|
|
@@ -395,7 +401,6 @@ const handleAuthenticationTrigger = async ({
|
|
|
395
401
|
meta: { ...baseMeta, event: 'LOGOUT' },
|
|
396
402
|
error
|
|
397
403
|
})
|
|
398
|
-
console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error)
|
|
399
404
|
}
|
|
400
405
|
return
|
|
401
406
|
}
|
|
@@ -433,7 +438,7 @@ const handleAuthenticationTrigger = async ({
|
|
|
433
438
|
meta: { ...baseMeta, event: 'LOGIN' }
|
|
434
439
|
})
|
|
435
440
|
await GenerateContext({
|
|
436
|
-
args: [{ user: userData, ...op }],
|
|
441
|
+
args: [normalizeTriggerPayload({ user: userData, ...op })],
|
|
437
442
|
app,
|
|
438
443
|
rules: StateManager.select("rules"),
|
|
439
444
|
user: {}, // TODO from currentUser ??
|
|
@@ -441,7 +446,8 @@ const handleAuthenticationTrigger = async ({
|
|
|
441
446
|
functionName,
|
|
442
447
|
functionsList,
|
|
443
448
|
services,
|
|
444
|
-
runAsSystem: true
|
|
449
|
+
runAsSystem: true,
|
|
450
|
+
deserializeArgs: false
|
|
445
451
|
})
|
|
446
452
|
} catch (error) {
|
|
447
453
|
emitTriggerEvent({
|
|
@@ -452,7 +458,6 @@ const handleAuthenticationTrigger = async ({
|
|
|
452
458
|
meta: { ...baseMeta, event: 'LOGIN' },
|
|
453
459
|
error
|
|
454
460
|
})
|
|
455
|
-
console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error)
|
|
456
461
|
}
|
|
457
462
|
return
|
|
458
463
|
}
|
|
@@ -485,7 +490,9 @@ const handleAuthenticationTrigger = async ({
|
|
|
485
490
|
meta: { ...baseMeta, event: 'DELETE' }
|
|
486
491
|
})
|
|
487
492
|
await GenerateContext({
|
|
488
|
-
args: isAutoTrigger
|
|
493
|
+
args: isAutoTrigger
|
|
494
|
+
? [normalizeTriggerPayload(userData)]
|
|
495
|
+
: [normalizeTriggerPayload({ user: userData, ...op })],
|
|
489
496
|
app,
|
|
490
497
|
rules: StateManager.select("rules"),
|
|
491
498
|
user: {}, // TODO from currentUser ??
|
|
@@ -493,7 +500,8 @@ const handleAuthenticationTrigger = async ({
|
|
|
493
500
|
functionName,
|
|
494
501
|
functionsList,
|
|
495
502
|
services,
|
|
496
|
-
runAsSystem: true
|
|
503
|
+
runAsSystem: true,
|
|
504
|
+
deserializeArgs: false
|
|
497
505
|
})
|
|
498
506
|
} catch (error) {
|
|
499
507
|
emitTriggerEvent({
|
|
@@ -504,7 +512,6 @@ const handleAuthenticationTrigger = async ({
|
|
|
504
512
|
meta: { ...baseMeta, event: 'DELETE' },
|
|
505
513
|
error
|
|
506
514
|
})
|
|
507
|
-
console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error)
|
|
508
515
|
}
|
|
509
516
|
return
|
|
510
517
|
}
|
|
@@ -539,7 +546,9 @@ const handleAuthenticationTrigger = async ({
|
|
|
539
546
|
meta: { ...baseMeta, event: 'UPDATE' }
|
|
540
547
|
})
|
|
541
548
|
await GenerateContext({
|
|
542
|
-
args: isAutoTrigger
|
|
549
|
+
args: isAutoTrigger
|
|
550
|
+
? [normalizeTriggerPayload(userData)]
|
|
551
|
+
: [normalizeTriggerPayload({ user: userData, ...op })],
|
|
543
552
|
app,
|
|
544
553
|
rules: StateManager.select("rules"),
|
|
545
554
|
user: {}, // TODO from currentUser ??
|
|
@@ -547,7 +556,8 @@ const handleAuthenticationTrigger = async ({
|
|
|
547
556
|
functionName,
|
|
548
557
|
functionsList,
|
|
549
558
|
services,
|
|
550
|
-
runAsSystem: true
|
|
559
|
+
runAsSystem: true,
|
|
560
|
+
deserializeArgs: false
|
|
551
561
|
})
|
|
552
562
|
} catch (error) {
|
|
553
563
|
emitTriggerEvent({
|
|
@@ -558,7 +568,6 @@ const handleAuthenticationTrigger = async ({
|
|
|
558
568
|
meta: { ...baseMeta, event: 'UPDATE' },
|
|
559
569
|
error
|
|
560
570
|
})
|
|
561
|
-
console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error)
|
|
562
571
|
}
|
|
563
572
|
return
|
|
564
573
|
}
|
|
@@ -643,7 +652,9 @@ const handleAuthenticationTrigger = async ({
|
|
|
643
652
|
meta: { ...baseMeta, event: 'CREATE' }
|
|
644
653
|
})
|
|
645
654
|
await GenerateContext({
|
|
646
|
-
args: isAutoTrigger
|
|
655
|
+
args: isAutoTrigger
|
|
656
|
+
? [normalizeTriggerPayload(userData)]
|
|
657
|
+
: [normalizeTriggerPayload({ user: userData, ...op })],
|
|
647
658
|
app,
|
|
648
659
|
rules: StateManager.select("rules"),
|
|
649
660
|
user: {}, // TODO from currentUser ??
|
|
@@ -651,7 +662,8 @@ const handleAuthenticationTrigger = async ({
|
|
|
651
662
|
functionName,
|
|
652
663
|
functionsList,
|
|
653
664
|
services,
|
|
654
|
-
runAsSystem: true
|
|
665
|
+
runAsSystem: true,
|
|
666
|
+
deserializeArgs: false
|
|
655
667
|
})
|
|
656
668
|
} catch (error) {
|
|
657
669
|
emitTriggerEvent({
|
|
@@ -662,7 +674,6 @@ const handleAuthenticationTrigger = async ({
|
|
|
662
674
|
meta: { ...baseMeta, event: 'CREATE' },
|
|
663
675
|
error
|
|
664
676
|
})
|
|
665
|
-
console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error)
|
|
666
677
|
}
|
|
667
678
|
})
|
|
668
679
|
registerOnClose(
|
|
@@ -755,14 +766,15 @@ const handleDataBaseTrigger = async ({
|
|
|
755
766
|
})
|
|
756
767
|
try {
|
|
757
768
|
await GenerateContext({
|
|
758
|
-
args: [change],
|
|
769
|
+
args: [normalizeTriggerPayload(change)],
|
|
759
770
|
app,
|
|
760
771
|
rules: StateManager.select("rules"),
|
|
761
772
|
user: {}, // TODO add from?
|
|
762
773
|
currentFunction: triggerHandler,
|
|
763
774
|
functionName,
|
|
764
775
|
functionsList,
|
|
765
|
-
services
|
|
776
|
+
services,
|
|
777
|
+
deserializeArgs: false
|
|
766
778
|
})
|
|
767
779
|
} catch (error) {
|
|
768
780
|
emitTriggerEvent({
|