@flowerforce/flowerbase 1.2.0 → 1.2.1-beta.2
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 +3 -0
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +5 -2
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +7 -10
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +3 -2
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -1
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +28 -2
- package/dist/features/rules/utils.d.ts.map +1 -1
- package/dist/features/rules/utils.js +11 -2
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +52 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -9
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +540 -483
- package/dist/services/mongodb-atlas/utils.d.ts +9 -2
- package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.js +113 -23
- package/dist/shared/handleUserRegistration.d.ts.map +1 -1
- package/dist/shared/handleUserRegistration.js +1 -0
- package/dist/shared/models/handleUserRegistration.model.d.ts +6 -2
- package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
- package/dist/utils/context/helpers.d.ts +6 -5
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/context/helpers.js +3 -0
- package/dist/utils/context/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +2 -0
- package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
- package/dist/utils/initializer/exposeRoutes.js +11 -4
- package/dist/utils/initializer/registerPlugins.d.ts +3 -1
- package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
- package/dist/utils/initializer/registerPlugins.js +9 -6
- package/dist/utils/roles/helpers.js +9 -2
- package/dist/utils/roles/machines/commonValidators.d.ts.map +1 -1
- package/dist/utils/roles/machines/commonValidators.js +10 -6
- package/dist/utils/roles/machines/read/B/validators.d.ts +4 -0
- package/dist/utils/roles/machines/read/B/validators.d.ts.map +1 -0
- package/dist/utils/roles/machines/read/B/validators.js +8 -0
- package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/C/index.js +10 -7
- package/dist/utils/roles/machines/read/C/validators.d.ts +5 -0
- package/dist/utils/roles/machines/read/C/validators.d.ts.map +1 -0
- package/dist/utils/roles/machines/read/C/validators.js +29 -0
- package/dist/utils/roles/machines/read/D/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/D/index.js +13 -11
- package/dist/utils/rules.d.ts +1 -1
- package/dist/utils/rules.d.ts.map +1 -1
- package/dist/utils/rules.js +26 -17
- package/jest.config.ts +2 -12
- package/jest.setup.ts +28 -0
- package/package.json +1 -1
- package/src/auth/controller.ts +3 -0
- package/src/auth/providers/custom-function/controller.ts +5 -2
- package/src/auth/providers/local-userpass/controller.ts +13 -10
- package/src/auth/utils.ts +6 -3
- package/src/constants.ts +7 -2
- package/src/fastify.d.ts +32 -15
- package/src/features/functions/controller.ts +36 -2
- package/src/features/rules/utils.ts +11 -2
- package/src/features/triggers/utils.ts +59 -2
- package/src/index.ts +21 -8
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
- package/src/services/mongodb-atlas/index.ts +143 -90
- package/src/services/mongodb-atlas/utils.ts +158 -22
- package/src/shared/handleUserRegistration.ts +3 -3
- package/src/shared/models/handleUserRegistration.model.ts +8 -3
- package/src/types/fastify-raw-body.d.ts +22 -0
- package/src/utils/__tests__/STEP_B_STATES.test.ts +1 -1
- package/src/utils/__tests__/STEP_C_STATES.test.ts +1 -1
- package/src/utils/__tests__/STEP_D_STATES.test.ts +2 -2
- package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +9 -4
- package/src/utils/__tests__/registerPlugins.test.ts +16 -1
- package/src/utils/context/helpers.ts +3 -0
- package/src/utils/context/index.ts +1 -0
- package/src/utils/initializer/exposeRoutes.ts +15 -8
- package/src/utils/initializer/registerPlugins.ts +15 -7
- package/src/utils/roles/helpers.ts +20 -3
- package/src/utils/roles/machines/commonValidators.ts +10 -5
- package/src/utils/roles/machines/read/B/validators.ts +8 -0
- package/src/utils/roles/machines/read/C/index.ts +11 -7
- package/src/utils/roles/machines/read/C/validators.ts +21 -0
- package/src/utils/roles/machines/read/D/index.ts +22 -12
- package/src/utils/rules.ts +31 -22
- package/tsconfig.spec.json +7 -0
|
@@ -12,6 +12,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.STEP_D_STATES = void 0;
|
|
13
13
|
const utils_1 = require("../../utils");
|
|
14
14
|
const validators_1 = require("./validators");
|
|
15
|
+
const runCheckIsValidFieldName = (_a) => __awaiter(void 0, [_a], void 0, function* ({ context, endValidation }) {
|
|
16
|
+
(0, utils_1.logMachineInfo)({
|
|
17
|
+
enabled: context.enableLog,
|
|
18
|
+
machine: 'D',
|
|
19
|
+
step: 2,
|
|
20
|
+
stepName: 'checkIsValidFieldName'
|
|
21
|
+
});
|
|
22
|
+
const document = (0, validators_1.checkIsValidFieldNameFn)(context);
|
|
23
|
+
return endValidation({ success: !!Object.keys(document).length, document });
|
|
24
|
+
});
|
|
15
25
|
exports.STEP_D_STATES = {
|
|
16
26
|
checkAdditionalFields: (_a) => __awaiter(void 0, [_a], void 0, function* ({ context, next, endValidation }) {
|
|
17
27
|
(0, utils_1.logMachineInfo)({
|
|
@@ -21,16 +31,8 @@ exports.STEP_D_STATES = {
|
|
|
21
31
|
stepName: 'checkAdditionalFields'
|
|
22
32
|
});
|
|
23
33
|
const check = (0, validators_1.checkAdditionalFieldsFn)(context);
|
|
24
|
-
return check ? next('
|
|
34
|
+
return check ? next('evaluateRead') : endValidation({ success: false });
|
|
25
35
|
}),
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
enabled: context.enableLog,
|
|
29
|
-
machine: 'D',
|
|
30
|
-
step: 2,
|
|
31
|
-
stepName: 'checkIsValidFieldName'
|
|
32
|
-
});
|
|
33
|
-
const document = (0, validators_1.checkIsValidFieldNameFn)(context);
|
|
34
|
-
return endValidation({ success: !!Object.keys(document).length, document });
|
|
35
|
-
})
|
|
36
|
+
evaluateRead: runCheckIsValidFieldName,
|
|
37
|
+
checkIsValidFieldName: runCheckIsValidFieldName
|
|
36
38
|
};
|
package/dist/utils/rules.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function expandQuery(template: Record<string, unknown>, objs: Record<string, unknown>):
|
|
1
|
+
export declare function expandQuery(template: Record<string, unknown>, objs: Record<string, unknown>): Record<string, unknown>;
|
|
2
2
|
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/utils/rules.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/utils/rules.ts"],"names":[],"mappings":"AAkCA,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAES,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAC9D"}
|
package/dist/utils/rules.js
CHANGED
|
@@ -5,23 +5,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.expandQuery = expandQuery;
|
|
7
7
|
const get_1 = __importDefault(require("lodash/get"));
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const resolvePlaceholder = (value, objs) => {
|
|
9
|
+
if (!value.startsWith('%%'))
|
|
10
|
+
return value;
|
|
11
|
+
const path = value.slice(2);
|
|
12
|
+
const [rootKey, ...rest] = path.split('.');
|
|
13
|
+
const rootToken = `%%${rootKey}`;
|
|
14
|
+
const rootValue = objs[rootToken];
|
|
15
|
+
if (!rest.length) {
|
|
16
|
+
return rootValue === undefined ? value : rootValue;
|
|
17
|
+
}
|
|
18
|
+
const resolved = (0, get_1.default)(rootValue, rest.join('.'));
|
|
19
|
+
return resolved === undefined ? value : resolved;
|
|
10
20
|
};
|
|
11
|
-
|
|
21
|
+
const expandValue = (input, objs) => {
|
|
22
|
+
if (Array.isArray(input)) {
|
|
23
|
+
return input.map((item) => expandValue(item, objs));
|
|
24
|
+
}
|
|
25
|
+
if (input && typeof input === 'object') {
|
|
26
|
+
return Object.fromEntries(Object.entries(input).map(([key, val]) => [key, expandValue(val, objs)]));
|
|
27
|
+
}
|
|
28
|
+
if (typeof input === 'string') {
|
|
29
|
+
return resolvePlaceholder(input, objs);
|
|
30
|
+
}
|
|
31
|
+
return input;
|
|
32
|
+
};
|
|
33
|
+
// Espande dinamicamente i placeholder con supporto per array e percorsi annidati
|
|
12
34
|
function expandQuery(template, objs) {
|
|
13
|
-
|
|
14
|
-
const regex = /:\s*"%%([a-zA-Z0-9_.]+)"/g;
|
|
15
|
-
Object.keys(objs).forEach(() => {
|
|
16
|
-
// Espandi tutti i placeholder %%values.<nested.property>
|
|
17
|
-
const callback = (match, path) => {
|
|
18
|
-
const value = (0, get_1.default)(objs, `%%${path}`); // Recupera il valore annidato da values
|
|
19
|
-
const finalValue = typeof value === 'string' ? `"${value}"` : value && JSON.stringify(value);
|
|
20
|
-
// TODO tolto i primi : creava questo tipo di oggetto {"userId"::"%%user.id"}
|
|
21
|
-
const val = `:${value !== undefined ? finalValue : match}`; // Sostituisci se esiste, altrimenti lascia il placeholder
|
|
22
|
-
return removeExtraColons(val);
|
|
23
|
-
};
|
|
24
|
-
expandedQuery = expandedQuery.replace(regex, callback);
|
|
25
|
-
});
|
|
26
|
-
return JSON.parse(expandedQuery); // Converti la stringa JSON di nuovo in un oggetto
|
|
35
|
+
return expandValue(template, objs);
|
|
27
36
|
}
|
package/jest.config.ts
CHANGED
|
@@ -4,21 +4,11 @@ module.exports = {
|
|
|
4
4
|
'^.+\\.[tj]s$': [
|
|
5
5
|
'ts-jest',
|
|
6
6
|
{
|
|
7
|
-
tsconfig: '
|
|
7
|
+
tsconfig: '<rootDir>/tsconfig.json'
|
|
8
8
|
}
|
|
9
9
|
]
|
|
10
10
|
},
|
|
11
|
-
|
|
12
|
-
collectCoverageFrom: ['./**/*.ts'],
|
|
13
|
-
coverageDirectory: 'coverage',
|
|
14
|
-
coverageThreshold: {
|
|
15
|
-
global: {
|
|
16
|
-
branches: 50,
|
|
17
|
-
functions: 90,
|
|
18
|
-
lines: 90,
|
|
19
|
-
statements: 90
|
|
20
|
-
}
|
|
21
|
-
},
|
|
11
|
+
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
|
22
12
|
testEnvironment: 'node',
|
|
23
13
|
testMatch: ['./**/*.test.ts']
|
|
24
14
|
}
|
package/jest.setup.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Blob as NodeBlob } from 'buffer'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
if (!process.env.FLOWERBASE_APP_PATH) {
|
|
5
|
+
process.env.FLOWERBASE_APP_PATH = path.resolve(__dirname, '../../tests/e2e/app')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const BaseBlob = typeof globalThis.Blob !== 'undefined' ? globalThis.Blob : NodeBlob
|
|
9
|
+
|
|
10
|
+
type PolyfillFilePropertyBag = FilePropertyBag & {
|
|
11
|
+
name?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class FilePolyfill extends BaseBlob {
|
|
15
|
+
lastModified: number
|
|
16
|
+
name: string
|
|
17
|
+
|
|
18
|
+
constructor(bits?: Iterable<BlobPart>, options?: FilePropertyBag) {
|
|
19
|
+
super(bits, options as FilePropertyBag)
|
|
20
|
+
const fileOptions = options as PolyfillFilePropertyBag
|
|
21
|
+
this.name = fileOptions?.name ?? ''
|
|
22
|
+
this.lastModified = fileOptions?.lastModified ?? Date.now()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof globalThis.File === 'undefined') {
|
|
27
|
+
globalThis.File = FilePolyfill as unknown as typeof File
|
|
28
|
+
}
|
package/package.json
CHANGED
package/src/auth/controller.ts
CHANGED
|
@@ -26,6 +26,9 @@ export async function authController(app: FastifyInstance) {
|
|
|
26
26
|
* @returns {Promise<Object>} A promise resolving with the user's profile data.
|
|
27
27
|
*/
|
|
28
28
|
app.get(AUTH_ENDPOINTS.PROFILE, async function (req) {
|
|
29
|
+
if (req.user.typ !== 'access') {
|
|
30
|
+
throw new Error('Access token required')
|
|
31
|
+
}
|
|
29
32
|
const user = await db
|
|
30
33
|
.collection<Record<string, unknown>>(authCollection)
|
|
31
34
|
.findOne({ _id: ObjectId.createFromHexString(req.user.id) })
|
|
@@ -77,18 +77,21 @@ export async function customFunctionController(app: FastifyInstance) {
|
|
|
77
77
|
|
|
78
78
|
if (res.id) {
|
|
79
79
|
const user = await handleUserRegistration(app, { run_as_system: true, skipUserCheck: true, provider: PROVIDER.CUSTOM_FUNCTION })({ email: res.id, password: generatePassword() })
|
|
80
|
+
if (!user?.insertedId) {
|
|
81
|
+
throw new Error('Failed to register custom user')
|
|
82
|
+
}
|
|
80
83
|
|
|
81
84
|
const currentUserData = {
|
|
82
85
|
_id: user.insertedId,
|
|
83
86
|
user_data: {
|
|
84
|
-
_id: user.insertedId
|
|
87
|
+
_id: user.insertedId
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
return {
|
|
88
91
|
access_token: this.createAccessToken(currentUserData),
|
|
89
92
|
refresh_token: this.createRefreshToken(currentUserData),
|
|
90
93
|
device_id: '',
|
|
91
|
-
user_id: user.insertedId.toString()
|
|
94
|
+
user_id: user.insertedId.toString()
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -55,8 +55,13 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
55
55
|
|
|
56
56
|
const result = await handleUserRegistration(app, { run_as_system: true, provider: PROVIDER.LOCAL_USERPASS })({ email: req.body.email.toLowerCase(), password: req.body.password })
|
|
57
57
|
|
|
58
|
+
if (!result?.insertedId) {
|
|
59
|
+
res?.status(500)
|
|
60
|
+
throw new Error('Failed to register user')
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
res?.status(201)
|
|
59
|
-
return { userId: result
|
|
64
|
+
return { userId: result.insertedId.toString() }
|
|
60
65
|
}
|
|
61
66
|
)
|
|
62
67
|
|
|
@@ -98,7 +103,12 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
98
103
|
: {}
|
|
99
104
|
delete authUser?.password
|
|
100
105
|
|
|
101
|
-
const userWithCustomData = {
|
|
106
|
+
const userWithCustomData = {
|
|
107
|
+
...authUser,
|
|
108
|
+
user_data: { ...(user || {}), _id: authUser._id },
|
|
109
|
+
data: { email: authUser.email },
|
|
110
|
+
id: authUser._id.toString()
|
|
111
|
+
}
|
|
102
112
|
|
|
103
113
|
if (authUser && authUser.status === 'pending') {
|
|
104
114
|
try {
|
|
@@ -123,14 +133,7 @@ export async function localUserPassController(app: FastifyInstance) {
|
|
|
123
133
|
) {
|
|
124
134
|
try {
|
|
125
135
|
await GenerateContext({
|
|
126
|
-
args: [
|
|
127
|
-
{
|
|
128
|
-
operationType: 'CREATE',
|
|
129
|
-
providers: 'local-userpass',
|
|
130
|
-
user: userWithCustomData,
|
|
131
|
-
time: new Date().getTime()
|
|
132
|
-
}
|
|
133
|
-
],
|
|
136
|
+
args: [userWithCustomData],
|
|
134
137
|
app,
|
|
135
138
|
rules: {},
|
|
136
139
|
user: userWithCustomData,
|
package/src/auth/utils.ts
CHANGED
|
@@ -115,13 +115,16 @@ export interface CustomUserDataConfig {
|
|
|
115
115
|
on_user_creation_function_name: string
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
const resolveAppPath = () =>
|
|
119
|
+
process.env.FLOWERBASE_APP_PATH ?? require.main?.path ?? process.cwd()
|
|
120
|
+
|
|
118
121
|
|
|
119
122
|
/**
|
|
120
123
|
* > Loads the auth config json file
|
|
121
124
|
* @testable
|
|
122
125
|
*/
|
|
123
126
|
export const loadAuthConfig = (): AuthConfig => {
|
|
124
|
-
const authPath = path.join(
|
|
127
|
+
const authPath = path.join(resolveAppPath(), 'auth/providers.json')
|
|
125
128
|
return JSON.parse(fs.readFileSync(authPath, 'utf-8'))
|
|
126
129
|
}
|
|
127
130
|
|
|
@@ -130,7 +133,7 @@ export const loadAuthConfig = (): AuthConfig => {
|
|
|
130
133
|
* @testable
|
|
131
134
|
*/
|
|
132
135
|
export const loadCustomUserData = (): CustomUserDataConfig => {
|
|
133
|
-
const userDataPath = path.join(
|
|
136
|
+
const userDataPath = path.join(resolveAppPath(), 'auth/custom_user_data.json')
|
|
134
137
|
return JSON.parse(fs.readFileSync(userDataPath, 'utf-8'))
|
|
135
138
|
}
|
|
136
139
|
|
|
@@ -205,4 +208,4 @@ export const getMailConfig = (
|
|
|
205
208
|
export const generatePassword = (length = 20) => {
|
|
206
209
|
const bytes = crypto.randomBytes(length);
|
|
207
210
|
return Array.from(bytes, (b) => CHARSET[b % CHARSET.length]).join("");
|
|
208
|
-
}
|
|
211
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadAuthConfig, loadCustomUserData } from './auth/utils'
|
|
2
|
+
import { ALLOWED_METHODS } from './'
|
|
2
3
|
|
|
3
4
|
const {
|
|
4
5
|
database_name,
|
|
@@ -15,7 +16,11 @@ export const DEFAULT_CONFIG = {
|
|
|
15
16
|
API_VERSION: process.env.API_VERSION || 'v2.0',
|
|
16
17
|
HTTPS_SCHEMA: process.env.HTTPS_SCHEMA || 'https',
|
|
17
18
|
HOST: process.env.HOST || '0.0.0.0',
|
|
18
|
-
ENABLE_LOGGER: process.env.ENABLE_LOGGER
|
|
19
|
+
ENABLE_LOGGER: process.env.ENABLE_LOGGER,
|
|
20
|
+
CORS_OPTIONS: {
|
|
21
|
+
origin: "*",
|
|
22
|
+
methods: ["GET", "POST", "PUT", "DELETE"] as ALLOWED_METHODS[]
|
|
23
|
+
}
|
|
19
24
|
}
|
|
20
25
|
export const API_VERSION = `/api/client/${DEFAULT_CONFIG.API_VERSION}`
|
|
21
26
|
export const HTTPS_SCHEMA = DEFAULT_CONFIG.HTTPS_SCHEMA
|
|
@@ -39,4 +44,4 @@ export const AUTH_CONFIG = {
|
|
|
39
44
|
export const S3_CONFIG = {
|
|
40
45
|
ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
|
|
41
46
|
SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY
|
|
42
|
-
}
|
|
47
|
+
}
|
package/src/fastify.d.ts
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
|
-
import { FastifyRequest as FastifyRequestType } from 'fastify'
|
|
1
|
+
import { FastifyRequest as FastifyRequestType, FastifyReply } from 'fastify'
|
|
2
2
|
|
|
3
3
|
type User = Record<string, unknown>
|
|
4
4
|
type UserData = Record<string, unknown>
|
|
5
|
+
type JwtUserData = UserData
|
|
6
|
+
|
|
7
|
+
type JwtAccessPayload = {
|
|
8
|
+
typ: 'access'
|
|
9
|
+
id: string
|
|
10
|
+
user_data: JwtUserData
|
|
11
|
+
data: JwtUserData
|
|
12
|
+
custom_data?: JwtUserData
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type JwtRefreshPayload = {
|
|
16
|
+
typ: 'refresh'
|
|
17
|
+
baas_id: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type JwtPayload = JwtAccessPayload | JwtRefreshPayload
|
|
21
|
+
|
|
22
|
+
type JwtAccessUser = JwtAccessPayload & {
|
|
23
|
+
sub: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type JwtRefreshUser = JwtRefreshPayload & {
|
|
27
|
+
sub: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type JwtUser = JwtAccessUser | JwtRefreshUser
|
|
5
31
|
|
|
6
32
|
declare module 'fastify' {
|
|
7
33
|
interface FastifyInstance {
|
|
@@ -9,20 +35,11 @@ declare module 'fastify' {
|
|
|
9
35
|
createAccessToken(user: User): string
|
|
10
36
|
createRefreshToken(user: User): string
|
|
11
37
|
}
|
|
38
|
+
}
|
|
12
39
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
typ: 'refresh'
|
|
18
|
-
sub: string
|
|
19
|
-
user_data: UserData
|
|
20
|
-
user: User
|
|
21
|
-
}
|
|
22
|
-
| {
|
|
23
|
-
typ: 'access'
|
|
24
|
-
user_data: UserData
|
|
25
|
-
id: string
|
|
26
|
-
}
|
|
40
|
+
declare module '@fastify/jwt' {
|
|
41
|
+
interface FastifyJWT {
|
|
42
|
+
payload: JwtPayload
|
|
43
|
+
user: JwtUser
|
|
27
44
|
}
|
|
28
45
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ObjectId } from 'bson'
|
|
2
2
|
import { ChangeStream, Document } from 'mongodb';
|
|
3
|
+
import type { FastifyRequest } from 'fastify'
|
|
3
4
|
import { services } from '../../services'
|
|
4
5
|
import { StateManager } from '../../state'
|
|
5
6
|
import { GenerateContext } from '../../utils/context'
|
|
@@ -7,7 +8,31 @@ import { Base64Function, FunctionCallBase64Dto, FunctionCallDto } from './dtos'
|
|
|
7
8
|
import { FunctionController } from './interface'
|
|
8
9
|
import { executeQuery } from './utils'
|
|
9
10
|
|
|
11
|
+
const normalizeUser = (payload: Record<string, any> | undefined) => {
|
|
12
|
+
if (!payload) return undefined
|
|
13
|
+
const nestedUser =
|
|
14
|
+
payload.data ?? payload.user_data ?? payload.custom_data ?? payload
|
|
15
|
+
const flattened =
|
|
16
|
+
typeof nestedUser === 'object' && nestedUser !== null ? nestedUser : {}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
...payload,
|
|
20
|
+
...flattened,
|
|
21
|
+
custom_data: payload.custom_data ?? flattened,
|
|
22
|
+
user_data: payload.user_data ?? flattened,
|
|
23
|
+
data: payload.data ?? flattened
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getRequestUser = (req: FastifyRequest) => {
|
|
28
|
+
const candidate = req.user as Record<string, any> | undefined
|
|
29
|
+
return normalizeUser(candidate)
|
|
30
|
+
}
|
|
10
31
|
|
|
32
|
+
const logFunctionCall = (method: string, user: Record<string, any> | undefined, args: unknown[]) => {
|
|
33
|
+
if (process.env.DEBUG_FUNCTIONS !== 'true') return
|
|
34
|
+
console.log('[functions-debug]', method, user ? { id: user.id, role: user.role, email: user.email } : 'no-user', args)
|
|
35
|
+
}
|
|
11
36
|
|
|
12
37
|
/**
|
|
13
38
|
* > Creates a pre handler for every query
|
|
@@ -24,7 +49,10 @@ export const functionsController: FunctionController = async (
|
|
|
24
49
|
const streams = {} as Record<string, ChangeStream<Document, Document>>
|
|
25
50
|
|
|
26
51
|
app.post<{ Body: FunctionCallDto }>('/call', async (req, res) => {
|
|
27
|
-
const
|
|
52
|
+
const user = getRequestUser(req)
|
|
53
|
+
if (!user || user.typ !== 'access') {
|
|
54
|
+
throw new Error('Access token required')
|
|
55
|
+
}
|
|
28
56
|
const { name: method, arguments: args } = req.body
|
|
29
57
|
|
|
30
58
|
if ('service' in req.body) {
|
|
@@ -39,6 +67,7 @@ export const functionsController: FunctionController = async (
|
|
|
39
67
|
.db(database)
|
|
40
68
|
.collection(collection)[method]
|
|
41
69
|
|
|
70
|
+
logFunctionCall(`service:${req.body.service}:${method}`, user, args)
|
|
42
71
|
const operatorsByType = await executeQuery({
|
|
43
72
|
currentMethod,
|
|
44
73
|
query,
|
|
@@ -61,6 +90,7 @@ export const functionsController: FunctionController = async (
|
|
|
61
90
|
throw new Error(`Function "${req.body.name}" is private`)
|
|
62
91
|
}
|
|
63
92
|
|
|
93
|
+
logFunctionCall(`function:${method}`, user, args)
|
|
64
94
|
const result = await GenerateContext({
|
|
65
95
|
args: req.body.arguments,
|
|
66
96
|
app,
|
|
@@ -76,7 +106,11 @@ export const functionsController: FunctionController = async (
|
|
|
76
106
|
app.get<{
|
|
77
107
|
Querystring: FunctionCallBase64Dto
|
|
78
108
|
}>('/call', async (req, res) => {
|
|
79
|
-
const { query
|
|
109
|
+
const { query } = req
|
|
110
|
+
const user = getRequestUser(req)
|
|
111
|
+
if (!user || user.typ !== 'access') {
|
|
112
|
+
throw new Error('Access token required')
|
|
113
|
+
}
|
|
80
114
|
const { baas_request, stitch_request } = query
|
|
81
115
|
|
|
82
116
|
const config: Base64Function = JSON.parse(
|
|
@@ -5,11 +5,20 @@ import { Rules, RulesConfig } from './interface'
|
|
|
5
5
|
|
|
6
6
|
export const loadRules = async (rootDir = process.cwd()): Promise<Rules> => {
|
|
7
7
|
const rulesRoot = path.join(rootDir, 'data_sources', 'mongodb-atlas')
|
|
8
|
-
const
|
|
8
|
+
const recursivelyCollectFiles = (dir: string): string[] => {
|
|
9
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
10
|
+
const fullPath = path.join(dir, entry.name)
|
|
11
|
+
if (entry.isDirectory()) {
|
|
12
|
+
return recursivelyCollectFiles(fullPath)
|
|
13
|
+
}
|
|
14
|
+
return entry.isFile() ? [fullPath] : []
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
const files = recursivelyCollectFiles(rulesRoot)
|
|
9
18
|
const rulesFiles = files.filter((x) => (x as string).endsWith('rules.json'))
|
|
10
19
|
|
|
11
20
|
const rulesByCollection = rulesFiles.reduce((acc, rulesFile) => {
|
|
12
|
-
const filePath =
|
|
21
|
+
const filePath = rulesFile
|
|
13
22
|
const collectionRules = readJsonContent(filePath) as RulesConfig
|
|
14
23
|
acc[collectionRules.collection] = collectionRules
|
|
15
24
|
|
|
@@ -7,6 +7,41 @@ import { readJsonContent } from '../../utils'
|
|
|
7
7
|
import { GenerateContext } from '../../utils/context'
|
|
8
8
|
import { HandlerParams, Trigger, Triggers } from './interface'
|
|
9
9
|
|
|
10
|
+
const registerOnClose = (
|
|
11
|
+
app: HandlerParams['app'],
|
|
12
|
+
handler: () => Promise<void> | void,
|
|
13
|
+
label: string
|
|
14
|
+
) => {
|
|
15
|
+
if (app.server) {
|
|
16
|
+
app.server.once('close', () => {
|
|
17
|
+
Promise.resolve(handler()).catch((error) => {
|
|
18
|
+
console.error(`${label} close error`, error)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
app.addHook('onClose', async () => {
|
|
26
|
+
try {
|
|
27
|
+
await handler()
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`${label} close error`, error)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(`${label} hook registration error`, error)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const shouldIgnoreStreamError = (error: unknown) => {
|
|
38
|
+
const err = error as { name?: string; message?: string }
|
|
39
|
+
if (err?.name === 'MongoClientClosedError') return true
|
|
40
|
+
if (err?.message?.includes('client was closed')) return true
|
|
41
|
+
if (err?.message?.includes('Client is closed')) return true
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
10
45
|
/**
|
|
11
46
|
* Loads trigger files from the specified directory and returns them as an array of objects.
|
|
12
47
|
* Each object contains the file name and the parsed JSON content.
|
|
@@ -54,7 +89,7 @@ const handleCronTrigger = async ({
|
|
|
54
89
|
services,
|
|
55
90
|
app
|
|
56
91
|
}: HandlerParams) => {
|
|
57
|
-
cron.schedule(config.schedule, async () => {
|
|
92
|
+
const task = cron.schedule(config.schedule, async () => {
|
|
58
93
|
await GenerateContext({
|
|
59
94
|
args: [],
|
|
60
95
|
app,
|
|
@@ -65,6 +100,7 @@ const handleCronTrigger = async ({
|
|
|
65
100
|
services
|
|
66
101
|
})
|
|
67
102
|
})
|
|
103
|
+
registerOnClose(app, () => task.stop(), 'Scheduled trigger')
|
|
68
104
|
}
|
|
69
105
|
|
|
70
106
|
const handleAuthenticationTrigger = async ({
|
|
@@ -88,6 +124,10 @@ const handleAuthenticationTrigger = async ({
|
|
|
88
124
|
.watch(pipeline, {
|
|
89
125
|
fullDocument: 'whenAvailable'
|
|
90
126
|
})
|
|
127
|
+
changeStream.on('error', (error) => {
|
|
128
|
+
if (shouldIgnoreStreamError(error)) return
|
|
129
|
+
console.error('Authentication trigger change stream error', error)
|
|
130
|
+
})
|
|
91
131
|
changeStream.on('change', async function (change) {
|
|
92
132
|
const document = change['fullDocument' as keyof typeof change] as Record<
|
|
93
133
|
string,
|
|
@@ -120,6 +160,13 @@ const handleAuthenticationTrigger = async ({
|
|
|
120
160
|
})
|
|
121
161
|
}
|
|
122
162
|
})
|
|
163
|
+
registerOnClose(
|
|
164
|
+
app,
|
|
165
|
+
async () => {
|
|
166
|
+
await changeStream.close()
|
|
167
|
+
},
|
|
168
|
+
'Authentication trigger'
|
|
169
|
+
)
|
|
123
170
|
}
|
|
124
171
|
|
|
125
172
|
/**
|
|
@@ -175,6 +222,10 @@ const handleDataBaseTrigger = async ({
|
|
|
175
222
|
? 'whenAvailable'
|
|
176
223
|
: undefined
|
|
177
224
|
})
|
|
225
|
+
changeStream.on('error', (error) => {
|
|
226
|
+
if (shouldIgnoreStreamError(error)) return
|
|
227
|
+
console.error('Database trigger change stream error', error)
|
|
228
|
+
})
|
|
178
229
|
changeStream.on('change', async function ({ clusterTime, ...change }) {
|
|
179
230
|
await GenerateContext({
|
|
180
231
|
args: [change],
|
|
@@ -186,7 +237,13 @@ const handleDataBaseTrigger = async ({
|
|
|
186
237
|
services
|
|
187
238
|
})
|
|
188
239
|
})
|
|
189
|
-
|
|
240
|
+
registerOnClose(
|
|
241
|
+
app,
|
|
242
|
+
async () => {
|
|
243
|
+
await changeStream.close()
|
|
244
|
+
},
|
|
245
|
+
'Database trigger'
|
|
246
|
+
)
|
|
190
247
|
}
|
|
191
248
|
|
|
192
249
|
export const TRIGGER_HANDLERS = {
|
package/src/index.ts
CHANGED
|
@@ -14,12 +14,22 @@ import { exposeRoutes } from './utils/initializer/exposeRoutes'
|
|
|
14
14
|
import { registerPlugins } from './utils/initializer/registerPlugins'
|
|
15
15
|
export * from './model'
|
|
16
16
|
|
|
17
|
+
|
|
18
|
+
export type ALLOWED_METHODS = "GET" | "POST" | "PUT" | "DELETE"
|
|
19
|
+
|
|
20
|
+
export type CorsConfig = {
|
|
21
|
+
origin: string
|
|
22
|
+
methods: ALLOWED_METHODS[]
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
export type InitializeConfig = {
|
|
18
26
|
projectId: string
|
|
19
27
|
mongodbUrl?: string
|
|
20
28
|
jwtSecret?: string
|
|
21
29
|
port?: number
|
|
22
30
|
host?: string
|
|
31
|
+
corsConfig?: CorsConfig
|
|
32
|
+
basePath?: string
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
/**
|
|
@@ -35,25 +45,27 @@ export async function initialize({
|
|
|
35
45
|
host = DEFAULT_CONFIG.HOST,
|
|
36
46
|
jwtSecret = DEFAULT_CONFIG.JWT_SECRET,
|
|
37
47
|
port = DEFAULT_CONFIG.PORT,
|
|
38
|
-
mongodbUrl = DEFAULT_CONFIG.MONGODB_URL
|
|
48
|
+
mongodbUrl = DEFAULT_CONFIG.MONGODB_URL,
|
|
49
|
+
corsConfig = DEFAULT_CONFIG.CORS_OPTIONS,
|
|
50
|
+
basePath
|
|
39
51
|
}: InitializeConfig) {
|
|
52
|
+
const resolvedBasePath = basePath ?? require.main?.path ?? process.cwd()
|
|
40
53
|
const fastify = Fastify({
|
|
41
54
|
logger: !!DEFAULT_CONFIG.ENABLE_LOGGER
|
|
42
55
|
})
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
console.log("BASE PATH", basePath)
|
|
57
|
+
console.log("BASE PATH", resolvedBasePath)
|
|
46
58
|
|
|
47
59
|
console.log("CURRENT PORT", port)
|
|
48
60
|
console.log("CURRENT HOST", host)
|
|
49
61
|
|
|
50
|
-
const functionsList = await loadFunctions(
|
|
62
|
+
const functionsList = await loadFunctions(resolvedBasePath)
|
|
51
63
|
console.log("Functions LOADED")
|
|
52
|
-
const triggersList = await loadTriggers(
|
|
64
|
+
const triggersList = await loadTriggers(resolvedBasePath)
|
|
53
65
|
console.log("Triggers LOADED")
|
|
54
|
-
const endpointsList = await loadEndpoints(
|
|
66
|
+
const endpointsList = await loadEndpoints(resolvedBasePath)
|
|
55
67
|
console.log("Endpoints LOADED")
|
|
56
|
-
const rulesList = await loadRules(
|
|
68
|
+
const rulesList = await loadRules(resolvedBasePath)
|
|
57
69
|
console.log("Rules LOADED")
|
|
58
70
|
|
|
59
71
|
const stateConfig = {
|
|
@@ -91,7 +103,8 @@ export async function initialize({
|
|
|
91
103
|
register: fastify.register,
|
|
92
104
|
mongodbUrl,
|
|
93
105
|
jwtSecret,
|
|
94
|
-
functionsList
|
|
106
|
+
functionsList,
|
|
107
|
+
corsConfig
|
|
95
108
|
})
|
|
96
109
|
|
|
97
110
|
console.log('Plugins registration COMPLETED')
|