Skip to content

Commit c7dc6eb

Browse files
authored
Create metastore and datastore custom hooks and resource component to use them (#4)
* rewrite hook to take parameters * Saved work * Add sort to datastore query * Rename useDatastore and add tests for transformSort * Add tests for transformConditions * Adds tests for resource and usedatastore
1 parent 17d7b94 commit c7dc6eb

17 files changed

Lines changed: 479 additions & 22 deletions

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@civicactions/data-catalog-services",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "",
55
"main": "lib/index.js",
66
"scripts": {
@@ -76,10 +76,11 @@
7676
"@babel/preset-env": "^7.5.5",
7777
"@babel/preset-react": "^7.0.0",
7878
"@babel/runtime": "^7.6.2",
79-
"@testing-library/dom": "^7.16.1",
79+
"@testing-library/dom": "^7.29.0",
8080
"@testing-library/jest-dom": "^5.10.1",
8181
"@testing-library/react": "^10.2.1",
8282
"@testing-library/react-hooks": "^3.3.0",
83+
"@testing-library/user-event": "^12.6.0",
8384
"babel-core": "^6.26.3",
8485
"babel-eslint": "^10.0.3",
8586
"babel-jest": "^24.9.0",
@@ -110,8 +111,8 @@
110111
"url-loader": "^1.1.2"
111112
},
112113
"peerDependencies": {
113-
"react": "^16.9.0",
114-
"react-dom": "^16.9.0"
114+
"react": "^16.13.1",
115+
"react-dom": "^16.13.1"
115116
},
116117
"files": [
117118
"lib",

src/Resource/helpers.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createContext } from 'react';
2+
3+
export const ResourceDispatch = createContext(null);
4+
5+
// Build columns in correct structure for Datatable component.
6+
export function prepareColumns(columns) {
7+
return columns.map((column) => ({
8+
Header: column,
9+
accessor: column,
10+
}));
11+
}

src/Resource/helpers.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { prepareColumns } from './helpers';
2+
3+
describe('prepareColumns', () => {
4+
test('transform an array into React Table columns', async () => {
5+
const testArray1 = ['my_column', 'column_2'];
6+
7+
expect(prepareColumns(testArray1)).toEqual([
8+
{Header: 'my_column', accessor: 'my_column'},
9+
{Header: 'column_2', accessor: 'column_2'},
10+
]);
11+
});
12+
});

src/Resource/index.jsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import useDatastore from '../hooks/useDatastore';
4+
import { ResourceDispatch } from './helpers';
5+
6+
const Resource = ({ distribution, rootUrl, children, options }) => {
7+
const { identifier } = distribution;
8+
const [currentPage, setCurrentPage] = useState(0);
9+
const {
10+
loading,
11+
values,
12+
columns,
13+
count,
14+
limit,
15+
offset,
16+
setResource,
17+
setLimit,
18+
setOffset,
19+
setConditions,
20+
setSort,
21+
} = useDatastore(identifier, rootUrl, options);
22+
const actions = {
23+
setResource,
24+
setLimit,
25+
setOffset,
26+
setCurrentPage,
27+
setConditions,
28+
setSort,
29+
};
30+
return (
31+
<ResourceDispatch.Provider value={{
32+
loading: loading,
33+
items: values,
34+
columns: columns,
35+
actions: actions,
36+
totalRows: count,
37+
limit: limit,
38+
offset: offset,
39+
currentPage: currentPage,
40+
}}>
41+
{(values.length)
42+
&& children
43+
}
44+
</ResourceDispatch.Provider>
45+
);
46+
}
47+
48+
Resource.defaultProps = {
49+
options: {},
50+
};
51+
52+
Resource.propTypes = {
53+
distribution: PropTypes.shape({
54+
identifier: PropTypes.string.isRequired,
55+
data: PropTypes.shape({
56+
downloadURL: PropTypes.string.isRequired,
57+
format: PropTypes.string,
58+
title: PropTypes.string,
59+
mediaType: PropTypes.string,
60+
})
61+
}).isRequired,
62+
rootUrl: PropTypes.string.isRequired,
63+
children: PropTypes.element.isRequired,
64+
options: PropTypes.shape({
65+
limit: PropTypes.number,
66+
offset: PropTypes.number,
67+
keys: PropTypes.bool,
68+
prepareColumns: PropTypes.func
69+
})
70+
};
71+
72+
export default Resource;

src/Resource/resource.test.jsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
import axios from 'axios';
3+
import {act} from 'react-dom/test-utils';
4+
import { render, screen } from '@testing-library/react';
5+
import userEvent from '@testing-library/user-event'
6+
import '@testing-library/jest-dom/extend-expect';
7+
import Resource from './index';
8+
import { ResourceDispatch } from './helpers';
9+
10+
jest.mock('axios');
11+
const rootUrl = 'http://dkan.com/api/1';
12+
const data = {
13+
data: {
14+
results: [{record_id: '1', column_1: 'fizz', column_2: 'dkan'}],
15+
count: '1'
16+
}
17+
}
18+
const distribution = {
19+
identifier: "1234-1234",
20+
data: {
21+
downloadURL: `${rootUrl}/files/file.csv`,
22+
format: "csv",
23+
title: "Dist Title"
24+
}
25+
}
26+
27+
const MyTestComponent = () => {
28+
const { totalRows, items, actions, limit, offset } = React.useContext(ResourceDispatch)
29+
const { setLimit, setOffset } = actions;
30+
return (
31+
<div>
32+
<p>{items[0].column_1} and {totalRows} and {limit} and {offset}</p>
33+
<button onClick={() => setLimit(25)}>Up Limit</button>
34+
<button onClick={() => setOffset(10)}>Up Offset</button>
35+
</div>
36+
)
37+
}
38+
39+
describe('<Resource />', () => {
40+
test('renders data', async () => {
41+
await act(async () => {
42+
await axios.post.mockImplementation(() => Promise.resolve(data));
43+
render(
44+
<Resource
45+
distribution={distribution}
46+
rootUrl={rootUrl}
47+
>
48+
<MyTestComponent />
49+
</Resource>
50+
);
51+
});
52+
expect(screen.getByText('fizz and 1 and 20 and 0')).toBeInTheDocument();
53+
await act(async () => {
54+
userEvent.click(screen.getByText('Up Limit'));
55+
});
56+
expect(screen.getByText('fizz and 1 and 25 and 0')).toBeInTheDocument();
57+
await act(async () => {
58+
userEvent.click(screen.getByText('Up Offset'));
59+
});
60+
expect(screen.getByText('fizz and 1 and 25 and 10')).toBeInTheDocument();
61+
62+
});
63+
});

src/hooks/useDatastore/fetch.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import axios from 'axios';
2+
3+
export async function fetchDataFromQuery(id, rootUrl, options) {
4+
const { keys, limit, offset, conditions, sort, prepareColumns, setValues, setCount, setColumns, setLoading } = options;
5+
if(!id) {
6+
// TODO: Throw error
7+
return false;
8+
}
9+
if(typeof setLoading === 'function') {
10+
setLoading(true);
11+
}
12+
return await axios.post(`${rootUrl}/datastore/query/?`, {
13+
resources: [{id: id, alias: 't'}],
14+
keys: keys,
15+
limit: limit,
16+
offset: offset,
17+
conditions: conditions,
18+
sort: sort,
19+
})
20+
.then((res) => {
21+
const { data } = res;
22+
setValues(data.results),
23+
setCount(data.count)
24+
if(data.results.length) {
25+
setColumns(prepareColumns ? prepareColumns(Object.keys(data.results[0])) : Object.keys(data.results[0]))
26+
}
27+
if(typeof setLoading === 'function') {
28+
setLoading(false);
29+
}
30+
return data;
31+
})
32+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import axios from 'axios';
2+
import { fetchDataFromQuery } from './fetch';
3+
4+
jest.mock('axios');
5+
const rootUrl = 'http://dkan.com/api/1';
6+
const data = {
7+
data: {
8+
results: [{record_id: '1', column_1: 'fizz', column_2: 'dkan'}],
9+
count: '1'
10+
}
11+
}
12+
const distribution = {
13+
identifier: "1234-1234",
14+
data: {
15+
downloadURL: `${rootUrl}/files/file.csv`,
16+
format: "csv",
17+
title: "Dist Title"
18+
}
19+
}
20+
21+
describe('fetchDataFromQuery', () => {
22+
test('returns data from datastore query endpoint', async () => {
23+
axios.post.mockImplementation(() => Promise.resolve(data));
24+
const results = await fetchDataFromQuery(distribution.identifier, rootUrl, {
25+
keys: true,
26+
limit: 20,
27+
offset: 0,
28+
conditions: [],
29+
sort: {asc: [], desc: []},
30+
setValues: () => {},
31+
setCount: () => {},
32+
setColumns: () => {}
33+
})
34+
expect(results.count).toEqual(data.data.count);
35+
expect(results.results).toEqual(data.data.results);
36+
})
37+
});

src/hooks/useDatastore/index.jsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {useState, useEffect} from 'react';
2+
import { fetchDataFromQuery } from './fetch';
3+
4+
const useDatastore = (resourceId, rootAPIUrl, options) => {
5+
const keys = options.keys ? options.keys : true;
6+
const { prepareColumns } = options;
7+
const [values, setValues] = useState([]);
8+
const [id, setResource] = useState(resourceId);
9+
const [rootUrl, setRootUrl] = useState(rootAPIUrl);
10+
const [limit, setLimit] = useState(options.limit ? options.limit : 20);
11+
const [count, setCount] = useState(null);
12+
const [columns, setColumns] = useState([]);
13+
const [offset, setOffset] = useState(options.offset ? options.offset : 0);
14+
const [loading, setLoading] = useState(false);
15+
const [conditions, setConditions] = useState()
16+
const [sort, setSort] = useState()
17+
// const [joins, setJoins] = useState()
18+
// const [properties, setProperties] = useState()
19+
20+
useEffect(() => {
21+
if(!loading) {
22+
fetchDataFromQuery(id, rootUrl,
23+
{ keys, limit, offset, conditions, sort, prepareColumns, setValues, setCount, setColumns, setLoading}
24+
)
25+
}
26+
}, [id, rootUrl, limit, offset, conditions, sort])
27+
28+
return {
29+
loading,
30+
values,
31+
count,
32+
columns,
33+
limit,
34+
offset,
35+
setResource,
36+
setRootUrl,
37+
setLimit,
38+
setOffset,
39+
setConditions,
40+
setSort,
41+
}
42+
}
43+
44+
export default useDatastore;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// OPERATORS
2+
// =
3+
// <> not equal to
4+
// BETWEEN
5+
// IN
6+
// NOT IN
7+
// >=
8+
// <=
9+
// like
10+
11+
export function transformTableFilterToQueryCondition(filterArray) {
12+
const conditions = filterArray.map((f) => {
13+
return {
14+
resource: 't',
15+
property: f.id,
16+
value: `%${f.value}%`,
17+
operator: 'LIKE',
18+
}
19+
});
20+
return conditions;
21+
}
22+
23+
export function transformTableFilterToSQLCondition(filterArray) {
24+
if(!filterArray || filterArray.length === 0) {
25+
return '';
26+
}
27+
28+
const where_clauses = [];
29+
filterArray.forEach((v, i) => {
30+
// Switch delimiter to, and strip any double-quote for Dkan2's sql query.
31+
let value = `%25${v.value}%25`;
32+
where_clauses[i] = `${v.id} = "${v.value.replace('"', '')}"`;
33+
});
34+
return `[WHERE ${where_clauses.join(' AND ')}]`;
35+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { transformTableFilterToQueryCondition } from './transformConditions';
2+
3+
describe('transformTableFilterToQueryCondition', () => {
4+
test('transform an array from of filters from React Table into DKAN query format', async () => {
5+
const testArray1 = [{id: "my_label", value: 'abcd'}];
6+
const testArray2 = [{id: "my_label", value: 'abcd'}, {id: "another_label", value: '1234'}];
7+
8+
expect(transformTableFilterToQueryCondition(testArray1)).toEqual([
9+
{resource: 't', property: 'my_label', value: '%abcd%', operator: 'LIKE'}
10+
]);
11+
expect(transformTableFilterToQueryCondition(testArray2)).toEqual([
12+
{resource: 't', property: 'my_label', value: '%abcd%', operator: 'LIKE'},
13+
{resource: 't', property: 'another_label', value: '%1234%', operator: 'LIKE'}
14+
]);
15+
});
16+
});

0 commit comments

Comments
 (0)