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

refactor #331

Merged
merged 49 commits into from
Feb 20, 2021
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6767f24
refactor: use a flag rather than querying latest to find current subs.
issa-tseng Jan 19, 2021
d6cfe40
improve: parse out instanceNames as they are given, and backfill.
issa-tseng Jan 21, 2021
bfbce35
wip: transition to slonik, part 1.
issa-tseng Jan 25, 2021
629959b
wip: most (?) query module changes complete. nothing works.
issa-tseng Jan 29, 2021
ce93112
wip: more query module changes.
issa-tseng Jan 29, 2021
e285b84
wip: began reworking instances; removed most of the trivial instances.
issa-tseng Jan 30, 2021
e160f0a
wip: deprecated more instances, merged more code. freed crypto.
issa-tseng Jan 31, 2021
f0a3638
wip: finished initial model rewrite. addressed lint. other changes.
issa-tseng Feb 1, 2021
13c339f
wip: consolidate some more things, delete some more code.
issa-tseng Feb 1, 2021
d862b24
wip: fix migrations, a lot of other random issues. starts clean.
issa-tseng Feb 2, 2021
5a5cba3
wip: make the unit tests work again.
issa-tseng Feb 2, 2021
a8458c2
wip: it starts up, and it tries successfully to run api tests now.
issa-tseng Feb 3, 2021
1022e2c
wip: 11 passing api tests.
issa-tseng Feb 3, 2021
2713dd1
wip: 25 passing tests (all of api/session). reworked actees, audits.
issa-tseng Feb 4, 2021
cab8c05
wip: fix the unit tests again, and mark skip on nonworking tests.
issa-tseng Feb 4, 2021
04a7a9b
wip: 573 tests passing (+53 from api/users).
issa-tseng Feb 5, 2021
4253a46
wip: 636 passing tests. assignments tests work now. some lint cleaning.
issa-tseng Feb 5, 2021
e23ec11
wip: 655 tests passing. backup and config working now.
issa-tseng Feb 5, 2021
6f8afdb
wip: 720 passing tests. made projects work, which forced others to work.
issa-tseng Feb 6, 2021
00ec8e5
wip: 856 passing tests. most of forms but some regressions.
issa-tseng Feb 7, 2021
183f4d6
wip: 905 passing tests. all of forms works now.
issa-tseng Feb 7, 2021
d3a32fa
wip: 1012 passing tests. cleaned up almost everything but submissions.
issa-tseng Feb 8, 2021
8ce5f20
wip: 1031 passing tests. really just submissions left now.
issa-tseng Feb 8, 2021
fcea9b5
wip: 1180 tests passing. main submissions tests pass now.
issa-tseng Feb 9, 2021
839adf8
wip: 1232 tests pass. 22 to go.
issa-tseng Feb 9, 2021
4c9ec9c
refactor: complete refactor. hopefully nothing changed.
issa-tseng Feb 9, 2021
1fcdfbc
noop: cleanup; trim deadcode, remove some logging.
issa-tseng Feb 9, 2021
2d9616c
noop: move everything around!, pt2, pt1: fixup "instance"/"frame" names.
issa-tseng Feb 9, 2021
eca4b52
noop: move everything around!, pt2, pt2: reorganize third party shims.
issa-tseng Feb 9, 2021
f7858ff
noop: finally cave and just add global.tap to production.
issa-tseng Feb 9, 2021
c5e08ea
noop: move everything around!, pt2, pt3: separate mail into messages/io.
issa-tseng Feb 9, 2021
5b51274
noop: move everything around!, pt2, pt4: move outbound to formats.
issa-tseng Feb 9, 2021
321d03a
noop: move everything around!, pt2, pt5: rename json.js to odata.js.
issa-tseng Feb 9, 2021
0e5adf4
noop: minor property rename from the refactor.
issa-tseng Feb 9, 2021
1b4956b
test: restore a test that was removed during the refactor.
issa-tseng Feb 9, 2021
cf51f7b
test: unit tests for frame, and remove a bunch of excess code.
issa-tseng Feb 9, 2021
47558a0
test: extender and unjoiner db tests.
issa-tseng Feb 9, 2021
dcd6863
test: last few util/db tests for the refactor.
issa-tseng Feb 9, 2021
dde3a85
improve: boost application performance by 40% with one config flag!
issa-tseng Feb 9, 2021
8f360d1
noop: split attachment handling from query/forms to match submissions.
issa-tseng Feb 10, 2021
63fd9c4
noop: linter.
issa-tseng Feb 10, 2021
1f0f052
noop: some code review things. nothing should have changed.
issa-tseng Feb 12, 2021
71307a3
new: basic submission versioning functionality via POST /submission
issa-tseng Feb 12, 2021
45089e1
test: ensure that all test instanceIDs are globally unique.
issa-tseng Feb 12, 2021
f7aff52
improve: restrict instanceId uniq constraint to form; allow edit upsert.
issa-tseng Feb 13, 2021
e09ea98
noop: a lot of code cleanup from code review.
issa-tseng Feb 13, 2021
7ae9f18
noop: more code review changes. everything should be ready.
issa-tseng Feb 13, 2021
9d47408
improve: minor code cleanup after code review.
issa-tseng Feb 19, 2021
0714348
build: update slonik to pass the tests.
issa-tseng Feb 20, 2021
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
2 changes: 2 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"import/order": "off",
"lines-between-class-members": "off",
"max-len": "off",
"no-continue": "off",
"no-debugger": "off",
"no-else-return": "off",
"no-multiple-empty-lines": "off",
Expand All @@ -29,6 +30,7 @@
"operator-linebreak": "off",
"padded-blocks": "off",
"prefer-template": "off",
"space-infix-ops": "off",
"spaced-comment": "off"
}
}
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ node_modules:
npm install

migrations: node_modules
node -e 'const { withDatabase, migrate } = require("./lib/model/database"); withDatabase(require("config").get("default.database"))(migrate);'
node -e 'const { withDatabase, migrate } = require("./lib/model/migrate"); withDatabase(require("config").get("default.database"))(migrate);'

base: node_modules migrations

Expand Down
2 changes: 1 addition & 1 deletion lib/bin/backup.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const { initDrive, ensureDirectory, uploadFile, persistCredentials } = require('
run(emailing('backupFailed', auditing('backup', async () => {
// fetch backup config. fail early and silently unless it exists.
let config;
try { config = await getConfiguration('backups.main'); } catch { return 'no backup configured'; }
try { config = await getConfiguration('backups.main'); } catch (_) { return 'no backup configured'; }
const configValue = JSON.parse(config.value);

// run the pgdump and encrypt it into a zipfile.
Expand Down
27 changes: 15 additions & 12 deletions lib/bin/run-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,39 @@ const { merge } = require('ramda');
const config = require('config');
const exit = require('express-graceful-exit');

global.tap = (x) => { console.log(x); return x; }; // eslint-disable-line no-console

////////////////////////////////////////////////////////////////////////////////
// CONTAINER SETUP

// initialize our top-level static database instance.
const { connect } = require('../model/database');
const db = connect(config.get('default.database'));
// initialize our slonik connection pool.
const { slonikPool } = require('../external/slonik');
const db = slonikPool(config.get('default.database'));

// set up our mailer.
const env = config.get('default.env');
const { mailer } = require('../outbound/mail');
const { mailer } = require('../external/mail');
const mail = mailer(merge(config.get('default.email'), { env }));

// get a google client.
const googler = require('../outbound/google');
const googler = require('../external/google');
const google = googler(config.get('default.external.google'));

// get a sentry and xlsform client, and a crypto module.
const Sentry = require('../util/sentry').init(config.get('default.external.sentry'));
const xlsform = require('../util/xlsform').init(config.get('default.xlsform'));
const crypto = require('../util/crypto');
// get a sentry and xlsform client, and a password module.
const Sentry = require('../external/sentry').init(config.get('default.external.sentry'));
const xlsform = require('../external/xlsform').init(config.get('default.xlsform'));
const bcrypt = require('../util/crypto').password(require('bcrypt'));

// get an Enketo client
const enketo = require('../util/enketo').init(config.get('default.enketo'));
const enketo = require('../external/enketo').init(config.get('default.enketo'));


////////////////////////////////////////////////////////////////////////////////
// START HTTP SERVICE

// initialize our container, then generate an http service out of it.
const container = require('../model/package').withDefaults({ db, mail, env, google, Sentry, crypto, xlsform, enketo });
const container = require('../model/container')
.withDefaults({ db, mail, env, google, Sentry, bcrypt, xlsform, enketo });
const service = require('../http/service')(container);

// insert the graceful exit middleware.
Expand Down Expand Up @@ -77,7 +80,7 @@ const term = () => {
exit.gracefulExitHandler(service, server, {
log: true,
exitProcess: false,
callback: () => db.destroy(() => process.exit(0))
callback: () => db.end().then(() => process.exit(0))
});
};

Expand Down
4 changes: 3 additions & 1 deletion lib/data/attachments.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ const streamAttachments = (inStream, decryptor) => {
const writable = new Writable({
objectMode: true,
highWaterMark: 5, // the default is 16, we'll be a little more conservative.
write(att, _, done) {
write(x, _, done) {
const att = x.row;

// this sanitization means that two filenames could end up identical.
// luckily, this is not actually illegal in the zip spec; two files can live at precisely
// the same location, and the conflict is dealt with interactively by the unzipping client.
Expand Down
15 changes: 7 additions & 8 deletions lib/data/briefcase.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,8 @@ const streamBriefcaseCsvs = (inStream, inFields, xmlFormId, decryptor, rootOnly
let rootHeaderSent = false;
const rootStream = new Transform({
objectMode: true,
transform(row, _, done) {
transform(submission, _, done) {
try {
const { submission } = row; // just save some syntax

// send header if we have to. i wish there were a cleaner way to do this.
if (rootHeaderSent === false) {
this.push(rootHeader);
Expand All @@ -258,10 +256,11 @@ const streamBriefcaseCsvs = (inStream, inFields, xmlFormId, decryptor, rootOnly
// if we have encData instead of xml, we must decrypt before we can read data.
// the decryptor will return null if it does not have a decryption key for the
// record. this is okay; we pass the null through and processRow deals with it.
const { encryption } = submission.aux;
const xml =
(row.localKey == null) ? row.xml :
(row.encHasData === false) ? 'missing' : // eslint-disable-line indent
decryptor(row.encData, row.encKeyId, row.localKey, submission.instanceId, row.encIndex); // eslint-disable-line indent
(submission.def.localKey == null) ? submission.xml :
(encryption.encHasData === false) ? 'missing' : // eslint-disable-line indent
decryptor(encryption.encData, encryption.encKeyId, submission.def.localKey, submission.instanceId, encryption.encIndex); // eslint-disable-line indent

// if something about the xml didn't work so well, we can figure out what
// to say and bail out early.
Expand All @@ -270,13 +269,13 @@ const streamBriefcaseCsvs = (inStream, inFields, xmlFormId, decryptor, rootOnly
(xml === null) ? 'not decrypted' : null; // eslint-disable-line indent
if (status != null) {
const result = [];
writeMetadata(result, rootMeta, submission, row.submitter, row.attachments, status);
writeMetadata(result, rootMeta, submission, submission.aux.submitter, submission.aux.attachment, status);
return done(null, result);
}

// write the root row we get back from parsing the xml.
processRow(xml, submission.instanceId, fields, rootOnly).then((result) => {
writeMetadata(result, rootMeta, submission, row.submitter, row.attachments);
writeMetadata(result, rootMeta, submission, submission.aux.submitter, submission.aux.attachment);
done(null, result);
}, done); // pass through errors.
} catch (ex) { done(ex); }
Expand Down
7 changes: 5 additions & 2 deletions lib/data/client-audits.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ for (const header of headers) headerLookup[header] = true;
//
// TODO: if the csv is ragged our behaviour is somewhat undefined.
const parseClientAudits = (buffer) => {
const { ClientAudit } = require('../model/frames'); // TODO: better to eliminate the cyclic require.
const parser = parse(buffer, parseOptions);
const audits = [];

Expand All @@ -45,9 +46,9 @@ const parseClientAudits = (buffer) => {
// and now set ourselves up to actually process each cell of each row.
parser.on('data', (row) => {
const audit = { remainder: {} };
audits.push(audit);
for (let idx = 0; (idx < row.length) && (idx < names.length); idx += 1)
(known[idx] ? audit : audit.remainder)[names[idx]] = row[idx];
audits.push(new ClientAudit(audit));
});
});

Expand All @@ -71,7 +72,9 @@ const streamClientAudits = (inStream, form) => {
let first = true;
const csvifier = new Transform({
objectMode: true,
transform(data, _, done) {
transform(x, _, done) {
const data = x.row;

// TODO: we do not currently try/catch this block because it feels low risk.
// this may not actually be the case..
if (first === true) {
Expand Down
81 changes: 34 additions & 47 deletions lib/data/odata-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,68 +7,60 @@
// including this file, may be copied, modified, propagated, or distributed
// except according to the terms contained in the LICENSE file.

const { identity } = require('ramda');
const { sql } = require('slonik');
const { raw } = require('slonik-sql-tag-raw');
const odataParser = require('odata-v4-parser');
const Problem = require('../util/problem');

////////////////////////////////////////
// AST NODE TRANSFORMATION

const extractFunctions = [ 'year', 'month', 'day', 'hour', 'minute', 'second' ];
const methodCall = (fn, params, sql, bindings) => {
const methodCall = (fn, params) => {
// n.b. odata-v4-parser appears to already validate function name and arity.
const lowerName = fn.toLowerCase();
if (extractFunctions.includes(lowerName)) {
sql.push(`extract(${lowerName} from `);
op(params[0], sql, bindings); // eslint-disable-line no-use-before-define
sql.push(')');
} else if (fn === 'now') {
sql.push('now()');
}
};
const binaryOp = (left, right, operator, sql, bindings) => {
sql.push('('); // always explicitly express the original AST op precedence for safety.
op(left, sql, bindings); // eslint-disable-line no-use-before-define
sql.push(` ${operator} `);
op(right, sql, bindings); // eslint-disable-line no-use-before-define
sql.push(')');
if (extractFunctions.includes(lowerName))
return sql`extract(${raw(lowerName)} from ${op(params[0])})`; // eslint-disable-line no-use-before-define
else if (fn === 'now')
return sql`now()`;
};
const op = (node, sql, bindings) => {
const binaryOp = (left, right, operator) =>
// always use parens to ensure the original AST op precedence.
sql`(${op(left)} ${raw(operator)} ${op(right)})`; // eslint-disable-line no-use-before-define

const op = (node) => {
if (node.type === 'FirstMemberExpression') {
if (node.raw === '__system/submissionDate') {
sql.push('??');
bindings.push('submissions.createdAt'); // TODO: HACK HACK
return sql.identifier([ 'submissions', 'createdAt' ]); // TODO: HACK HACK
} else if (node.raw === '__system/submitterId') {
sql.push('??');
bindings.push('submissions.submitterId'); // TODO: HACK HACK
return sql.identifier([ 'submissions', 'submitterId' ]); // TODO: HACK HACK
} else if (node.raw === '__system/reviewStatus') {
return sql.identifier([ 'submissions', 'reviewStatus' ]); // TODO: HACK HACK
} else {
throw Problem.internal.unsupportedODataField({ at: node.position, text: node.raw });
}
} else if (node.type === 'Literal') {
sql.push('?');
bindings.push(node.raw);
return node.raw;
} else if (node.type === 'MethodCallExpression') {
methodCall(node.value.method, node.value.parameters, sql, bindings);
return methodCall(node.value.method, node.value.parameters);
} else if (node.type === 'EqualsExpression') {
binaryOp(node.value.left, node.value.right, '=', sql, bindings);
return binaryOp(node.value.left, node.value.right, '=');
} else if (node.type === 'NotEqualsExpression') {
binaryOp(node.value.left, node.value.right, '!=', sql, bindings);
return binaryOp(node.value.left, node.value.right, '!=');
} else if (node.type === 'LesserThanExpression') {
binaryOp(node.value.left, node.value.right, '<', sql, bindings);
return binaryOp(node.value.left, node.value.right, '<');
} else if (node.type === 'LesserOrEqualsExpression') {
binaryOp(node.value.left, node.value.right, '<=', sql, bindings);
return binaryOp(node.value.left, node.value.right, '<=');
} else if (node.type === 'GreaterThanExpression') {
binaryOp(node.value.left, node.value.right, '>', sql, bindings);
return binaryOp(node.value.left, node.value.right, '>');
} else if (node.type === 'GreaterOrEqualsExpression') {
binaryOp(node.value.left, node.value.right, '>=', sql, bindings);
return binaryOp(node.value.left, node.value.right, '>=');
} else if (node.type === 'AndExpression') {
binaryOp(node.value.left, node.value.right, 'and', sql, bindings);
return binaryOp(node.value.left, node.value.right, 'and');
} else if (node.type === 'OrExpression') {
binaryOp(node.value.left, node.value.right, 'or', sql, bindings);
return binaryOp(node.value.left, node.value.right, 'or');
} else if (node.type === 'NotExpression') {
sql.push('(not ');
op(node.value, sql, bindings);
sql.push(')');
return sql`(not ${op(node.value)})`;
} else {
throw Problem.internal.unsupportedODataExpression({ at: node.position, type: node.type, text: node.raw });
}
Expand All @@ -77,20 +69,15 @@ const op = (node, sql, bindings) => {
////////////////////////////////////////
// MAIN ENTRY POINT

const applyODataFilter = (expr) => {
if (expr == null) return identity;
const odataFilter = (expr) => {
if (expr == null) return sql`true`;

return (db) => {
let ast; // still hate this.
try { ast = odataParser.filter(expr); } // eslint-disable-line brace-style
catch (ex) { throw Problem.user.unparseableODataExpression({ reason: ex.message }); }
let ast; // still hate this.
try { ast = odataParser.filter(expr); } // eslint-disable-line brace-style
catch (ex) { throw Problem.user.unparseableODataExpression({ reason: ex.message }); }

const sql = [];
const bindings = [];
op(ast, sql, bindings);
return db.whereRaw(sql.join(''), bindings);
};
return op(ast);
};

module.exports = { applyODataFilter };
module.exports = { odataFilter };

13 changes: 8 additions & 5 deletions lib/data/json.js → lib/data/odata.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ const navigationLink = (schemaStack, instanceId, slicer = identity) => {

// manually extracts fields from a row into a js obj given a schema fieldlist.
// NOTE: expects extended submission metadata, for exporting submitter metadata!
const submissionToOData = (fields, table, { xml, encHasData, submission, submitter, attachments, localKey }, options = {}) => new Promise((resolve) => {
const submissionToOData = (fields, table, submission, options = {}) => new Promise((resolve) => {
const { submitter, attachment } = submission.aux;
const encHasData = (submission.aux.encryption != null) ? submission.aux.encryption.encHasData : false;

// we use SchemaStack to navigate the tree.
const schemaStack = new SchemaStack(fields);

Expand All @@ -105,7 +108,7 @@ const submissionToOData = (fields, table, { xml, encHasData, submission, submitt

// if we have an encDataAttachmentName, we must be an encrypted row. flag that
// and we'll deal with it pretty much immediately.
const encrypted = (localKey != null);
const encrypted = (submission.def.localKey != null);

// we always build a tree structure from root to track where we are, even if
// we are actually outputting some subtrees of it.
Expand All @@ -128,8 +131,8 @@ const submissionToOData = (fields, table, { xml, encHasData, submission, submitt
submissionDate: submission.createdAt,
submitterId,
submitterName: submitter.displayName || null,
attachmentsPresent: attachments.present || 0,
attachmentsExpected: attachments.expected || 0,
attachmentsPresent: attachment.present || 0,
attachmentsExpected: attachment.expected || 0,
status: encrypted ? (encHasData ? 'NotDecrypted' : 'MissingEncryptedFormData') : null
}
});
Expand Down Expand Up @@ -311,7 +314,7 @@ const submissionToOData = (fields, table, { xml, encHasData, submission, submitt
}
}, { xmlMode: true, decodeEntities: true });

parser.write(xml);
parser.write(submission.xml);
parser.end();
});

Expand Down
4 changes: 2 additions & 2 deletions lib/util/enketo.js → lib/external/enketo.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const http = require('http');
const https = require('https');
const path = require('path');
const querystring = require('querystring');
const { isBlank } = require('./util');
const Problem = require('./problem');
const { isBlank } = require('../util/util');
const Problem = require('../util/problem');

const mock = {
create: () => Promise.reject(Problem.internal.enketoNotConfigured()),
Expand Down
File renamed without changes.
Loading