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

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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;
}
};
180 changes: 142 additions & 38 deletions server/routes/sources/sources-queries.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,168 @@
import { db, pgPromise } from '../../db/index.js';
import convertObjectKeysToSnakeCase from '../shared-routes-utils.js';

import {
convertObjectKeysToSnakeCase,
convertObjectKeysToCamelCase,
} from '../shared-routes-utils.js';

const SOURCE_FIELDS = `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`;

const CROSSWALK_FIELDS = `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"`;

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
`;
INSERT INTO sources(\${this:name})
VALUES(\${this:csv})
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
`;
INSERT INTO crosswalk(\${this:name})
VALUES(\${this:csv})
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);
export async function getAllSources() {
const query = {
name: 'find-sources',
text: `SELECT ${SOURCE_FIELDS} FROM sources;`,
};
const source = await db.any(query);
return source;
}

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
FROM sources;`;
const source = await db.any(query);
export async function getSourceByIdSourceName(idSourceName) {
const query = {
name: 'find-source',
text: `SELECT ${SOURCE_FIELDS}
FROM sources
WHERE id_source_name = $1`,
values: idSourceName,
};

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

export async function getCrosswalkByIdSourceName(idSourceName) {
const query = {
name: 'find-crosswalk',
text: `SELECT ${CROSSWALK_FIELDS}
FROM crosswalk where id_source_name = $1`,
values: idSourceName,
};
const source = await db.one(query);
return source;
}

export async function updateSourceById(updatedSourceData, id) {
const updatedSourceDataInSnakeCase =
convertObjectKeysToSnakeCase(updatedSourceData);
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_source_name = '${data.idSourceName}'
RETURNING ${keysString};`,
);
const query =
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(', ');

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

return updatedSource;
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 });
}
});

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