diff --git a/server/admin-next/package.json b/server/admin-next/package.json index afe93bc5..8431a1eb 100644 --- a/server/admin-next/package.json +++ b/server/admin-next/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "nodemon", "start": "cross-env NODE_ENV=production node dist/admin-next/src/server/index.js", - "test": "node --test -r ts-node/register src/client/*.test.ts src/client/*.test.tsx", + "test": "node --test -r ts-node/register src/client/*.test.ts src/client/*.test.tsx src/server/middleware/express-mongoose-ra-json-server/utils/*.test.ts", "check:type": "tsc --noEmit", "build": "pnpm build:client && pnpm build:server", "build:client": "vite build", diff --git a/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/index.ts b/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/index.ts index c3e082b9..ce89fd4f 100644 --- a/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/index.ts +++ b/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/index.ts @@ -30,6 +30,9 @@ export interface raExpressMongooseOptions { /** Fields to search from ?q (used for autofill and search) */ q?: string[]; + /** Extra $or condition built from ?q, e.g. structured `nickname#discriminator` */ + qFilter?: (q: string) => Record | undefined; + /** Base name for ACLs (e.g. list operation does baseName.list) */ aclName?: string; @@ -67,6 +70,7 @@ export function raExpressMongoose( ) { const { q, + qFilter, allowedRegexFields = [], readOnlyFields, inputTransformer = (input: any) => input, @@ -105,7 +109,8 @@ export function raExpressMongoose( ), model, allowedRegexFields, - q + q, + qFilter ), }; let query = model.find(filterQuery); diff --git a/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/utils/parseQuery.test.ts b/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/utils/parseQuery.test.ts new file mode 100644 index 00000000..c13f619c --- /dev/null +++ b/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/utils/parseQuery.test.ts @@ -0,0 +1,36 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import parseQuery from './parseQuery'; + +const model: any = { schema: { path: () => ({ cast: (v: any) => v }) } }; +const uniqueName = (q: string) => { + const [nickname, discriminator] = q.split('#'); + return discriminator ? { nickname, discriminator } : undefined; +}; + +test('adds nickname#discriminator match to $or', () => { + const result = parseQuery( + { q: 'admin#6485' }, + model, + ['nickname'], + ['nickname', 'email'], + uniqueName + ); + assert.equal(result.q, undefined); + assert.deepEqual(result.$or.at(-1), { + nickname: 'admin', + discriminator: '6485', + }); +}); + +test('keeps plain search unchanged without #', () => { + const result = parseQuery( + { q: 'admin' }, + model, + ['nickname'], + ['nickname', 'email'], + uniqueName + ); + assert.equal(result.$or.length, 2); + assert.deepEqual(result.$or[1], { email: 'admin' }); +}); diff --git a/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/utils/parseQuery.ts b/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/utils/parseQuery.ts index b589e4c6..b549e511 100644 --- a/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/utils/parseQuery.ts +++ b/server/admin-next/src/server/middleware/express-mongoose-ra-json-server/utils/parseQuery.ts @@ -11,6 +11,7 @@ interface parseQueryParam { * Turns ?q into $or queries, deletes q * @param {Object} results Original object with the q field * @param {string[]} fields Fields to apply q to + * @param {Function} qFilter Optional extra $or condition built from q */ export default function parseQuery< T extends parseQueryParam, @@ -19,7 +20,8 @@ export default function parseQuery< result: T, model: M, allowedRegexes: string[], - fields?: string[] + fields?: string[], + qFilter?: (q: string) => Record | undefined ): T & { $or?: any } { if (!fields) return result; if (result.q) { @@ -32,6 +34,8 @@ export default function parseQuery< const newFilter = { [field]: result.q }; result.$or.push(castFilter(newFilter, model, allowedRegexes)); }); + const extra = qFilter?.(String(result.q)); + if (extra) result.$or.push(extra); delete result.q; } return result; diff --git a/server/admin-next/src/server/router/api.ts b/server/admin-next/src/server/router/api.ts index 63f3d670..18874690 100644 --- a/server/admin-next/src/server/router/api.ts +++ b/server/admin-next/src/server/router/api.ts @@ -177,6 +177,10 @@ router.use( auth(), raExpressMongoose(userModel, { q: ['_id', 'nickname', 'email'], + qFilter: (q) => { + const [nickname, discriminator] = q.split('#'); + return discriminator ? { nickname, discriminator } : undefined; + }, allowedRegexFields: ['nickname'], }) );