Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => ({
Expand Down
34 changes: 31 additions & 3 deletions packages/flowerbase/src/features/functions/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,27 @@ const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
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<string, unknown>,
options: unknown
): Record<string, unknown> | undefined => {
const flattened: Record<string, unknown> = {}
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> | unknown } => {
Expand Down Expand Up @@ -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,
Expand All @@ -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]
Expand All @@ -404,7 +432,7 @@ export const functionsController: FunctionController = async (
filter,
update,
projection,
options,
options: mergedOptions,
returnNewDocument,
document,
documents,
Expand Down
Loading