@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.
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/auth/controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AASzC;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,eAAe,iBAsLxD"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/auth/controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAgBzC;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,eAAe,iBA0LxD"}
@@ -16,6 +16,12 @@ const state_1 = require("../state");
16
16
  const crypto_1 = require("../utils/crypto");
17
17
  const utils_1 = require("./utils");
18
18
  const HANDLER_TYPE = 'preHandler';
19
+ const unauthorizedSessionError = {
20
+ message: 'Unauthorized',
21
+ error: 'unauthorized',
22
+ errorCode: 'InvalidSession',
23
+ error_code: 'InvalidSession'
24
+ };
19
25
  /**
20
26
  * Controller for handling user authentication, profile retrieval, and session management.
21
27
  * @testable
@@ -96,11 +102,13 @@ function authController(app) {
96
102
  }, function (req, res) {
97
103
  return __awaiter(this, void 0, void 0, function* () {
98
104
  if (req.user.typ !== 'refresh') {
99
- throw new Error(utils_1.AUTH_ERRORS.INVALID_TOKEN);
105
+ res.code(401).send(unauthorizedSessionError);
106
+ return;
100
107
  }
101
108
  const authHeader = req.headers.authorization;
102
109
  if (!(authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith('Bearer '))) {
103
- throw new Error(utils_1.AUTH_ERRORS.INVALID_TOKEN);
110
+ res.code(401).send(unauthorizedSessionError);
111
+ return;
104
112
  }
105
113
  const refreshToken = authHeader.slice('Bearer '.length).trim();
106
114
  const refreshTokenHash = (0, crypto_1.hashToken)(refreshToken);
@@ -110,11 +118,13 @@ function authController(app) {
110
118
  expiresAt: { $gt: new Date() }
111
119
  });
112
120
  if (!storedToken) {
113
- throw new Error(utils_1.AUTH_ERRORS.INVALID_TOKEN);
121
+ res.code(401).send(unauthorizedSessionError);
122
+ return;
114
123
  }
115
124
  const auth_user = yield (db === null || db === void 0 ? void 0 : db.collection(authCollection).findOne({ _id: new this.mongo.ObjectId(req.user.sub) }));
116
125
  if (!auth_user) {
117
- throw new Error(`User with ID ${req.user.sub} not found`);
126
+ res.code(401).send(unauthorizedSessionError);
127
+ return;
118
128
  }
119
129
  const user = userCollection && constants_1.AUTH_CONFIG.user_id_field
120
130
  ? (yield db.collection(userCollection).findOne({ [constants_1.AUTH_CONFIG.user_id_field]: req.user.sub }))
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/auth/plugins/jwt.ts"],"names":[],"mappings":"AAKA,KAAK,OAAO,GAAG;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAkBD;;;;;;;GAOG;iUAC8C,OAAO;AAAxD,wBA+GE"}
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/auth/plugins/jwt.ts"],"names":[],"mappings":"AAKA,KAAK,OAAO,GAAG;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAkBD;;;;;;;GAOG;iUAC8C,OAAO;AAAxD,wBA4GE"}
@@ -95,15 +95,18 @@ exports.default = (0, fastify_plugin_1.default)(function (fastify, opts) {
95
95
  });
96
96
  fastify.decorate('createAccessToken', function (user) {
97
97
  const id = user._id.toString();
98
- const userData = isRecord(user.user_data) ? Object.assign({}, user.user_data) : {};
99
98
  const customData = isRecord(user.custom_data)
100
- ? Object.assign({}, user.custom_data) : Object.assign({}, userData);
101
- const mergedUserData = Object.assign(Object.assign(Object.assign({}, customData), userData), { _id: id, id, email: typeof user.email === 'string' ? user.email : userData.email });
99
+ ? Object.assign({}, user.custom_data) : (isRecord(user.user_data) ? Object.assign({}, user.user_data) : {});
100
+ const authData = {
101
+ _id: id,
102
+ id,
103
+ email: typeof user.email === 'string' ? user.email : customData.email
104
+ };
102
105
  return this.jwt.sign({
103
106
  typ: 'access',
104
107
  id,
105
- data: mergedUserData,
106
- user_data: mergedUserData,
108
+ data: authData,
109
+ user_data: customData,
107
110
  custom_data: customData
108
111
  }, {
109
112
  iss: BAAS_ID,
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../src/features/functions/controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AA8ChD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,kBAqKjC,CAAA"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../src/features/functions/controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAoFhD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,kBAqPjC,CAAA"}
@@ -8,11 +8,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
11
22
  Object.defineProperty(exports, "__esModule", { value: true });
12
23
  exports.functionsController = void 0;
13
24
  const bson_1 = require("bson");
14
25
  const services_1 = require("../../services");
15
- const state_1 = require("../../state");
16
26
  const context_1 = require("../../utils/context");
17
27
  const utils_1 = require("./utils");
18
28
  const normalizeUser = (payload) => {
@@ -47,6 +57,20 @@ const isReturnedError = (value) => {
47
57
  return typeof candidate.message === 'string' && typeof candidate.name === 'string';
48
58
  };
49
59
  const serializeEjson = (value) => JSON.stringify(bson_1.EJSON.serialize(value, { relaxed: false }));
60
+ const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
61
+ const sharedWatchStreams = new Map();
62
+ let watchSubscriberCounter = 0;
63
+ const parseWatchFilter = (args) => {
64
+ var _a;
65
+ if (!isRecord(args))
66
+ return undefined;
67
+ const candidate = (_a = (isRecord(args.filter) ? args.filter : undefined)) !== null && _a !== void 0 ? _a : (isRecord(args.query) ? args.query : undefined);
68
+ return candidate ? candidate : undefined;
69
+ };
70
+ const isReadableDocumentResult = (value) => !!value &&
71
+ typeof value === 'object' &&
72
+ !Array.isArray(value) &&
73
+ Object.keys(value).length > 0;
50
74
  /**
51
75
  * > Creates a pre handler for every query
52
76
  * @param app -> the fastify instance
@@ -55,7 +79,6 @@ const serializeEjson = (value) => JSON.stringify(bson_1.EJSON.serialize(value, {
55
79
  */
56
80
  const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0, function* (app, { functionsList, rules }) {
57
81
  app.addHook('preHandler', app.jwtAuthentication);
58
- const streams = {};
59
82
  app.post('/call', {
60
83
  schema: {
61
84
  tags: ['Functions']
@@ -140,10 +163,9 @@ const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0
140
163
  throw new Error('Access token required');
141
164
  }
142
165
  const { baas_request, stitch_request } = query;
143
- const config = JSON.parse(Buffer.from(baas_request || stitch_request || '', 'base64').toString('utf8'));
144
- const [{ database, collection }] = config.arguments;
145
- const app = state_1.StateManager.select('app');
146
- const services = state_1.StateManager.select('services');
166
+ const decodedConfig = JSON.parse(Buffer.from(baas_request || stitch_request || '', 'base64').toString('utf8'));
167
+ const config = bson_1.EJSON.deserialize(decodedConfig);
168
+ const [_a] = config.arguments, { database, collection } = _a, watchArgs = __rest(_a, ["database", "collection"]);
147
169
  const headers = {
148
170
  'Content-Type': 'text/event-stream',
149
171
  'Cache-Control': 'no-cache',
@@ -154,31 +176,107 @@ const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0
154
176
  };
155
177
  res.raw.writeHead(200, headers);
156
178
  res.raw.flushHeaders();
157
- const requestKey = baas_request || stitch_request;
158
- if (!requestKey)
159
- return;
160
- const changeStream = streams[requestKey];
161
- if (changeStream) {
162
- changeStream.on('change', (change) => {
163
- res.raw.write(`data: ${serializeEjson(change)}\n\n`);
179
+ const streamKey = `${database}::${collection}`;
180
+ const subscriberId = `${Date.now()}-${watchSubscriberCounter++}`;
181
+ const extraFilter = parseWatchFilter(watchArgs);
182
+ const mongoClient = app.mongo.client;
183
+ let hub = sharedWatchStreams.get(streamKey);
184
+ if (!hub) {
185
+ const stream = mongoClient.db(database).collection(collection).watch([], {
186
+ fullDocument: 'whenAvailable'
164
187
  });
165
- req.raw.on('close', () => {
166
- var _a;
167
- console.log("change stream closed");
168
- (_a = changeStream === null || changeStream === void 0 ? void 0 : changeStream.close) === null || _a === void 0 ? void 0 : _a.call(changeStream);
169
- delete streams[requestKey];
170
- });
171
- return;
188
+ hub = {
189
+ database,
190
+ collection,
191
+ stream,
192
+ subscribers: new Map()
193
+ };
194
+ sharedWatchStreams.set(streamKey, hub);
172
195
  }
173
- streams[requestKey] = yield services['mongodb-atlas'](app, {
196
+ const ensureHubListeners = (currentHub) => {
197
+ if (currentHub.listenersBound) {
198
+ return;
199
+ }
200
+ const closeHub = () => __awaiter(void 0, void 0, void 0, function* () {
201
+ currentHub.stream.off('change', onHubChange);
202
+ currentHub.stream.off('error', onHubError);
203
+ sharedWatchStreams.delete(streamKey);
204
+ try {
205
+ yield currentHub.stream.close();
206
+ }
207
+ catch (_a) {
208
+ // Ignore stream close errors.
209
+ }
210
+ });
211
+ const onHubChange = (change) => __awaiter(void 0, void 0, void 0, function* () {
212
+ const subscribers = Array.from(currentHub.subscribers.values());
213
+ yield Promise.all(subscribers.map((subscriber) => __awaiter(void 0, void 0, void 0, function* () {
214
+ var _a, _b, _c;
215
+ const subscriberRes = subscriber.response;
216
+ if (subscriberRes.writableEnded || subscriberRes.destroyed) {
217
+ currentHub.subscribers.delete(subscriber.id);
218
+ return;
219
+ }
220
+ const docId = (_b = (_a = change === null || change === void 0 ? void 0 : change.documentKey) === null || _a === void 0 ? void 0 : _a._id) !== null && _b !== void 0 ? _b : (_c = change === null || change === void 0 ? void 0 : change.fullDocument) === null || _c === void 0 ? void 0 : _c._id;
221
+ if (typeof docId === 'undefined')
222
+ return;
223
+ const readQuery = subscriber.extraFilter
224
+ ? { $and: [subscriber.extraFilter, { _id: docId }] }
225
+ : { _id: docId };
226
+ try {
227
+ const readableDoc = yield services_1.services['mongodb-atlas'](app, {
228
+ user: subscriber.user,
229
+ rules
230
+ })
231
+ .db(currentHub.database)
232
+ .collection(currentHub.collection)
233
+ .findOne(readQuery);
234
+ if (!isReadableDocumentResult(readableDoc))
235
+ return;
236
+ subscriberRes.write(`data: ${serializeEjson(change)}\n\n`);
237
+ }
238
+ catch (error) {
239
+ subscriberRes.write(`event: error\ndata: ${formatFunctionExecutionError(error)}\n\n`);
240
+ subscriberRes.end();
241
+ currentHub.subscribers.delete(subscriber.id);
242
+ }
243
+ })));
244
+ if (!currentHub.subscribers.size) {
245
+ yield closeHub();
246
+ }
247
+ });
248
+ const onHubError = (error) => __awaiter(void 0, void 0, void 0, function* () {
249
+ for (const subscriber of currentHub.subscribers.values()) {
250
+ const subscriberRes = subscriber.response;
251
+ if (!subscriberRes.writableEnded && !subscriberRes.destroyed) {
252
+ subscriberRes.write(`event: error\ndata: ${formatFunctionExecutionError(error)}\n\n`);
253
+ subscriberRes.end();
254
+ }
255
+ }
256
+ currentHub.subscribers.clear();
257
+ yield closeHub();
258
+ });
259
+ currentHub.stream.on('change', onHubChange);
260
+ currentHub.stream.on('error', onHubError);
261
+ currentHub.listenersBound = true;
262
+ };
263
+ ensureHubListeners(hub);
264
+ const subscriber = {
265
+ id: subscriberId,
174
266
  user,
175
- rules
176
- })
177
- .db(database)
178
- .collection(collection)
179
- .watch([], { fullDocument: 'whenAvailable' });
180
- streams[requestKey].on('change', (change) => {
181
- res.raw.write(`data: ${serializeEjson(change)}\n\n`);
267
+ response: res.raw,
268
+ extraFilter
269
+ };
270
+ hub.subscribers.set(subscriberId, subscriber);
271
+ req.raw.on('close', () => {
272
+ const currentHub = sharedWatchStreams.get(streamKey);
273
+ if (!currentHub)
274
+ return;
275
+ currentHub.subscribers.delete(subscriberId);
276
+ if (!currentHub.subscribers.size) {
277
+ void currentHub.stream.close();
278
+ sharedWatchStreams.delete(streamKey);
279
+ }
182
280
  });
183
281
  }));
184
282
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/index.ts"],"names":[],"mappings":"AAuBA,OAAO,EAGL,oBAAoB,EAErB,MAAM,SAAS,CAAA;AA0uChB,QAAA,MAAM,YAAY,EAAE,oBA6BlB,CAAA;AAEF,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/index.ts"],"names":[],"mappings":"AAwBA,OAAO,EAGL,oBAAoB,EAErB,MAAM,SAAS,CAAA;AAk1ChB,QAAA,MAAM,YAAY,EAAE,oBA6BlB,CAAA;AAEF,eAAe,YAAY,CAAA"}
@@ -113,6 +113,53 @@ const normalizeFindOneAndUpdateOptions = (options) => {
113
113
  return Object.assign(Object.assign({}, rest), { returnDocument: returnNewDocument ? 'after' : 'before' });
114
114
  };
115
115
  const buildAndQuery = (clauses) => clauses.length ? { $and: clauses } : {};
116
+ const toWatchMatchFilter = (value) => {
117
+ if (Array.isArray(value)) {
118
+ return value.map((item) => toWatchMatchFilter(item));
119
+ }
120
+ if (!isPlainObject(value))
121
+ return value;
122
+ return Object.entries(value).reduce((acc, [key, current]) => {
123
+ if (key.startsWith('$')) {
124
+ acc[key] = toWatchMatchFilter(current);
125
+ return acc;
126
+ }
127
+ acc[`fullDocument.${key}`] = toWatchMatchFilter(current);
128
+ return acc;
129
+ }, {});
130
+ };
131
+ const resolveWatchArgs = (pipelineOrOptions, options) => {
132
+ var _a;
133
+ const inputPipeline = Array.isArray(pipelineOrOptions) ? pipelineOrOptions : [];
134
+ const rawOptions = (_a = (Array.isArray(pipelineOrOptions) ? options : pipelineOrOptions)) !== null && _a !== void 0 ? _a : {};
135
+ if (!isPlainObject(rawOptions)) {
136
+ return {
137
+ pipeline: inputPipeline,
138
+ options: options,
139
+ extraMatches: []
140
+ };
141
+ }
142
+ const _b = rawOptions, { filter: watchFilter, ids } = _b, watchOptions = __rest(_b, ["filter", "ids"]);
143
+ const extraMatches = [];
144
+ if (typeof watchFilter !== 'undefined') {
145
+ extraMatches.push({ $match: toWatchMatchFilter(watchFilter) });
146
+ }
147
+ if (Array.isArray(ids)) {
148
+ extraMatches.push({
149
+ $match: {
150
+ $or: [
151
+ { 'documentKey._id': { $in: ids } },
152
+ { 'fullDocument._id': { $in: ids } }
153
+ ]
154
+ }
155
+ });
156
+ }
157
+ return {
158
+ pipeline: inputPipeline,
159
+ options: watchOptions,
160
+ extraMatches
161
+ };
162
+ };
116
163
  const hasAtomicOperators = (data) => Object.keys(data).some((key) => key.startsWith('$'));
117
164
  const normalizeUpdatePayload = (data) => hasAtomicOperators(data) ? data : { $set: data };
118
165
  const hasOperatorExpressions = (value) => isPlainObject(value) && Object.keys(value).some((key) => key.startsWith('$'));
@@ -316,22 +363,22 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
316
363
  };
317
364
  return {
318
365
  /**
319
- * Finds a single document in a MongoDB collection with optional role-based filtering and validation.
320
- *
321
- * @param {Filter<Document>} query - The MongoDB query used to match the document.
322
- * @param {Document} [projection] - Optional projection to select returned fields.
323
- * @param {FindOneOptions} [options] - Optional settings for the findOne operation.
324
- * @returns {Promise<Document | {} | null>} A promise resolving to the document if found and permitted, an empty object if access is denied, or `null` if not found.
325
- *
326
- * @description
327
- * If `run_as_system` is enabled, the function behaves like a standard `collection.findOne(query)` with no access checks.
328
- * Otherwise:
329
- * - Merges the provided query with any access control filters using `getFormattedQuery`.
330
- * - Attempts to find the document using the formatted query.
331
- * - Determines the user's role via `getWinningRole`.
332
- * - Validates the result using `checkValidation` to ensure read permission.
333
- * - If validation fails, returns an empty object; otherwise returns the validated document.
334
- */
366
+ * Finds a single document in a MongoDB collection with optional role-based filtering and validation.
367
+ *
368
+ * @param {Filter<Document>} query - The MongoDB query used to match the document.
369
+ * @param {Document} [projection] - Optional projection to select returned fields.
370
+ * @param {FindOneOptions} [options] - Optional settings for the findOne operation.
371
+ * @returns {Promise<Document | {} | null>} A promise resolving to the document if found and permitted, an empty object if access is denied, or `null` if not found.
372
+ *
373
+ * @description
374
+ * If `run_as_system` is enabled, the function behaves like a standard `collection.findOne(query)` with no access checks.
375
+ * Otherwise:
376
+ * - Merges the provided query with any access control filters using `getFormattedQuery`.
377
+ * - Attempts to find the document using the formatted query.
378
+ * - Determines the user's role via `getWinningRole`.
379
+ * - Validates the result using `checkValidation` to ensure read permission.
380
+ * - If validation fails, returns an empty object; otherwise returns the validated document.
381
+ */
335
382
  findOne: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (query = {}, projectionOrOptions, options) {
336
383
  var _a;
337
384
  try {
@@ -804,32 +851,35 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
804
851
  *
805
852
  * This allows fine-grained control over what change events a user can observe, based on roles and filters.
806
853
  */
807
- watch: (pipeline = [], options) => {
854
+ watch: (pipelineOrOptions = [], options) => {
808
855
  try {
856
+ const { pipeline, options: watchOptions, extraMatches } = resolveWatchArgs(pipelineOrOptions, options);
809
857
  if (!run_as_system) {
810
858
  (0, utils_3.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
811
859
  // Apply access filters to initial change stream pipeline
812
860
  const formattedQuery = (0, utils_3.getFormattedQuery)(filters, {}, user);
813
- const firstStep = formattedQuery.length ? {
814
- $match: {
815
- $and: formattedQuery
861
+ const watchFormattedQuery = formattedQuery.map((condition) => toWatchMatchFilter(condition));
862
+ const firstStep = watchFormattedQuery.length
863
+ ? {
864
+ $match: {
865
+ $and: watchFormattedQuery
866
+ }
816
867
  }
817
- } : undefined;
818
- const formattedPipeline = [
819
- firstStep,
820
- ...pipeline
821
- ].filter(Boolean);
822
- const result = collection.watch(formattedPipeline, options);
868
+ : undefined;
869
+ const formattedPipeline = [firstStep, ...extraMatches, ...pipeline].filter(Boolean);
870
+ const result = collection.watch(formattedPipeline, watchOptions);
823
871
  const originalOn = result.on.bind(result);
824
872
  /**
825
873
  * Validates a change event against the user's roles.
826
874
  *
827
875
  * @param {Document} change - A change event from the ChangeStream.
828
- * @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document }>}
876
+ * @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document, hasFullDocument: boolean, hasWinningRole: boolean }>}
829
877
  */
830
- const isValidChange = (_a) => __awaiter(void 0, [_a], void 0, function* ({ fullDocument, updateDescription }) {
878
+ const isValidChange = (change) => __awaiter(void 0, void 0, void 0, function* () {
879
+ const { fullDocument, updateDescription } = change;
880
+ const hasFullDocument = !!fullDocument;
831
881
  const winningRole = (0, utils_2.getWinningRole)(fullDocument, user, roles);
832
- const { status, document } = winningRole
882
+ const fullDocumentValidation = winningRole
833
883
  ? yield (0, machines_1.checkValidation)(winningRole, {
834
884
  type: 'read',
835
885
  roles,
@@ -837,6 +887,7 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
837
887
  expansions: {}
838
888
  }, user)
839
889
  : fallbackAccess(fullDocument);
890
+ const { status, document } = fullDocumentValidation;
840
891
  const { status: updatedFieldsStatus, document: updatedFields } = winningRole
841
892
  ? yield (0, machines_1.checkValidation)(winningRole, {
842
893
  type: 'read',
@@ -845,15 +896,32 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
845
896
  expansions: {}
846
897
  }, user)
847
898
  : fallbackAccess(updateDescription === null || updateDescription === void 0 ? void 0 : updateDescription.updatedFields);
848
- return { status, document, updatedFieldsStatus, updatedFields };
899
+ return {
900
+ status,
901
+ document,
902
+ updatedFieldsStatus,
903
+ updatedFields,
904
+ hasFullDocument,
905
+ hasWinningRole: !!winningRole
906
+ };
849
907
  });
850
908
  // Override the .on() method to apply validation before emitting events
851
909
  result.on = (eventType, listener) => {
852
910
  return originalOn(eventType, (change) => __awaiter(void 0, void 0, void 0, function* () {
853
- const { status, document, updatedFieldsStatus, updatedFields } = yield isValidChange(change);
854
- if (!status)
855
- return;
911
+ var _a, _b, _c, _d;
912
+ const { document, updatedFieldsStatus, updatedFields, hasFullDocument, hasWinningRole } = yield isValidChange(change);
856
913
  const filteredChange = Object.assign(Object.assign({}, change), { fullDocument: document, updateDescription: Object.assign(Object.assign({}, change.updateDescription), { updatedFields: updatedFieldsStatus ? updatedFields : {} }) });
914
+ console.log('[flowerbase watch] delivered change', {
915
+ collection: collName,
916
+ operationType: change === null || change === void 0 ? void 0 : change.operationType,
917
+ eventType,
918
+ hasFullDocument,
919
+ hasWinningRole,
920
+ updatedFieldsStatus,
921
+ documentKey: ((_c = (_b = (_a = change === null || change === void 0 ? void 0 : change.documentKey) === null || _a === void 0 ? void 0 : _a._id) === null || _b === void 0 ? void 0 : _b.toString) === null || _c === void 0 ? void 0 : _c.call(_b)) ||
922
+ ((_d = change === null || change === void 0 ? void 0 : change.documentKey) === null || _d === void 0 ? void 0 : _d._id) ||
923
+ null
924
+ });
857
925
  listener(filteredChange);
858
926
  }));
859
927
  };
@@ -861,7 +929,7 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
861
929
  return result;
862
930
  }
863
931
  // System mode: no filtering applied
864
- const result = collection.watch(pipeline, options);
932
+ const result = collection.watch([...extraMatches, ...pipeline], watchOptions);
865
933
  emitMongoEvent('watch');
866
934
  return result;
867
935
  }
@@ -1 +1 @@
1
- {"version":3,"file":"exposeRoutes.d.ts","sourceRoot":"","sources":["../../../src/utils/initializer/exposeRoutes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAOzC;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAU,SAAS,eAAe,kBAmF1D,CAAA"}
1
+ {"version":3,"file":"exposeRoutes.d.ts","sourceRoot":"","sources":["../../../src/utils/initializer/exposeRoutes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAOzC;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAU,SAAS,eAAe,kBAqF1D,CAAA"}
@@ -59,6 +59,7 @@ const exposeRoutes = (fastify) => __awaiter(void 0, void 0, void 0, function* ()
59
59
  const db = fastify.mongo.client.db(constants_1.DB_NAME);
60
60
  const { email, password } = req.body;
61
61
  const hashedPassword = yield (0, crypto_1.hashPassword)(password);
62
+ const now = new Date();
62
63
  const users = db.collection(authCollection).find();
63
64
  const list = yield (users === null || users === void 0 ? void 0 : users.toArray());
64
65
  if (list === null || list === void 0 ? void 0 : list.length) {
@@ -71,6 +72,7 @@ const exposeRoutes = (fastify) => __awaiter(void 0, void 0, void 0, function* ()
71
72
  email: email,
72
73
  password: hashedPassword,
73
74
  status: 'confirmed',
75
+ createdAt: now,
74
76
  custom_data: {}
75
77
  });
76
78
  yield (db === null || db === void 0 ? void 0 : db.collection(authCollection).updateOne({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowerforce/flowerbase",
3
- "version": "1.7.4",
3
+ "version": "1.7.5-beta.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -4,9 +4,16 @@ import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../constants'
4
4
  import { StateManager } from '../state'
5
5
  import { hashToken } from '../utils/crypto'
6
6
  import { SessionCreatedDto } from './dtos'
7
- import { AUTH_ENDPOINTS, AUTH_ERRORS } from './utils'
7
+ import { AUTH_ENDPOINTS } from './utils'
8
8
 
9
9
  const HANDLER_TYPE = 'preHandler'
10
+ const unauthorizedSessionError = {
11
+ message: 'Unauthorized',
12
+ error: 'unauthorized',
13
+ errorCode: 'InvalidSession',
14
+ error_code: 'InvalidSession'
15
+ } as const
16
+ type UnauthorizedSessionReply = typeof unauthorizedSessionError
10
17
 
11
18
  /**
12
19
  * Controller for handling user authentication, profile retrieval, and session management.
@@ -90,7 +97,7 @@ export async function authController(app: FastifyInstance) {
90
97
  * @param {import('fastify').FastifyReply} res - The response object.
91
98
  * @returns {Promise<SessionCreatedDto>} A promise resolving with the newly created session data.
92
99
  */
93
- app.post<{ Reply: SessionCreatedDto }>(
100
+ app.post<{ Reply: SessionCreatedDto | UnauthorizedSessionReply }>(
94
101
  AUTH_ENDPOINTS.SESSION,
95
102
  {
96
103
  schema: {
@@ -99,12 +106,14 @@ export async function authController(app: FastifyInstance) {
99
106
  },
100
107
  async function (req, res) {
101
108
  if (req.user.typ !== 'refresh') {
102
- throw new Error(AUTH_ERRORS.INVALID_TOKEN)
109
+ res.code(401).send(unauthorizedSessionError)
110
+ return
103
111
  }
104
112
 
105
113
  const authHeader = req.headers.authorization
106
114
  if (!authHeader?.startsWith('Bearer ')) {
107
- throw new Error(AUTH_ERRORS.INVALID_TOKEN)
115
+ res.code(401).send(unauthorizedSessionError)
116
+ return
108
117
  }
109
118
  const refreshToken = authHeader.slice('Bearer '.length).trim()
110
119
  const refreshTokenHash = hashToken(refreshToken)
@@ -114,7 +123,8 @@ export async function authController(app: FastifyInstance) {
114
123
  expiresAt: { $gt: new Date() }
115
124
  })
116
125
  if (!storedToken) {
117
- throw new Error(AUTH_ERRORS.INVALID_TOKEN)
126
+ res.code(401).send(unauthorizedSessionError)
127
+ return
118
128
  }
119
129
 
120
130
  const auth_user = await db
@@ -122,7 +132,8 @@ export async function authController(app: FastifyInstance) {
122
132
  .findOne({ _id: new this.mongo.ObjectId(req.user.sub) })
123
133
 
124
134
  if (!auth_user) {
125
- throw new Error(`User with ID ${req.user.sub} not found`)
135
+ res.code(401).send(unauthorizedSessionError)
136
+ return
126
137
  }
127
138
 
128
139
  const user = userCollection && AUTH_CONFIG.user_id_field
@@ -140,7 +140,7 @@ describe('jwtAuthentication', () => {
140
140
  expect(decoded.id).toBe(authId.toHexString())
141
141
  expect(decoded.sub).toBe(authId.toHexString())
142
142
  expect(decoded.data._id).toBe(authId.toHexString())
143
- expect(decoded.user_data._id).toBe(authId.toHexString())
143
+ expect(decoded.user_data._id).toBe(linkedId.toHexString())
144
144
  expect(decoded.custom_data._id).toBe(linkedId.toHexString())
145
145
  expect(decoded.custom_data.role).toBe('owner')
146
146
  })
@@ -101,24 +101,21 @@ export default fp(async function (fastify, opts: Options) {
101
101
 
102
102
  fastify.decorate('createAccessToken', function (user: WithId<Document>) {
103
103
  const id = user._id.toString()
104
- const userData = isRecord(user.user_data) ? { ...user.user_data } : {}
105
104
  const customData = isRecord(user.custom_data)
106
105
  ? { ...user.custom_data }
107
- : { ...userData }
108
- const mergedUserData = {
109
- ...customData,
110
- ...userData,
106
+ : (isRecord(user.user_data) ? { ...user.user_data } : {})
107
+ const authData = {
111
108
  _id: id,
112
109
  id,
113
- email: typeof user.email === 'string' ? user.email : userData.email
110
+ email: typeof user.email === 'string' ? user.email : customData.email
114
111
  }
115
112
 
116
113
  return this.jwt.sign(
117
114
  {
118
115
  typ: 'access',
119
116
  id,
120
- data: mergedUserData,
121
- user_data: mergedUserData,
117
+ data: authData,
118
+ user_data: customData,
122
119
  custom_data: customData
123
120
  },
124
121
  {