From c4f8d1d2c3794d2567d94c877a59fd2e48a2b669 Mon Sep 17 00:00:00 2001 From: Nicola Marogna Date: Mon, 22 Jun 2026 12:13:53 +0200 Subject: [PATCH] fix: rebuilds options from realm-web flat fields; covers find/findOne/count/findOneAndUpdate --- .../functions/__tests__/controller.test.ts | 45 +++++++++++++++++++ .../src/features/functions/controller.ts | 34 ++++++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/flowerbase/src/features/functions/__tests__/controller.test.ts b/packages/flowerbase/src/features/functions/__tests__/controller.test.ts index 2178991..e9f5615 100644 --- a/packages/flowerbase/src/features/functions/__tests__/controller.test.ts +++ b/packages/flowerbase/src/features/functions/__tests__/controller.test.ts @@ -61,6 +61,51 @@ describe('functionsController', () => { ) }) + it('reconstructs realm-web flattened options (sort/limit/project) into the options arg', async () => { + const cursor: any = { + sort: jest.fn(() => cursor), + skip: jest.fn(() => cursor), + limit: jest.fn(() => cursor), + toArray: () => Promise.resolve([]) + } + const find = jest.fn().mockReturnValue(cursor) + services['mongodb-atlas'] = jest.fn(() => ({ + db: jest.fn().mockReturnValue({ + collection: jest.fn().mockReturnValue({ find }) + }) + })) as any + + const response = await app.inject({ + method: 'POST', + url: '/call', + payload: { + service: 'mongodb-atlas', + name: 'find', + arguments: [ + { + database: 'app', + collection: 'todos', + query: { archived: false }, + // realm-web flattens these to the top level; `projection` is `project` + sort: { createdAt: -1 }, + limit: 5, + project: { name: 1 } + } + ] + } + }) + + expect(response.statusCode).toBe(200) + const [, , passedOptions] = find.mock.calls[0] + expect(passedOptions).toEqual( + expect.objectContaining({ + sort: { createdAt: -1 }, + limit: 5, + projection: { name: 1 } + }) + ) + }) + it('passes mongodb-atlas distinct service arguments through POST /call', async () => { const distinct = jest.fn().mockResolvedValue(['open']) services['mongodb-atlas'] = jest.fn(() => ({ diff --git a/packages/flowerbase/src/features/functions/controller.ts b/packages/flowerbase/src/features/functions/controller.ts index 0f1d6e4..907ea5b 100644 --- a/packages/flowerbase/src/features/functions/controller.ts +++ b/packages/flowerbase/src/features/functions/controller.ts @@ -84,6 +84,27 @@ const isPlainRecord = (value: unknown): value is Record => { return prototype === Object.prototype || prototype === null } +// realm-web flattens these option fields to the top level of the service call. +const FLATTENED_OPTION_KEYS = ['sort', 'limit', 'skip', 'upsert', 'returnNewDocument'] as const + +const mergeRealmOptions = ( + rawArgs: Record, + options: unknown +): Record | undefined => { + const flattened: Record = {} + for (const k of FLATTENED_OPTION_KEYS) { + if (typeof rawArgs[k] !== 'undefined') flattened[k] = rawArgs[k] + } + // find/findOne send `project`; findOneAnd* send `projection`. + const projection = rawArgs.project ?? rawArgs.projection + if (typeof projection !== 'undefined') flattened.projection = projection + + // Nested `options` (flowerbase-client wire format) wins over flattened fields. + const nested = isRecord(options) ? options : undefined + if (!nested && !Object.keys(flattened).length) return undefined + return { ...flattened, ...(nested ?? {}) } +} + const isCursorLike = ( value: unknown ): value is { toArray: () => Promise | unknown } => { @@ -372,7 +393,8 @@ export const functionsController: FunctionController = async ( if (!serviceFn) { throw new Error(`Service "${req.body.service}" does not exist`) } - const [{ + const [rawArgs] = args + const { database, collection, key, @@ -386,12 +408,18 @@ export const functionsController: FunctionController = async ( documents, operations, pipeline = [] - }] = args + } = rawArgs if (!isSupportedQueryMethod(method)) { throw new Error(`Unsupported service method "${String(method)}"`) } + // The realm-web SDK flattens query options (sort, limit, skip, upsert, + // returnNewDocument and projection — renamed `project` for find/findOne) + // to the top level instead of nesting them under `options`. Reconstruct an + // options object from those flattened fields so the operators honor them. + const mergedOptions = mergeRealmOptions(rawArgs, options) + const currentMethod = serviceFn(app, { rules, user }) .db(database) .collection(collection)[method] @@ -404,7 +432,7 @@ export const functionsController: FunctionController = async ( filter, update, projection, - options, + options: mergedOptions, returnNewDocument, document, documents,