Skip to content
Open
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
25 changes: 13 additions & 12 deletions apps/xyz/mod/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ A query string must returned from the getQueryFromTemplate() method.

The query and SQL params to be substituted in the database process are send to the dbs_connection.

The dbs_connection is called with a params object holding the query, variables, statement timeout, and nonblocking flag. The res.locals object is assigned as params.locals, so that a composing host can hand values set by its middleware to a dbs_connection it replaced. Clients cannot write to res.locals.

The rows returned from the dbs_connection are then passed to the sendRows() method.

@param {req} req HTTP request.
Expand Down Expand Up @@ -128,14 +130,18 @@ export default async function query(req, res) {

logger(query, 'query');

// The dbs module ignores params.locals, which a connection replaced by the host may read.
const params = {
locals: res.locals,
nonblocking: template.nonblocking,
query,
timeout: template.statement_timeout,
variables: req.params.SQL,
};

// Nonblocking queries will not wait for results but return once the query is sent.
if (template.nonblocking) {
const connected = await dbs_connections[template.dbs](
query,
req.params.SQL,
template.statement_timeout,
{ nonblocking: true },
);
const connected = await dbs_connections[template.dbs](params);

if (connected instanceof Error) {
return res
Expand All @@ -149,12 +155,7 @@ export default async function query(req, res) {
.send(`Non blocking request sent at ${new Date().toISOString()}.`);
}

// Run the query
const rows = await dbs_connections[template.dbs](
query,
req.params.SQL,
template.statement_timeout,
);
const rows = await dbs_connections[template.dbs](params);

sendRows(res, template, rows);
}
Expand Down
20 changes: 12 additions & 8 deletions apps/xyz/mod/utils/dbs.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,22 @@ Blocking queries are passed to the executeQuery method, which retries queries th

Nonblocking queries are attempted once without retry. The returned promise resolves true once the query has been sent to the database, without waiting for the query to complete. The query is sent before the caller can respond, so the database will execute the query even if a serverless process is frozen after the response. The promise resolves with the error if the client fails to connect or the statement timeout cannot be set. Errors from the query after it has been sent are logged, since the query result is not awaited.

The params object may be extended with additional properties. These are ignored by the clientQuery method but can be read by a dbs connection method replaced by a composing host.

@this {Pool} The connection pool to use for the query.
@param {string} query SQL query to execute
@param {Array} [variables] Parameters for the SQL query
@param {number} [timeout] Statement timeout in milliseconds. Defaults to xyzEnv.STATEMENT_TIMEOUT.
@param {Object} [options]
@property {boolean} [options.nonblocking] Resolve once the query is sent without waiting for the query result.
@param {Object} params
@property {string} params.query SQL query to execute
@property {Array} [params.variables] Parameters for the SQL query
@property {number} [params.timeout] Statement timeout in milliseconds. Defaults to xyzEnv.STATEMENT_TIMEOUT.
@property {boolean} [params.nonblocking] Resolve once the query is sent without waiting for the query result.
@returns {Promise<Array|boolean|Error>} Query rows, true for a sent nonblocking query, or an error.
*/
async function clientQuery(query, variables, timeout, options = {}) {
timeout ??= xyzEnv.STATEMENT_TIMEOUT;
async function clientQuery(params) {
const { query, variables } = params;

const timeout = params.timeout ?? xyzEnv.STATEMENT_TIMEOUT;

if (!options.nonblocking) {
if (!params.nonblocking) {
return executeQuery(this, query, variables, timeout);
}

Expand Down
12 changes: 6 additions & 6 deletions apps/xyz/mod/utils/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,19 @@ function postgresql() {

// Nonblocking log inserts are not retried.
// A retried insert would log dbs_retry, which would insert another log into the failing dbs.
dbs[params.dbs](
`INSERT INTO ${table}
dbs[params.dbs]({
nonblocking: true,
query: `INSERT INTO ${table}
(process, datetime, key, log, message)
VALUES ($1, $2, $3, $4, $5)`,
[
timeout: 3000,
variables: [
process_id,
Number.parseInt(Date.now() / 1000),
key,
logstring,
errorMessage,
],
3000,
{ nonblocking: true },
);
});
};
}
65 changes: 56 additions & 9 deletions apps/xyz/tests/mod/query.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,48 @@ describe('Query: Testing Query API', () => {
expect(mockLayerDb).toHaveBeenCalled();
expect(mockWorkspaceDb).not.toHaveBeenCalled();
});

it('passes res.locals to the dbs connection as params.locals', async () => {
const { req, res } = createMocks({
params: {
template: 'mock_template',
user: { roles: ['admin'], admin: true },
},
});

res.locals.foo = 'bar';

checkWorkspaceCache.mockResolvedValueOnce({ dbs: undefined });
getTemplate.mockResolvedValueOnce({
template: 'SELECT * FROM mock_table',
dbs: 'template_db',
});

await query(req, res);

expect(mockTemplateDb.mock.calls[0]).toHaveLength(1);
expect(mockTemplateDb.mock.calls[0][0].locals).toBe(res.locals);
});

it('does not pass the request to the dbs connection', async () => {
const { req, res } = createMocks({
params: {
template: 'mock_template',
user: { roles: ['admin'], admin: true },
},
});

checkWorkspaceCache.mockResolvedValueOnce({ dbs: undefined });
getTemplate.mockResolvedValueOnce({
template: 'SELECT * FROM mock_table',
dbs: 'template_db',
});

await query(req, res);

expect(mockTemplateDb.mock.calls[0]).toHaveLength(1);
expect(Object.values(mockTemplateDb.mock.calls[0][0])).not.toContain(req);
});
});

describe('Template resolution', () => {
Expand Down Expand Up @@ -383,7 +425,8 @@ describe('Query: Testing Query API', () => {
await query(req, res);

expect(mockDbQuery).toHaveBeenCalled();
const [queryStr, sqlParams] = mockDbQuery.mock.calls[0];
const { query: queryStr, variables: sqlParams } =
mockDbQuery.mock.calls[0][0];

// ${field} and ${table} are replaced inline.
expect(queryStr).toContain('SELECT age FROM users');
Expand Down Expand Up @@ -433,7 +476,7 @@ describe('Query: Testing Query API', () => {

await query(req, res);

expect(mockDbQuery.mock.calls[0][2]).toBe(9000);
expect(mockDbQuery.mock.calls[0][0].timeout).toBe(9000);
});

it('should ignore the statement_timeout request param', async () => {
Expand All @@ -448,7 +491,7 @@ describe('Query: Testing Query API', () => {

await query(req, res);

expect(mockDbQuery.mock.calls[0][2]).toBeUndefined();
expect(mockDbQuery.mock.calls[0][0].timeout).toBeUndefined();
});
});

Expand All @@ -463,14 +506,18 @@ describe('Query: Testing Query API', () => {
},
});

res.locals.foo = 'bar';

await query(req, res);

expect(res.statusCode).toBe(202);
expect(mockDbQuery).toHaveBeenCalledWith(
expect.any(String),
expect.anything(),
undefined,
{ nonblocking: true },
expect.objectContaining({
locals: res.locals,
nonblocking: true,
query: expect.any(String),
variables: expect.any(Array),
}),
);
});

Expand Down Expand Up @@ -508,7 +555,7 @@ describe('Query: Testing Query API', () => {
await query(req, res);

expect(mockDbQuery).toHaveBeenCalled();
const [queryStr] = mockDbQuery.mock.calls[0];
const { query: queryStr } = mockDbQuery.mock.calls[0][0];
expect(queryStr).toContain('SELECT distinct(name)');
expect(queryStr).toContain('FROM users');
expect(queryStr).toContain('ORDER BY name');
Expand All @@ -528,7 +575,7 @@ describe('Query: Testing Query API', () => {
await query(req, res);

expect(mockDbQuery).toHaveBeenCalled();
const [queryStr] = mockDbQuery.mock.calls[0];
const { query: queryStr } = mockDbQuery.mock.calls[0][0];
expect(queryStr).toContain('max(price)');
expect(queryStr).toContain('FROM products');
});
Expand Down
Loading
Loading