Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zoobot/feature/data sources #126

Merged
merged 21 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,982 changes: 863 additions & 6,119 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion server/routes/csv/csv-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ csvRouter.get('/', async (req, res) => {

const jsonData = JSON.parse(JSON.stringify(data));
const cityName = city
? city.toLowerCase().replaceAll(' ', '_')
? city.toLowerCase().replaceAll(' ', '-')
: 'all-cities';
const csvPath = `server/csv-downloads/${cityName}.csv`;
const ws = fs.createWriteStream(csvPath);
Expand Down
32 changes: 29 additions & 3 deletions server/routes/shared-routes-utils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
// function camelToSnakeCase(camelIn) {
// return camelIn.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
// }

function snakeToCamelCase(snakeIn) {
return snakeIn.replace(/([-_][a-z])/g, (group) =>
group.toUpperCase().replace('-', '').replace('_', ''),
);
}

export const convertObjectKeysToCamelCase = (obj) => {
const newObj = {};
for (const key in obj) {
newObj[snakeToCamelCase(key)] = obj[key];
}
return newObj;
};

function camelToSnakeCase(camelIn) {
return camelIn.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
return camelIn.replace(/[A-Z0-9]/g, (letter) => {
if (/[A-Z]/.test(letter)) {
return `_${letter.toLowerCase()}`;
} else if (/[0-9]/.test(letter)) {
return `_${letter}`;
} else {
return letter;
}
});
}

export default function convertObjectKeysToSnakeCase(obj) {
export const convertObjectKeysToSnakeCase = (obj) => {
const newObj = {};

// eslint-disable-next-line no-restricted-syntax
Expand All @@ -14,4 +40,4 @@ export default function convertObjectKeysToSnakeCase(obj) {
}
}
return newObj;
}
};
151 changes: 121 additions & 30 deletions server/routes/sources/sources-queries.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,155 @@
import { db, pgPromise } from '../../db/index.js';
import convertObjectKeysToSnakeCase from '../shared-routes-utils.js';
import {
convertObjectKeysToSnakeCase,
convertObjectKeysToCamelCase,
} from '../shared-routes-utils.js';

export async function createSource(data) {
// eslint-disable-next-line no-unused-vars
const { crosswalk, destinations, ...source } = data;

const dataInSnakeCase = convertObjectKeysToSnakeCase(source);

const query = `
INSERT INTO sources(\${this:name})
VALUES(\${this:csv})
RETURNING country, city, id, created
RETURNING id_source_name as "idSourceName"
`;

const response = await db.one(query, source);
const response = await db.one(query, dataInSnakeCase);
return response;
}

export async function createCrosswalk(data) {
const dataInSnakeCase = convertObjectKeysToSnakeCase(data);
const query = `
INSERT INTO crosswalk(\${this:name})
VALUES(\${this:csv})
RETURNING id
RETURNING id_source_name as "idSourceName"
`;

const response = await db.one(query, data);
const response = await db.one(query, dataInSnakeCase);
return response;
}

export async function findSourceCountry(country) {
const query = `SELECT
id, iso_alpha_3 as country, state, city,
email, contact, who, phone,
info, download, broken, broken_reason as note
FROM sources
WHERE country = $1;`;
const values = [country];
const source = await db.any(query, values);
return source;
}
const getFields = `id_source_name as "idSourceName",
iso_alpha_3 as "isoAlpha3",
country,
state,
city,
email,
contact,
phone,
info,
download,
notes,
filename,
format,
longitude,
latitude,
license,
broken`;

export async function getAllSources() {
const query = `SELECT id, iso_alpha_3 as country, state, city,
email, contact, who, phone,
info, download, broken, broken_reason as note
const query = `SELECT ${getFields}
FROM sources;`;
const source = await db.any(query);
return source;
}

export async function updateSourceById(updatedSourceData, id) {
const updatedSourceDataInSnakeCase =
convertObjectKeysToSnakeCase(updatedSourceData);
export async function getSourceByIdSourceName(idSourceName) {
const query = `SELECT ${getFields}
FROM sources
WHERE id_source_name = '${idSourceName}';`;
const source = await db.one(query);
return source;
}

export async function getCrosswalkByIdSourceName(idSourceName) {
const query = `SELECT
id_source_name as "idSourceName",
common,
species,
genus,
scientific,
family,
variety,
class,
dbh,
height,
structure,
trunks,
age,
health,
crown,
spread,
planted,
updated,
location,
note,
address,
id_reference as "idReference",
owner,
ule,
ule_min as "uleMin",
ule_max as "uleMax",
cost,
audited,
longitude,
latitude,
city,
state,
zip,
country,
neighborhood,
url,
urlimage,
status,
email,
volunteer,
notes,
legal_status as legalStatus,
irrigation,
count,
dbh_min as "dbhMin",
dbh_max as "dbhMax",
height_min as "heightMin",
height_max as "heightMax",
crown_min as "crownMin",
crown_max as "crownMax"
FROM crosswalk where id_source_name = '${idSourceName}';`;
zoobot marked this conversation as resolved.
Show resolved Hide resolved
const source = await db.one(query);
return source;
}

export async function updateSourceByIdSourceName(data) {
const dataInSnakeCase = convertObjectKeysToSnakeCase(data);
const keys = Object.keys(dataInSnakeCase);
const keysString = keys.join(', ');

const condition = pgPromise.as.format(`WHERE id = ${id} RETURNING *`);
const condition = pgPromise.as.format(
` WHERE id_source_name = '${data.idSourceName}'
RETURNING ${keysString};`,
);
const query =
pgPromise.helpers.update(
updatedSourceDataInSnakeCase,
Object.keys(updatedSourceDataInSnakeCase),
'sources',
) + condition;
const updatedSource = await db.one(query, updatedSourceDataInSnakeCase);
pgPromise.helpers.update(dataInSnakeCase, keys, 'sources') + condition;
const updatedResponse = await db.one(query, dataInSnakeCase);
const camelCaseResponse = convertObjectKeysToCamelCase(await updatedResponse);
return camelCaseResponse;
}

export async function updateCrosswalkByIdSourceName(data) {
const { id_source_name, ...dataInSnakeCase } =
convertObjectKeysToSnakeCase(data);
const keys = Object.keys(dataInSnakeCase);
const keysString = keys.join(', ');

return updatedSource;
const condition = pgPromise.as.format(
` WHERE id_source_name = '${id_source_name}' RETURNING ${keysString};`,
);
const query =
pgPromise.helpers.update(dataInSnakeCase, keys, 'crosswalk') + condition;
const updatedResponse = await db.one(query, dataInSnakeCase);
const camelCaseSource = convertObjectKeysToCamelCase(await updatedResponse);
return { idSourceName: data.idSourceName, ...camelCaseSource };
}
74 changes: 48 additions & 26 deletions server/routes/sources/sources-router.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,76 @@
import express from 'express';
import AppError from '../../errors/AppError.js';
import {
findSourceCountry,
updateSourceById,
getSourceByIdSourceName,
getCrosswalkByIdSourceName,
updateSourceByIdSourceName,
createSource,
createCrosswalk,
getAllSources,
updateCrosswalkByIdSourceName,
} from './sources-queries.js';
import validateSource from './sources-validations.js';

const sourcesRouter = express.Router();

sourcesRouter.get('/', async (req, res) => {
const { id, country } = req.query;
const { idSourceName, sources } = req.query;

const idSource = !id ? '*' : id;
const source = !country
? await getAllSources()
: await findSourceCountry(country, idSource);
res.status(200).json(source ?? {});
if (!idSourceName && sources === 'All') {
let foundSources = await getAllSources();
if (!foundSources || foundSources.length === 0)
throw new AppError(400, 'Error getting source');
res.status(200).json(foundSources);
}

if (idSourceName) {
const responseSource = await getSourceByIdSourceName(idSourceName);
const responseCrosswalk = await getCrosswalkByIdSourceName(idSourceName);
if (!responseSource) throw new AppError(400, 'Error getting source');
res
.status(200)
.json({ source: responseSource, crosswalk: responseCrosswalk });
}
});
zoobot marked this conversation as resolved.
Show resolved Hide resolved

sourcesRouter.post('/', async (req, res) => {
// eslint-disable-next-line no-unused-vars
const { crosswalk, ...source } = req.body;
const responseSource = await createSource(source);
if (!responseSource) throw new AppError(400, 'Error creating source');
const validated = await validateSource(req);
if (!validated) throw new AppError(400, 'Error validating source');

const { crosswalk = null, source = null } = req.body;
let responseSource, responseCrosswalk;
if (source) {
responseSource = await createSource(source);
if (!responseSource) throw new AppError(400, 'Error creating source');
}

if (crosswalk) {
const responseCrosswalk = await createCrosswalk({
id: source.id,
...crosswalk,
});
responseCrosswalk = await createCrosswalk(crosswalk);
if (!responseCrosswalk) throw new AppError(400, 'Error creating Crosswalk');
}

res.status(201).json(responseSource);
const response = { source: responseSource, crosswalk: responseCrosswalk };
res.status(200).json(response);
});

sourcesRouter.put('/', async (req, res) => {
const { id, ...body } = req.body;
// eslint-disable-next-line no-unused-vars
const validated = await validateSource(req);
if (!validated) throw new AppError(400, 'Error validating source');

if (!id) {
throw new AppError(
400,
'sourcesRouter.put Missing required parameter: id.',
);
const { crosswalk = null, source = null } = req.body;
let responseSource, responseCrosswalk;
if (source) {
responseSource = await updateSourceByIdSourceName(source);
if (!responseSource) throw new AppError(400, 'Error creating source');
}

const updatedSource = await updateSourceById(body, id);

res.status(200).json(updatedSource);
if (crosswalk) {
responseCrosswalk = await updateCrosswalkByIdSourceName(crosswalk);
if (!responseCrosswalk) throw new AppError(400, 'Error creating Crosswalk');
}
const response = { source: responseSource, crosswalk: responseCrosswalk };
res.status(200).json(response);
});

export default sourcesRouter;
12 changes: 12 additions & 0 deletions server/routes/sources/sources-validations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function validateSource(req) {
if (!req?.body) return false;
const { crosswalk = null, source = null } = req.body;
if (source) {
if (source?.idSourceName === undefined) return false;
}
if (crosswalk) {
if (crosswalk?.idSourceName === undefined) return false;
}

return true;
}
Loading