Skip to content

Commit

Permalink
Create metastore and datastore custom hooks and resource component to…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
dgading authored Dec 23, 2020
1 parent 17d7b94 commit c7dc6eb
Show file tree
Hide file tree
Showing 17 changed files with 479 additions and 22 deletions.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@civicactions/data-catalog-services",
"version": "0.1.1",
"version": "0.1.2",
"description": "",
"main": "lib/index.js",
"scripts": {
Expand Down Expand Up @@ -76,10 +76,11 @@
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.6.2",
"@testing-library/dom": "^7.16.1",
"@testing-library/dom": "^7.29.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.2.1",
"@testing-library/react-hooks": "^3.3.0",
"@testing-library/user-event": "^12.6.0",
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
Expand Down Expand Up @@ -110,8 +111,8 @@
"url-loader": "^1.1.2"
},
"peerDependencies": {
"react": "^16.9.0",
"react-dom": "^16.9.0"
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"files": [
"lib",
Expand Down
11 changes: 11 additions & 0 deletions src/Resource/helpers.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext } from 'react';

export const ResourceDispatch = createContext(null);

// Build columns in correct structure for Datatable component.
export function prepareColumns(columns) {
return columns.map((column) => ({
Header: column,
accessor: column,
}));
}
12 changes: 12 additions & 0 deletions src/Resource/helpers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { prepareColumns } from './helpers';

describe('prepareColumns', () => {
test('transform an array into React Table columns', async () => {
const testArray1 = ['my_column', 'column_2'];

expect(prepareColumns(testArray1)).toEqual([
{Header: 'my_column', accessor: 'my_column'},
{Header: 'column_2', accessor: 'column_2'},
]);
});
});
72 changes: 72 additions & 0 deletions src/Resource/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import useDatastore from '../hooks/useDatastore';
import { ResourceDispatch } from './helpers';

const Resource = ({ distribution, rootUrl, children, options }) => {
const { identifier } = distribution;
const [currentPage, setCurrentPage] = useState(0);
const {
loading,
values,
columns,
count,
limit,
offset,
setResource,
setLimit,
setOffset,
setConditions,
setSort,
} = useDatastore(identifier, rootUrl, options);
const actions = {
setResource,
setLimit,
setOffset,
setCurrentPage,
setConditions,
setSort,
};
return (
<ResourceDispatch.Provider value={{
loading: loading,
items: values,
columns: columns,
actions: actions,
totalRows: count,
limit: limit,
offset: offset,
currentPage: currentPage,
}}>
{(values.length)
&& children
}
</ResourceDispatch.Provider>
);
}

Resource.defaultProps = {
options: {},
};

Resource.propTypes = {
distribution: PropTypes.shape({
identifier: PropTypes.string.isRequired,
data: PropTypes.shape({
downloadURL: PropTypes.string.isRequired,
format: PropTypes.string,
title: PropTypes.string,
mediaType: PropTypes.string,
})
}).isRequired,
rootUrl: PropTypes.string.isRequired,
children: PropTypes.element.isRequired,
options: PropTypes.shape({
limit: PropTypes.number,
offset: PropTypes.number,
keys: PropTypes.bool,
prepareColumns: PropTypes.func
})
};

export default Resource;
63 changes: 63 additions & 0 deletions src/Resource/resource.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import axios from 'axios';
import {act} from 'react-dom/test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom/extend-expect';
import Resource from './index';
import { ResourceDispatch } from './helpers';

jest.mock('axios');
const rootUrl = 'http://dkan.com/api/1';
const data = {
data: {
results: [{record_id: '1', column_1: 'fizz', column_2: 'dkan'}],
count: '1'
}
}
const distribution = {
identifier: "1234-1234",
data: {
downloadURL: `${rootUrl}/files/file.csv`,
format: "csv",
title: "Dist Title"
}
}

const MyTestComponent = () => {
const { totalRows, items, actions, limit, offset } = React.useContext(ResourceDispatch)
const { setLimit, setOffset } = actions;
return (
<div>
<p>{items[0].column_1} and {totalRows} and {limit} and {offset}</p>
<button onClick={() => setLimit(25)}>Up Limit</button>
<button onClick={() => setOffset(10)}>Up Offset</button>
</div>
)
}

describe('<Resource />', () => {
test('renders data', async () => {
await act(async () => {
await axios.post.mockImplementation(() => Promise.resolve(data));
render(
<Resource
distribution={distribution}
rootUrl={rootUrl}
>
<MyTestComponent />
</Resource>
);
});
expect(screen.getByText('fizz and 1 and 20 and 0')).toBeInTheDocument();
await act(async () => {
userEvent.click(screen.getByText('Up Limit'));
});
expect(screen.getByText('fizz and 1 and 25 and 0')).toBeInTheDocument();
await act(async () => {
userEvent.click(screen.getByText('Up Offset'));
});
expect(screen.getByText('fizz and 1 and 25 and 10')).toBeInTheDocument();

});
});
32 changes: 32 additions & 0 deletions src/hooks/useDatastore/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import axios from 'axios';

export async function fetchDataFromQuery(id, rootUrl, options) {
const { keys, limit, offset, conditions, sort, prepareColumns, setValues, setCount, setColumns, setLoading } = options;
if(!id) {
// TODO: Throw error
return false;
}
if(typeof setLoading === 'function') {
setLoading(true);
}
return await axios.post(`${rootUrl}/datastore/query/?`, {
resources: [{id: id, alias: 't'}],
keys: keys,
limit: limit,
offset: offset,
conditions: conditions,
sort: sort,
})
.then((res) => {
const { data } = res;
setValues(data.results),
setCount(data.count)
if(data.results.length) {
setColumns(prepareColumns ? prepareColumns(Object.keys(data.results[0])) : Object.keys(data.results[0]))
}
if(typeof setLoading === 'function') {
setLoading(false);
}
return data;
})
}
37 changes: 37 additions & 0 deletions src/hooks/useDatastore/fetch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import axios from 'axios';
import { fetchDataFromQuery } from './fetch';

jest.mock('axios');
const rootUrl = 'http://dkan.com/api/1';
const data = {
data: {
results: [{record_id: '1', column_1: 'fizz', column_2: 'dkan'}],
count: '1'
}
}
const distribution = {
identifier: "1234-1234",
data: {
downloadURL: `${rootUrl}/files/file.csv`,
format: "csv",
title: "Dist Title"
}
}

describe('fetchDataFromQuery', () => {
test('returns data from datastore query endpoint', async () => {
axios.post.mockImplementation(() => Promise.resolve(data));
const results = await fetchDataFromQuery(distribution.identifier, rootUrl, {
keys: true,
limit: 20,
offset: 0,
conditions: [],
sort: {asc: [], desc: []},
setValues: () => {},
setCount: () => {},
setColumns: () => {}
})
expect(results.count).toEqual(data.data.count);
expect(results.results).toEqual(data.data.results);
})
});
44 changes: 44 additions & 0 deletions src/hooks/useDatastore/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {useState, useEffect} from 'react';
import { fetchDataFromQuery } from './fetch';

const useDatastore = (resourceId, rootAPIUrl, options) => {
const keys = options.keys ? options.keys : true;
const { prepareColumns } = options;
const [values, setValues] = useState([]);
const [id, setResource] = useState(resourceId);
const [rootUrl, setRootUrl] = useState(rootAPIUrl);
const [limit, setLimit] = useState(options.limit ? options.limit : 20);
const [count, setCount] = useState(null);
const [columns, setColumns] = useState([]);
const [offset, setOffset] = useState(options.offset ? options.offset : 0);
const [loading, setLoading] = useState(false);
const [conditions, setConditions] = useState()
const [sort, setSort] = useState()
// const [joins, setJoins] = useState()
// const [properties, setProperties] = useState()

useEffect(() => {
if(!loading) {
fetchDataFromQuery(id, rootUrl,
{ keys, limit, offset, conditions, sort, prepareColumns, setValues, setCount, setColumns, setLoading}
)
}
}, [id, rootUrl, limit, offset, conditions, sort])

return {
loading,
values,
count,
columns,
limit,
offset,
setResource,
setRootUrl,
setLimit,
setOffset,
setConditions,
setSort,
}
}

export default useDatastore;
35 changes: 35 additions & 0 deletions src/hooks/useDatastore/transformConditions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// OPERATORS
// =
// <> not equal to
// BETWEEN
// IN
// NOT IN
// >=
// <=
// like

export function transformTableFilterToQueryCondition(filterArray) {
const conditions = filterArray.map((f) => {
return {
resource: 't',
property: f.id,
value: `%${f.value}%`,
operator: 'LIKE',
}
});
return conditions;
}

export function transformTableFilterToSQLCondition(filterArray) {
if(!filterArray || filterArray.length === 0) {
return '';
}

const where_clauses = [];
filterArray.forEach((v, i) => {
// Switch delimiter to, and strip any double-quote for Dkan2's sql query.
let value = `%25${v.value}%25`;
where_clauses[i] = `${v.id} = "${v.value.replace('"', '')}"`;
});
return `[WHERE ${where_clauses.join(' AND ')}]`;
}
16 changes: 16 additions & 0 deletions src/hooks/useDatastore/transformConditions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { transformTableFilterToQueryCondition } from './transformConditions';

describe('transformTableFilterToQueryCondition', () => {
test('transform an array from of filters from React Table into DKAN query format', async () => {
const testArray1 = [{id: "my_label", value: 'abcd'}];
const testArray2 = [{id: "my_label", value: 'abcd'}, {id: "another_label", value: '1234'}];

expect(transformTableFilterToQueryCondition(testArray1)).toEqual([
{resource: 't', property: 'my_label', value: '%abcd%', operator: 'LIKE'}
]);
expect(transformTableFilterToQueryCondition(testArray2)).toEqual([
{resource: 't', property: 'my_label', value: '%abcd%', operator: 'LIKE'},
{resource: 't', property: 'another_label', value: '%1234%', operator: 'LIKE'}
]);
});
});
14 changes: 14 additions & 0 deletions src/hooks/useDatastore/transformSorts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function transformTableSortToQuerySort(sortArray) {
let newQuery = {
asc: [],
desc: [],
}
sortArray.forEach((s) => {
if (s.desc) {
return newQuery.desc.push(s.id)
} else {
return newQuery.asc.push(s.id)
}
})
return newQuery;
}
12 changes: 12 additions & 0 deletions src/hooks/useDatastore/transformSorts.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { transformTableSortToQuerySort } from './transformSorts';

describe('transformTableSortToQuerySort', () => {
test('transform an array from of sorts from React Table into DKAN query format', async () => {
const testArray1 = [{id: "my_label", desc: true}]
const testArray2 = [{id: "another_label", desc: false}]

expect(transformTableSortToQuerySort(testArray1)).toEqual({asc: [], desc:['my_label']});
expect(transformTableSortToQuerySort(testArray2)).toEqual({asc: ['another_label'], desc:[]});
expect(transformTableSortToQuerySort(testArray1.concat(testArray2))).toEqual({asc: ['another_label'], desc:['my_label']});
});
});
Loading

0 comments on commit c7dc6eb

Please sign in to comment.