diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1c525..3b2070a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,16 @@ Find all notable changes of this project in this file. ## unpublished (v2.0.0) -- package updates +### Improvements +- updated dependencies +- updated `urlBuilder` and integrated URL encoding + +### New feature +- [#48](https://github.com/raphiniert-com/ra-data-postgrest/issues/48) solved by pull request [#55](https://github.com/raphiniert-com/ra-data-postgrest/pull/55), the create method should return the data returned by postgrest and not the posted data - @[christiaanwesterbeek](https://github.com/christiaanwesterbeek) +- [#39](https://github.com/raphiniert-com/ra-data-postgrest/pull/39), allow passing extra headers via meta for react-admin hooks - @[christiaanwesterbeek](https://github.com/christiaanwesterbeek) +- [#41](https://github.com/raphiniert-com/ra-data-postgrest/pull/41), allow columns filtering for getList and getManyReference - @[christiaanwesterbeek](https://github.com/christiaanwesterbeek) +- added support of [#41](https://github.com/raphiniert-com/ra-data-postgrest/pull/41) for other functions, but `create` and `updateMany` +- [#38](https://github.com/raphiniert-com/ra-data-postgrest/pull/38), Let the sort param in getList be optional - @[christiaanwesterbeek](https://github.com/christiaanwesterbeek) ## v1.2.1 - 2023-04-07 diff --git a/README.md b/README.md index 958759f..462aaaa 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,48 @@ const dataProvider = postgrestRestProvider( ); ``` +### Passing extra headers via meta +Postgrest supports calling functions with a single JSON parameter by sending the header Prefer: params=single-object with your request according to its [docs](https://postgrest.org/en/stable/api.html#calling-functions-with-a-single-json-parameter). + +Within the data provider one can add any kind of header to the request while calling react-admin hooks, e.g.: +``` +const [create, { isLoading, error }] = useCreate( + 'rpc/my-function', + { + data: { ... }, + meta: { headers: { Prefer: 'params=single-object' } }, + } +); +``` + +### Vertical filtering +Postgrest supports a feature of [Vertical Filtering (Columns)](https://postgrest.org/en/stable/api.html#vertical-filtering-columns). Within the react-admin hooks this feature can be used as in the following example: +``` +const { data, total, isLoading, error } = useGetList( + 'posts', + { + pagination: { page: 1, perPage: 10 }, + sort: { field: 'published_at', order: 'DESC' } + meta: { columns: ['id', 'title'] } + } +); +``` + +Further, one should be able to leverage this feature to rename columns: +``` +columns: ['id', 'somealias:title'] +``` +, to cast columns: +``` +columns: ['id::text', 'title'] +``` +and even get bits from a json or jsonb column" +``` +columns: ['id', 'json_data->>blood_type', 'json_data->phones'] +``` + +**Note**: not working for `create` and `updateMany`. + ## Developers notes The current development of this library was done with node v19.10 and npm 8.19.3. In this version the unit tests and the development environment should work. diff --git a/package-lock.json b/package-lock.json index 08845ad..73ba330 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@raphiniert/ra-data-postgrest", "version": "2.0.0-alpha.0", "license": "MIT", + "dependencies": { + "qs": "^6.11.1" + }, "devDependencies": { "@types/jest": "^28.1.4", "cross-env": "^7.0.3", @@ -1587,6 +1590,18 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1794,15 +1809,6 @@ } } }, - "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", - "peer": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -2420,15 +2426,6 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2510,8 +2507,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/gensync": { "version": "1.0.0-beta.2", @@ -2531,6 +2527,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -2631,7 +2640,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -2648,6 +2656,17 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/history": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", @@ -3851,6 +3870,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/oblivious-set": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", @@ -4196,22 +4223,18 @@ "node": ">=6" } }, - "node_modules/query-string": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", - "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", - "peer": true, + "node_modules/qs": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", + "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", "dependencies": { - "decode-uri-component": "^0.2.0", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" + "side-channel": "^1.0.4" }, "engines": { - "node": ">=6" + "node": ">=0.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/queue-microtask": { @@ -4261,6 +4284,51 @@ "react-router-dom": "^6.1.0" } }, + "node_modules/ra-core/node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "peer": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/ra-core/node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ra-core/node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "peer": true, + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ra-core/node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -4581,6 +4649,19 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -4621,15 +4702,6 @@ "source-map": "^0.6.0" } }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -6337,6 +6409,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -6482,12 +6563,6 @@ "ms": "2.1.2" } }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", - "peer": true - }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -6952,12 +7027,6 @@ "to-regex-range": "^5.0.1" } }, - "filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "peer": true - }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -7019,8 +7088,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "gensync": { "version": "1.0.0-beta.2", @@ -7034,6 +7102,16 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -7107,7 +7185,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -7118,6 +7195,11 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, "history": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", @@ -8049,6 +8131,11 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "peer": true }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, "oblivious-set": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", @@ -8303,16 +8390,12 @@ "dev": true, "peer": true }, - "query-string": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", - "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", - "peer": true, + "qs": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", + "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", "requires": { - "decode-uri-component": "^0.2.0", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" + "side-channel": "^1.0.4" } }, "queue-microtask": { @@ -8338,6 +8421,38 @@ "query-string": "^7.1.1", "react-is": "^17.0.2", "react-query": "^3.32.1" + }, + "dependencies": { + "decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "peer": true + }, + "filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "peer": true + }, + "query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "peer": true, + "requires": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "peer": true + } } }, "react": { @@ -8553,6 +8668,16 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -8587,12 +8712,6 @@ "source-map": "^0.6.0" } }, - "split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "peer": true - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 86e0c8f..849aebf 100644 --- a/package.json +++ b/package.json @@ -43,5 +43,8 @@ }, "peerDependencies": { "ra-core": "^4.1.0" + }, + "dependencies": { + "qs": "^6.11.1" } } diff --git a/src/index.ts b/src/index.ts index 722c033..099fbad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,16 @@ -import { fetchUtils, DataProvider } from 'ra-core'; +import { + fetchUtils, + DataProvider, + GetListParams, + GetOneParams, + GetManyParams, + GetManyReferenceParams, + UpdateParams, + UpdateManyParams, + CreateParams, + DeleteParams, + DeleteManyParams, +} from 'ra-core'; import { PrimaryKey, PostgRestOperator, @@ -12,6 +24,8 @@ import { decodeId, isCompoundKey, } from './urlBuilder'; +import qs from 'qs'; + /** * Maps react-admin queries to a postgrest REST API * @@ -56,30 +70,38 @@ export default ( defaultListOp: PostgRestOperator = 'eq', primaryKeys: Map = defaultPrimaryKeys ): DataProvider => ({ - getList: (resource, params) => { + getList: (resource, params: Partial = {}) => { const primaryKey = getPrimaryKey(resource, primaryKeys); const { page, perPage } = params.pagination; - const { field, order } = params.sort; - const parsedFilter = parseFilters(params.filter, defaultListOp); + const { field, order } = params.sort || {}; + const { filter, select } = parseFilters(params, defaultListOp); - const query = { - order: getOrderBy(field, order, primaryKey), + let query = { offset: String((page - 1) * perPage), limit: String(perPage), // append filters - ...parsedFilter, + ...filter, }; + if (field) { + query.order = getOrderBy(field, order, primaryKey); + } + + if (select) { + query.select = select; + } + // add header that Content-Range is in returned header const options = { headers: new Headers({ Accept: 'application/json', Prefer: 'count=exact', + ...(params.meta?.headers || {}), }), }; - const url = `${apiUrl}/${resource}?${new URLSearchParams(query)}`; + const url = `${apiUrl}/${resource}?${qs.stringify(query)}`; return httpClient(url, options).then(({ headers, json }) => { if (!headers.has('content-range')) { @@ -99,66 +121,75 @@ export default ( }); }, - getOne: (resource, params) => { - const id = params.id; + getOne: (resource, params: Partial = {}) => { + const { id, meta } = params; const primaryKey = getPrimaryKey(resource, primaryKeys); - const query = getQuery(primaryKey, id, resource); + const query = getQuery(primaryKey, id, resource, meta); - const url = `${apiUrl}/${resource}?${query}`; + const url = `${apiUrl}/${resource}?${qs.stringify(query)}`; return httpClient(url, { headers: new Headers({ accept: 'application/vnd.pgrst.object+json', + ...(params.meta?.headers || {}), }), }).then(({ json }) => ({ data: dataWithId(json, primaryKey), })); }, - getMany: (resource, params) => { + getMany: (resource, params: Partial = {}) => { const ids = params.ids; const primaryKey = getPrimaryKey(resource, primaryKeys); - const query = getQuery(primaryKey, ids, resource); - - const url = `${apiUrl}/${resource}?${query}`; + const query = getQuery(primaryKey, ids, resource, params.meta); + const url = `${apiUrl}/${resource}?${qs.stringify(query)}`; return httpClient(url).then(({ json }) => ({ data: json.map(data => dataWithId(data, primaryKey)), })); }, - getManyReference: (resource, params) => { + getManyReference: ( + resource, + params: Partial = {} + ) => { const { page, perPage } = params.pagination; const { field, order } = params.sort; - const parsedFilter = parseFilters(params.filter, defaultListOp); + + const { filter, select } = parseFilters(params, defaultListOp); const primaryKey = getPrimaryKey(resource, primaryKeys); - const query = params.target + let query = params.target ? { [params.target]: `eq.${params.id}`, order: getOrderBy(field, order, primaryKey), offset: String((page - 1) * perPage), limit: String(perPage), - ...parsedFilter, + ...filter, } : { order: getOrderBy(field, order, primaryKey), offset: String((page - 1) * perPage), limit: String(perPage), - ...parsedFilter, + ...filter, }; + if (select) { + query.select = select; + } + // add header that Content-Range is in returned header const options = { headers: new Headers({ Accept: 'application/json', Prefer: 'count=exact', + ...(params.meta?.headers || {}), }), }; - const url = `${apiUrl}/${resource}?${new URLSearchParams(query)}`; + const url = `${apiUrl}/${resource}?${qs.stringify(query)}`; return httpClient(url, options).then(({ headers, json }) => { if (!headers.has('content-range')) { @@ -178,15 +209,15 @@ export default ( }); }, - update: (resource, params) => { - const { id, data } = params; + update: (resource, params: Partial = {}) => { + const { id, data, meta } = params; const primaryKey = getPrimaryKey(resource, primaryKeys); - const query = getQuery(primaryKey, id, resource); + const query = getQuery(primaryKey, id, resource, meta); const primaryKeyData = getKeyData(primaryKey, data); - const url = `${apiUrl}/${resource}?${query}`; + const url = `${apiUrl}/${resource}?${qs.stringify(query)}`; const body = JSON.stringify({ ...data, @@ -199,12 +230,13 @@ export default ( Accept: 'application/vnd.pgrst.object+json', Prefer: 'return=representation', 'Content-Type': 'application/json', + ...(params.meta?.headers || {}), }), body, }).then(({ json }) => ({ data: dataWithId(json, primaryKey) })); }, - updateMany: (resource, params) => { + updateMany: (resource, params: Partial = {}) => { const ids = params.ids; const primaryKey = getPrimaryKey(resource, primaryKeys); @@ -236,6 +268,7 @@ export default ( headers: new Headers({ Prefer: 'return=representation', 'Content-Type': 'application/json', + ...(params.meta?.headers || {}), }), body, }).then(({ json }) => ({ @@ -243,9 +276,8 @@ export default ( })); }, - create: (resource, params) => { + create: (resource, params: Partial = {}) => { const primaryKey = getPrimaryKey(resource, primaryKeys); - const url = `${apiUrl}/${resource}`; return httpClient(url, { @@ -254,6 +286,7 @@ export default ( Accept: 'application/vnd.pgrst.object+json', Prefer: 'return=representation', 'Content-Type': 'application/json', + ...(params.meta?.headers || {}), }), body: JSON.stringify(params.data), }).then(({ json }) => ({ @@ -264,13 +297,13 @@ export default ( })); }, - delete: (resource, params) => { - const id = params.id; + delete: (resource, params: Partial = {}) => { + const { id, meta } = params; const primaryKey = getPrimaryKey(resource, primaryKeys); - const query = getQuery(primaryKey, id, resource); + const query = getQuery(primaryKey, id, resource, meta); - const url = `${apiUrl}/${resource}?${query}`; + const url = `${apiUrl}/${resource}?${qs.stringify(query)}`; return httpClient(url, { method: 'DELETE', @@ -278,23 +311,25 @@ export default ( Accept: 'application/vnd.pgrst.object+json', Prefer: 'return=representation', 'Content-Type': 'application/json', + ...(params.meta?.headers || {}), }), }).then(({ json }) => ({ data: dataWithId(json, primaryKey) })); }, - deleteMany: (resource, params) => { - const ids = params.ids; + deleteMany: (resource, params: Partial = {}) => { + const { ids, meta } = params; const primaryKey = getPrimaryKey(resource, primaryKeys); - const query = getQuery(primaryKey, ids, resource); + const query = getQuery(primaryKey, ids, resource, meta); - const url = `${apiUrl}/${resource}?${query}`; + const url = `${apiUrl}/${resource}?${qs.stringify(query)}`; return httpClient(url, { method: 'DELETE', headers: new Headers({ Prefer: 'return=representation', 'Content-Type': 'application/json', + ...(params.meta?.headers || {}), }), }).then(({ json }) => ({ data: json.map(data => encodeId(data, primaryKey)), diff --git a/src/urlBuilder.ts b/src/urlBuilder.ts index e88e35d..470703a 100644 --- a/src/urlBuilder.ts +++ b/src/urlBuilder.ts @@ -44,13 +44,21 @@ const postgrestOperators = [ 'and', ] as const; +type ParsedFiltersResult = { + filter: any; + select: any; +}; + export type PostgRestOperator = (typeof postgrestOperators)[number]; export const parseFilters = ( - filter: Record, + params: any, defaultListOp: PostgRestOperator -) => { - let result = {}; +): Partial => { + const { filter, meta = {} } = params; + + let result: Partial = {}; + result.filter = {}; Object.keys(filter).forEach(function (key) { // key: the name of the object key @@ -76,21 +84,30 @@ export const parseFilters = ( ? `${value}` : `${operation}.${value}`; - if (result[splitKey[0]] === undefined) { + if (result.filter[splitKey[0]] === undefined) { // first operator for the key, we add it to the dict - result[splitKey[0]] = op; + result.filter[splitKey[0]] = op; } else { if (!Array.isArray(result[splitKey[0]])) { // second operator, we transform to an array - result[splitKey[0]] = [result[splitKey[0]], op]; + result.filter[splitKey[0]] = [ + result.filter[splitKey[0]], + op, + ]; } else { // third and subsequent, we add to array - result[splitKey[0]].push(op); + result.filter[splitKey[0]].push(op); } } }); }); + if (meta?.columns) { + result.select = Array.isArray(meta.columns) + ? meta.columns.join(',') + : meta.columns; + } + return result; }; @@ -140,8 +157,10 @@ export const isCompoundKey = (primaryKey: PrimaryKey): Boolean => { export const getQuery = ( primaryKey: PrimaryKey, ids: Identifier | Array, - resource: string -): string => { + resource: string, + meta: any = null +): any => { + let result: any = {}; if (Array.isArray(ids) && ids.length > 1) { // no standardized query with multiple ids possible for rpc endpoints which are api-exposed database functions if (resource.startsWith('rpc/')) { @@ -153,17 +172,18 @@ export const getQuery = ( } if (isCompoundKey(primaryKey)) { - // TODO: Should be URL encoded - return `or=(${ids.map(id => { - const primaryKeyParams = decodeId(id, primaryKey); - return `and(${primaryKey - .map((key, i) => `${key}.eq.${primaryKeyParams[i]}`) - .join(',')})`; - })})`; + result = { + or: `(${ids.map(id => { + const primaryKeyParams = decodeId(id, primaryKey); + return `and(${primaryKey + .map((key, i) => `${key}.eq.${primaryKeyParams[i]}`) + .join(',')})`; + })})`, + }; } else { - return new URLSearchParams({ + result = { [primaryKey[0]]: `in.(${ids.join(',')})`, - }).toString(); + }; } } else { // if ids is one Identifier @@ -171,27 +191,34 @@ export const getQuery = ( const primaryKeyParams = decodeId(id, primaryKey); if (isCompoundKey(primaryKey)) { - if (resource.startsWith('rpc/')) - // TODO: Should be URL encoded - return `${primaryKey - .map( - (key: string, i: any) => `${key}=${primaryKeyParams[i]}` - ) - .join('&')}`; - // TODO: Should be URL encoded - else - return `and=(${primaryKey - .map( + if (resource.startsWith('rpc/')) { + result = {}; + primaryKey.map( + (key: string, i: any) => + (result[key] = `${primaryKeyParams[i]}`) + ); + } else { + result = { + and: `(${primaryKey.map( (key: string, i: any) => `${key}.eq.${primaryKeyParams[i]}` - ) - .join(',')})`; + )})`, + }; + } } else { - return new URLSearchParams([ - [primaryKey[0], `eq.${id}`], - ]).toString(); + result = { + [primaryKey[0]]: `eq.${id}`, + }; } } + + if (meta && meta.columns) { + result.select = Array.isArray(meta.columns) + ? meta.columns.join(',') + : meta.columns; + } + + return result; }; export const getKeyData = (primaryKey: PrimaryKey, data: object): object => { diff --git a/tests/dataProvider/basic.test.ts b/tests/dataProvider/basic.test.ts index 57fc62d..40d7936 100644 --- a/tests/dataProvider/basic.test.ts +++ b/tests/dataProvider/basic.test.ts @@ -12,7 +12,7 @@ const cases: Case[] = [ filter: {}, meta: {}, }, - expectedUrl: '/posts?order=title.desc&offset=0&limit=10', + expectedUrl: '/posts?offset=0&limit=10&order=title.desc', expectedOptions: { headers: { accept: 'application/json', diff --git a/tests/dataProvider/getList.test.ts b/tests/dataProvider/getList.test.ts index 58c0832..d67ba53 100644 --- a/tests/dataProvider/getList.test.ts +++ b/tests/dataProvider/getList.test.ts @@ -19,7 +19,7 @@ describe('getList specific', () => { }, filter: {}, }, - expectedUrl: `/posts?order=id.asc&offset=0&limit=10`, + expectedUrl: `/posts?offset=0&limit=10&order=id.asc`, expectedOptions: { headers: { accept: 'application/json', diff --git a/tests/dataProvider/getOne.test.ts b/tests/dataProvider/getOne.test.ts index d72a12c..f3bffd1 100644 --- a/tests/dataProvider/getOne.test.ts +++ b/tests/dataProvider/getOne.test.ts @@ -1,4 +1,4 @@ -import { encodeId } from '../../src/urlBuilder'; +import qs from 'qs'; import { makeTestFromCase, Case } from './helper'; describe('getOne specific', () => { @@ -27,7 +27,7 @@ describe('getOne specific', () => { params: { id: JSON.stringify([1, 'X']), }, - expectedUrl: `/contacts?and=(id.eq.1,type.eq.X)`, + expectedUrl: '/contacts?'.concat(qs.stringify({and: '(id.eq.1,type.eq.X)'})), expectedOptions, }, { diff --git a/tests/dataProvider/update.test.ts b/tests/dataProvider/update.test.ts index 04bb913..56d653c 100644 --- a/tests/dataProvider/update.test.ts +++ b/tests/dataProvider/update.test.ts @@ -1,4 +1,4 @@ -import { encodeId } from '../../src/urlBuilder'; +import qs from 'qs'; import { makeTestFromCase, Case } from './helper'; describe('update specific', () => { @@ -14,7 +14,7 @@ describe('update specific', () => { data: { name: 'new name' }, meta: {}, }, - expectedUrl: '/contacts?and=(id.eq.1,type.eq.X)', + expectedUrl: '/contacts?'.concat(qs.stringify({and: '(id.eq.1,type.eq.X)'})), expectedOptions: { method: 'PATCH', body: JSON.stringify({ name: 'new name' }), diff --git a/tests/urlBuilder/helper.ts b/tests/urlBuilder/helper.ts index 3e162d1..85aa8f0 100644 --- a/tests/urlBuilder/helper.ts +++ b/tests/urlBuilder/helper.ts @@ -1,6 +1,3 @@ -export const qs = (queryParams: Record) => - new URLSearchParams(queryParams).toString(); - /** * To encode uri components to be RFC 3986-compliant - which encodes the characters !'()* * @url https://stackoverflow.com/questions/44429173/javascript-encodeuri-failed-to-encode-round-bracket diff --git a/tests/urlBuilder/urlBuilder.test.ts b/tests/urlBuilder/urlBuilder.test.ts index de07cfd..bdcd1b6 100644 --- a/tests/urlBuilder/urlBuilder.test.ts +++ b/tests/urlBuilder/urlBuilder.test.ts @@ -17,27 +17,79 @@ import { primaryKeySingle, resourcePimaryKeys, } from '../fixtures'; -import { qs } from './helper'; describe('parseFilters', () => { it('should parse filters', () => { expect( parseFilters( { - q1: 'foo', - 'q2@ilike': 'bar', - 'q3@like': 'baz qux', - 'q4@gt': 'c', + filter: { + q1: 'foo', + 'q2@ilike': 'bar', + 'q3@like': 'baz qux', + 'q4@gt': 'c', + } }, 'eq' ) ).toEqual({ - q1: 'eq.foo', - q2: 'ilike.*bar*', - q3: ['like.*baz*', 'like.*qux*'], - q4: 'gt.c', - }); + filter: { + q1: 'eq.foo', + q2: 'ilike.*bar*', + q3: ['like.*baz*', 'like.*qux*'], + q4: 'gt.c', + } + }); + }); + it('should parse filters with one select fields', () => { + expect( + parseFilters( + { + filter: { + q1: 'foo', + 'q2@ilike': 'bar', + 'q3@like': 'baz qux', + 'q4@gt': 'c', + }, + meta: { columns: 'title' } + }, + 'eq' + ) + ).toEqual({ + filter: { + q1: 'eq.foo', + q2: 'ilike.*bar*', + q3: ['like.*baz*', 'like.*qux*'], + q4: 'gt.c' + }, + select: 'title' + }); + }); + it('should parse filters with multiple select fields', () => { + expect( + parseFilters( + { + filter: { + q1: 'foo', + 'q2@ilike': 'bar', + 'q3@like': 'baz qux', + 'q4@gt': 'c', + }, + meta: { columns: ['id', 'title'] } + }, + 'eq' + ) + ).toEqual({ + filter: { + q1: 'eq.foo', + q2: 'ilike.*bar*', + q3: ['like.*baz*', 'like.*qux*'], + q4: 'gt.c' + }, + select: 'id,title' + }); }); + }); describe('getPrimaryKey', () => { @@ -107,7 +159,25 @@ describe('getQuery', () => { const id = 2; const query = getQuery(primaryKeySingle, id, resource); - expect(query).toEqual(qs({ id: 'eq.2' })); + expect(query).toEqual({ id: 'eq.2' }); + }); + + it('should return the query for a single id of normal resource with one select fields', () => { + const resource = 'todos'; + const id = 2; + const meta = { columns: 'field' }; + const query = getQuery(primaryKeySingle, id, resource, meta); + + expect(query).toEqual({ id: 'eq.2', select: 'field' }); + }); + + it('should return the query for a single id of normal resource with multiple select fields', () => { + const resource = 'todos'; + const id = 2; + const meta = { columns: ['id', 'field'] }; + const query = getQuery(primaryKeySingle, id, resource, meta); + + expect(query).toEqual({ id: 'eq.2', select: 'id,field' }); }); it('should return the query for multiple ids of normal resource', () => { @@ -115,14 +185,14 @@ describe('getQuery', () => { const ids = [1, 2, 3]; const query = getQuery(primaryKeySingle, ids, resource); - expect(query).toEqual(qs({ id: 'in.(1,2,3)' })); + expect(query).toEqual({ id: 'in.(1,2,3)' }); }); it('should return the query for a single id of a resource with a compound key', () => { const resource = 'todos'; const id = '[1,"X"]'; const query = getQuery(primaryKeyCompound, id, resource); - expect(query).toEqual('and=(id.eq.1,type.eq.X)'); + expect(query).toEqual({and: '(id.eq.1,type.eq.X)'}); }); it('should return the query for multiple ids of a resource with a compound key', () => { @@ -130,16 +200,16 @@ describe('getQuery', () => { const ids = ['[1,"X"]', '[2,"Y"]']; const query = getQuery(primaryKeyCompound, ids, resource); - expect(query).toEqual( - 'or=(and(id.eq.1,type.eq.X),and(id.eq.2,type.eq.Y))' - ); + expect(query).toEqual({ + or: '(and(id.eq.1,type.eq.X),and(id.eq.2,type.eq.Y))' + }); }); it('should return the query for a single id of an rpc resource', () => { const resource = 'rpc/get_todo'; const id = 2; const query = getQuery(primaryKeySingle, id, resource); - expect(query).toEqual(qs({ id: 'eq.2' })); + expect(query).toEqual({ id: 'eq.2' }); }); it('should log to console.error that calling an rpc with multiple ids is not supported', () => { @@ -161,7 +231,7 @@ describe('getQuery', () => { const id = '[1,"X"]'; const query = getQuery(primaryKeyCompound, id, resource); - expect(query).toEqual('id=1&type=X'); + expect(query).toEqual({id: '1', type:'X'}); }); });