From 8d759be419b0435074fcc2211994395e6c5e9921 Mon Sep 17 00:00:00 2001 From: nicgirault Date: Sat, 19 Aug 2023 15:04:11 +0200 Subject: [PATCH] feat: add additionalFields option to populate computed fields --- README.md | 15 +++++++++++++++ src/getList/index.ts | 23 ++++++++++++++++++++--- src/index.ts | 2 +- tests/index.spec.ts | 21 +++++++++++++++++++++ yarn.lock | 12 ++++++++++++ 5 files changed, 69 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 17b91ed..f0c9b29 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,21 @@ crud('/admin/posts', actions, { }) ``` +### Additional attributes + +Additional attributes can be populated in the read views. For example one can add a count of related records like this: + +```ts +crud('/admin/categories', actions, { + additionalAttributes: async category => { + return { + postsCount: await Post.count({ categoryId: category.id }) + } + }, + additionalAttributesConcurrency: 10 // 10 queries Post.count will be perform at the same time +}) +``` + ### Custom behavior & other ORMs ```ts diff --git a/src/getList/index.ts b/src/getList/index.ts index 3562c7d..5b39ad1 100644 --- a/src/getList/index.ts +++ b/src/getList/index.ts @@ -1,4 +1,5 @@ import { RequestHandler, Request, Response } from 'express' +import pLimit from 'p-limit'; import { setGetListHeaders } from './headers' @@ -12,15 +13,17 @@ export type Get = (conf: { res: Response }) => Promise<{ rows: R[]; count: number }> -export interface GetListOptions { +export interface GetListOptions { filters: FiltersOption + additionalAttributes: (record: R) => object | Promise + additionalAttributesConcurrency: number } type FiltersOption = Record any> export const getMany = ( doGetFilteredList: Get, - options?: Partial + options?: Partial> ): RequestHandler => async (req, res, next) => { try { const { limit, offset, filter, order } = await parseQuery( @@ -35,7 +38,11 @@ export const getMany = ( order, }, { req, res }) setGetListHeaders(res, offset, count, rows.length) - res.json(rows) + res.json( + options?.additionalAttributes + ? await computeAdditionalAttributes(options.additionalAttributes, options.additionalAttributesConcurrency ?? 1)(rows) + : rows + ) } catch (error) { next(error) @@ -74,3 +81,13 @@ const getFilter = async ( return result } + + +const computeAdditionalAttributes = + (additionalAttributes: GetListOptions["additionalAttributes"], concurrency: number) => { + const limit = pLimit(concurrency) + + return (records: R[]) => Promise.all(records.map(record => + limit(async () => ({ ...record, ...await additionalAttributes(record) })) + )) + } diff --git a/src/index.ts b/src/index.ts index ba222b8..fe19a5c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ export { Create, Destroy, Update, Get } export const crud = ( path: string, actions: Partial>, - options?: Partial + options?: Partial> ) => { const router = Router() router.use(bodyParser.json()) diff --git a/tests/index.spec.ts b/tests/index.spec.ts index 902f371..d000899 100644 --- a/tests/index.spec.ts +++ b/tests/index.spec.ts @@ -55,6 +55,27 @@ describe('crud', () => { order: [['name', 'DESC']], }, expectReqRes) }) + + + it('populates additional fields when provided', async () => { + const dataProvider = await setupApp( + crud('/users', { + get: jest.fn().mockResolvedValue({ rows: [{ id: 1 }], count: 1 }), + }, { + additionalAttributes: async (record) => { + return { additionalProperty: await new Promise(resolve => resolve(`value ${record.id}`)) } + } + }), + ctx + ) + + const response = await dataProvider.getList('users', { + pagination: { page: 0, perPage: 25 }, + sort: { field: 'id', order: 'DESC' }, + filter: {}, + }) + expect(response.data[0]).toEqual({ id: 1, additionalProperty: 'value 1' }) + }) }) describe('DELETE', () => { diff --git a/yarn.lock b/yarn.lock index ff727b5..00f5ac6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5304,6 +5304,13 @@ p-is-promise@^3.0.0: resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-3.0.0.tgz#58e78c7dfe2e163cf2a04ff869e7c1dba64a5971" integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== +p-limit@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -6925,3 +6932,8 @@ yargs@^16.2.0: string-width "^4.2.0" y18n "^5.0.5" yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==