Skip to content

Commit f3ff0c0

Browse files
Add support for table row aggregation and update table version (#2240)
Fixes OPS-4029
1 parent bed797c commit f3ff0c0

4 files changed

Lines changed: 214 additions & 3 deletions

File tree

compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ services:
33
tables:
44
container_name: tables
55
restart: unless-stopped
6-
image: public.ecr.aws/openops/openops-tables:0.2.17
6+
image: public.ecr.aws/openops/openops-tables:0.2.18
77
environment:
88
BASEROW_PUBLIC_URL: ${OPS_OPENOPS_TABLES_PUBLIC_URL}
99
BASEROW_PRIVATE_URL: http://localhost:3001

deploy/docker-compose/docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ services:
2323
environment:
2424
OPS_COMPONENT: app
2525
OPS_VERSION: ${OPS_VERSION:-latest}
26-
OPS_OPENOPS_TABLES_VERSION: 0.2.17
26+
OPS_OPENOPS_TABLES_VERSION: 0.2.18
2727
OPS_ANALYTICS_VERSION: 0.14.7
2828
depends_on:
2929
openops-tables:
@@ -49,7 +49,7 @@ services:
4949
depends_on:
5050
- openops-app
5151
openops-tables:
52-
image: public.ecr.aws/openops/openops-tables:0.2.17
52+
image: public.ecr.aws/openops/openops-tables:0.2.18
5353
restart: unless-stopped
5454
environment:
5555
BASEROW_PUBLIC_URL: ${OPS_OPENOPS_TABLES_PUBLIC_URL}

packages/openops/src/lib/openops-tables/rows.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,66 @@ function getEqualityFilterType(
257257
return ViewFilterTypesEnum.equal;
258258
}
259259

260+
export type AggregationSpec =
261+
| { type: 'count' }
262+
| { type: 'sum'; field: string }
263+
| { type: 'distinct_values'; field: string };
264+
265+
export interface TableFilter {
266+
fieldName: string;
267+
type: 'not_in';
268+
value: string[];
269+
}
270+
271+
export interface BatchTableAggregationsParams {
272+
tokenOrResolver: TokenOrResolver;
273+
tableIds: number[];
274+
filters?: TableFilter[];
275+
aggregations: AggregationSpec[];
276+
}
277+
278+
export type TableAggregationResult = {
279+
count?: number;
280+
[key: string]: number | string[] | undefined;
281+
};
282+
283+
export type BatchTableAggregationsResult = Record<
284+
string,
285+
TableAggregationResult
286+
>;
287+
288+
export async function batchTableAggregations(
289+
params: BatchTableAggregationsParams,
290+
): Promise<BatchTableAggregationsResult> {
291+
const url = 'api/database/rows/batch-aggregations/';
292+
293+
return executeWithConcurrencyLimit(
294+
async () => {
295+
const authenticationHeader = createAxiosHeaders(params.tokenOrResolver);
296+
return await makeOpenOpsTablesPost<BatchTableAggregationsResult>(
297+
url,
298+
{
299+
table_ids: params.tableIds,
300+
filters: (params.filters ?? []).map((filter) => ({
301+
field: filter.fieldName,
302+
type: filter.type,
303+
value: filter.value,
304+
})),
305+
aggregations: params.aggregations,
306+
},
307+
authenticationHeader,
308+
);
309+
},
310+
(error) => {
311+
logger.error('Error while posting batch table aggregations:', {
312+
error,
313+
url,
314+
tableIds: params.tableIds,
315+
});
316+
},
317+
);
318+
}
319+
260320
export async function batchDeleteRows(
261321
params: BatchDeleteRowsParams,
262322
): Promise<void> {

packages/openops/test/openops-tables/rows.test.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { axiosTablesRetryConfig } from '../../src/lib/openops-tables/requests-he
5555
import {
5656
addRow,
5757
batchDeleteRows,
58+
batchTableAggregations,
5859
deleteRow,
5960
getRowByPrimaryKeyValue,
6061
getRows,
@@ -356,6 +357,156 @@ describe('batchDeleteRows', () => {
356357
});
357358
});
358359

360+
describe('batchTableAggregations', () => {
361+
beforeEach(() => {
362+
jest.clearAllMocks();
363+
});
364+
365+
test('posts to correct url with count aggregation', async () => {
366+
makeOpenOpsTablesPostMock.mockResolvedValue({ '1': { count: 5 } });
367+
createAxiosHeadersMock.mockReturnValue('some header');
368+
369+
const result = await batchTableAggregations({
370+
tokenOrResolver: 'token',
371+
tableIds: [1],
372+
aggregations: [{ type: 'count' }],
373+
});
374+
375+
expect(result).toStrictEqual({ '1': { count: 5 } });
376+
expect(acquireMock).toBeCalledTimes(1);
377+
expect(releaseMock).toBeCalledTimes(1);
378+
expect(makeOpenOpsTablesPostMock).toBeCalledTimes(1);
379+
expect(makeOpenOpsTablesPostMock).toHaveBeenCalledWith(
380+
'api/database/rows/batch-aggregations/',
381+
{ table_ids: [1], filters: [], aggregations: [{ type: 'count' }] },
382+
'some header',
383+
);
384+
expect(createAxiosHeadersMock).toHaveBeenCalledWith('token');
385+
});
386+
387+
test('posts to correct url with sum aggregation', async () => {
388+
makeOpenOpsTablesPostMock.mockResolvedValue({
389+
'2': { 'sum__Estimated savings USD per month': 27880 },
390+
});
391+
createAxiosHeadersMock.mockReturnValue('some header');
392+
393+
const result = await batchTableAggregations({
394+
tokenOrResolver: 'token',
395+
tableIds: [2],
396+
aggregations: [{ type: 'sum', field: 'Estimated savings USD per month' }],
397+
});
398+
399+
expect(result).toStrictEqual({
400+
'2': { 'sum__Estimated savings USD per month': 27880 },
401+
});
402+
expect(makeOpenOpsTablesPostMock).toHaveBeenCalledWith(
403+
'api/database/rows/batch-aggregations/',
404+
{
405+
table_ids: [2],
406+
filters: [],
407+
aggregations: [
408+
{ type: 'sum', field: 'Estimated savings USD per month' },
409+
],
410+
},
411+
'some header',
412+
);
413+
});
414+
415+
test('posts multiple aggregations for multiple tables', async () => {
416+
makeOpenOpsTablesPostMock.mockResolvedValue({
417+
'1': { count: 10, sum__Cost: 500 },
418+
'2': { count: 3, sum__Cost: 120 },
419+
});
420+
createAxiosHeadersMock.mockReturnValue('some header');
421+
422+
const result = await batchTableAggregations({
423+
tokenOrResolver: 'token',
424+
tableIds: [1, 2],
425+
aggregations: [{ type: 'count' }, { type: 'sum', field: 'Cost' }],
426+
});
427+
428+
expect(result).toStrictEqual({
429+
'1': { count: 10, sum__Cost: 500 },
430+
'2': { count: 3, sum__Cost: 120 },
431+
});
432+
expect(makeOpenOpsTablesPostMock).toHaveBeenCalledWith(
433+
'api/database/rows/batch-aggregations/',
434+
{
435+
table_ids: [1, 2],
436+
filters: [],
437+
aggregations: [{ type: 'count' }, { type: 'sum', field: 'Cost' }],
438+
},
439+
'some header',
440+
);
441+
});
442+
443+
test('passes filters in request body', async () => {
444+
makeOpenOpsTablesPostMock.mockResolvedValue({ '1': { count: 2 } });
445+
createAxiosHeadersMock.mockReturnValue('some header');
446+
447+
await batchTableAggregations({
448+
tokenOrResolver: 'token',
449+
tableIds: [1],
450+
filters: [
451+
{
452+
fieldName: 'Status',
453+
type: 'not_in',
454+
value: ['Resolved', 'Dismissed'],
455+
},
456+
],
457+
aggregations: [{ type: 'count' }],
458+
});
459+
460+
expect(makeOpenOpsTablesPostMock).toHaveBeenCalledWith(
461+
'api/database/rows/batch-aggregations/',
462+
{
463+
table_ids: [1],
464+
filters: [
465+
{ field: 'Status', type: 'not_in', value: ['Resolved', 'Dismissed'] },
466+
],
467+
aggregations: [{ type: 'count' }],
468+
},
469+
'some header',
470+
);
471+
});
472+
473+
test('defaults filters to empty array when not provided', async () => {
474+
makeOpenOpsTablesPostMock.mockResolvedValue({ '1': { count: 0 } });
475+
createAxiosHeadersMock.mockReturnValue('some header');
476+
477+
await batchTableAggregations({
478+
tokenOrResolver: 'token',
479+
tableIds: [1],
480+
aggregations: [{ type: 'count' }],
481+
});
482+
483+
expect(makeOpenOpsTablesPostMock).toHaveBeenCalledWith(
484+
'api/database/rows/batch-aggregations/',
485+
expect.objectContaining({ filters: [] }),
486+
'some header',
487+
);
488+
});
489+
490+
test('logs error and rethrows when post fails', async () => {
491+
const error = new Error('network error');
492+
makeOpenOpsTablesPostMock.mockRejectedValue(error);
493+
createAxiosHeadersMock.mockReturnValue('some header');
494+
495+
await expect(
496+
batchTableAggregations({
497+
tokenOrResolver: 'token',
498+
tableIds: [1],
499+
aggregations: [{ type: 'count' }],
500+
}),
501+
).rejects.toThrow('network error');
502+
503+
expect(logger.error).toHaveBeenCalledWith(
504+
'Error while posting batch table aggregations:',
505+
expect.objectContaining({ error, tableIds: [1] }),
506+
);
507+
});
508+
});
509+
359510
describe('getRowByPrimaryKeyValue', () => {
360511
beforeEach(() => {
361512
jest.clearAllMocks();

0 commit comments

Comments
 (0)