Skip to content

Commit

Permalink
Add nullslast and nullsfirst support to getList (#164)
Browse files Browse the repository at this point in the history
Closes #163, #162, #160
  • Loading branch information
fzaninotto committed Sep 4, 2024
1 parent a33bf93 commit b282e86
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 11 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,12 @@ const [create, { isLoading, error }] = useCreate(
```

### Null sort order

Postgrest supports specifying the position of nulls in [sort ordering](https://postgrest.org/en/v12/references/api/tables_views.html#ordering). This can be configured via an optional data provider parameter:

```jsx
import { PostgRestSortOrder, IDataProviderConfig } from '@raphiniert/ra-data-postgrest';

const config: IDataProviderConfig = {
...
sortOrder: PostgRestSortOrder.AscendingNullsLastDescendingNullsLast
Expand All @@ -207,8 +210,23 @@ const config: IDataProviderConfig = {
const dataProvider = postgrestRestProvider(config);
```

This parameter impacts the `getList` and `getManyReference` calls.

It is important to note that null positioning in sort will impact index utilization so in some cases you'll want to add corresponding index on the database side.

You can also override this parameter on a per-query basis by passing `nullsfirst: true` or `nullslast: true` in the `meta` object of the query:

```jsx
const { data, total, isLoading, error } = useGetList(
'posts',
{
pagination: { page: 1, perPage: 10 },
sort: { field: 'published_at', order: 'DESC' },
meta: { nullslast: true }
}
);
```

### 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:

Expand Down
34 changes: 33 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export const defaultPrimaryKeys = new Map<string, PrimaryKey>();

export const defaultSchema = () => '';

export { PostgRestSortOrder };

export interface IDataProviderConfig {
apiUrl: string;
httpClient: (string, Options) => Promise<any>;
Expand Down Expand Up @@ -127,7 +129,37 @@ export default (config: IDataProviderConfig): DataProvider => ({
};

if (field) {
query.order = getOrderBy(field, order, primaryKey);
query.order = getOrderBy(
field,
order,
primaryKey,
config.sortOrder
);
if (
params.meta?.nullsfirst &&
!query.order.includes('nullsfirst')
) {
query.order = query.order.includes('nullslast')
? query.order.replace('nullslast', 'nullsfirst')
: `${query.order}.nullsfirst`;
}
if (
params.meta?.nullsfirst === false &&
query.order.includes('nullsfirst')
) {
query.order = query.order.replace('.nullsfirst', '');
}
if (params.meta?.nullslast && !query.order.includes('nullslast')) {
query.order = query.order.includes('nullsfirst')
? query.order.replace('nullsfirst', 'nullslast')
: `${query.order}.nullslast`;
}
if (
params.meta?.nullslast === false &&
query.order.includes('nullslast')
) {
query.order = query.order.replace('.nullslast', '');
}
}

if (select) {
Expand Down
2 changes: 1 addition & 1 deletion src/urlBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ export const getQuery = (
return result;
};

export enum PostgRestSortOrder {
export const enum PostgRestSortOrder {
AscendingNullsLastDescendingNullsFirst = 'asc,desc',
AscendingNullsLastDescendingNullsLast = 'asc,desc.nullslast',
AscendingNullsFirstDescendingNullsFirst = 'asc.nullsfirst,desc',
Expand Down
249 changes: 249 additions & 0 deletions tests/dataProvider/getList.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { makeTestFromCase, Case } from './helper';
import { PostgRestSortOrder } from '../../src/index';

describe('getList specific', () => {
const method = 'getList';
Expand Down Expand Up @@ -50,4 +51,252 @@ describe('getList specific', () => {
];

cases.forEach(makeTestFromCase);

describe('meta', () => {
describe('nullsfirst', () => {
makeTestFromCase({
test: 'nullsfirst true added in meta changes the sort order',
method,
resource: 'posts',
params: {
pagination: {
page: 1,
perPage: 10,
},
sort: {
field: 'id',
order: 'ASC',
},
filter: {},
meta: { nullsfirst: true },
},
expectedUrl: `/posts?offset=0&limit=10&order=id.asc.nullsfirst`,
expectedOptions: {
headers: {
accept: 'application/json',
prefer: 'count=exact',
},
},
httpClientResponseHeaders: {
'content-range': '0-9/100',
},
});
makeTestFromCase({
test: 'nullsfirst true added in meta is compatible with the default sort order',
method,
config: {
sortOrder:
PostgRestSortOrder.AscendingNullsFirstDescendingNullsFirst,
},
resource: 'posts',
params: {
pagination: {
page: 1,
perPage: 10,
},
sort: {
field: 'id',
order: 'ASC',
},
filter: {},
meta: { nullsfirst: true },
},
expectedUrl: `/posts?offset=0&limit=10&order=id.asc.nullsfirst`,
expectedOptions: {
headers: {
accept: 'application/json',
prefer: 'count=exact',
},
},
httpClientResponseHeaders: {
'content-range': '0-9/100',
},
});
makeTestFromCase({
test: 'nullsfirst true added in meta overrides the default sort order',
method,
config: {
sortOrder:
PostgRestSortOrder.AscendingNullsLastDescendingNullsFirst,
},
resource: 'posts',
params: {
pagination: {
page: 1,
perPage: 10,
},
sort: {
field: 'id',
order: 'ASC',
},
filter: {},
meta: { nullsfirst: true },
},
expectedUrl: `/posts?offset=0&limit=10&order=id.asc.nullsfirst`,
expectedOptions: {
headers: {
accept: 'application/json',
prefer: 'count=exact',
},
},
httpClientResponseHeaders: {
'content-range': '0-9/100',
},
});
makeTestFromCase({
test: 'nullsfirst false added in meta overrides the default sort order',
method,
config: {
sortOrder:
PostgRestSortOrder.AscendingNullsFirstDescendingNullsFirst,
},
resource: 'posts',
params: {
pagination: {
page: 1,
perPage: 10,
},
sort: {
field: 'id',
order: 'ASC',
},
filter: {},
meta: { nullsfirst: false },
},
expectedUrl: `/posts?offset=0&limit=10&order=id.asc`,
expectedOptions: {
headers: {
accept: 'application/json',
prefer: 'count=exact',
},
},
httpClientResponseHeaders: {
'content-range': '0-9/100',
},
});
});
});

describe('nullslast', () => {
makeTestFromCase({
test: 'nullslast true added in meta changes the sort order',
method,
resource: 'posts',
params: {
pagination: {
page: 1,
perPage: 10,
},
sort: {
field: 'id',
order: 'DESC',
},
filter: {},
meta: { nullslast: true },
},
expectedUrl: `/posts?offset=0&limit=10&order=id.desc.nullslast`,
expectedOptions: {
headers: {
accept: 'application/json',
prefer: 'count=exact',
},
},
httpClientResponseHeaders: {
'content-range': '0-9/100',
},
});
makeTestFromCase({
test: 'nullslast true added in meta is compatible with the default sort order',
method,
config: {
sortOrder:
PostgRestSortOrder.AscendingNullsLastDescendingNullsLast,
},
resource: 'posts',
params: {
pagination: {
page: 1,
perPage: 10,
},
sort: {
field: 'id',
order: 'DESC',
},
filter: {},
meta: { nullslast: true },
},
expectedUrl: `/posts?offset=0&limit=10&order=id.desc.nullslast`,
expectedOptions: {
headers: {
accept: 'application/json',
prefer: 'count=exact',
},
},
httpClientResponseHeaders: {
'content-range': '0-9/100',
},
});
makeTestFromCase({
test: 'nullslast true added in meta overrides the default sort order',
method,
config: {
sortOrder:
PostgRestSortOrder.AscendingNullsFirstDescendingNullsFirst,
},
resource: 'posts',
params: {
pagination: {
page: 1,
perPage: 10,
},
sort: {
field: 'id',
order: 'DESC',
},
filter: {},
meta: { nullslast: true },
},
expectedUrl: `/posts?offset=0&limit=10&order=id.desc.nullslast`,
expectedOptions: {
headers: {
accept: 'application/json',
prefer: 'count=exact',
},
},
httpClientResponseHeaders: {
'content-range': '0-9/100',
},
});
makeTestFromCase({
test: 'nullslast false added in meta overrides the default sort order',
method,
config: {
sortOrder:
PostgRestSortOrder.AscendingNullsLastDescendingNullsLast,
},
resource: 'posts',
params: {
pagination: {
page: 1,
perPage: 10,
},
sort: {
field: 'id',
order: 'DESC',
},
filter: {},
meta: { nullslast: false },
},
expectedUrl: `/posts?offset=0&limit=10&order=id.desc`,
expectedOptions: {
headers: {
accept: 'application/json',
prefer: 'count=exact',
},
},
httpClientResponseHeaders: {
'content-range': '0-9/100',
},
});
});
});
Loading

0 comments on commit b282e86

Please sign in to comment.