Skip to content

Commit

Permalink
Merge pull request #497 from psteinroe/feat/aggregates
Browse files Browse the repository at this point in the history
feat: support aggregates
  • Loading branch information
psteinroe authored Sep 5, 2024
2 parents 4551a48 + f89f158 commit 1c2dd56
Show file tree
Hide file tree
Showing 13 changed files with 320 additions and 6 deletions.
7 changes: 7 additions & 0 deletions .changeset/spotty-trains-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@supabase-cache-helpers/postgrest-react-query": minor
"@supabase-cache-helpers/postgrest-core": minor
"@supabase-cache-helpers/postgrest-swr": minor
---

feat: support for aggregates
13 changes: 11 additions & 2 deletions packages/postgrest-core/src/lib/parse-select-param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const parseSelectParam = (s: string, currentPath?: Path): Path[] => {
}

const foreignTables = result.reduce((prev, curr, idx, matches) => {
if (curr.name === 'selectedColumns') {
if (curr.name === 'selectedColumns' && curr.value.length > 0) {
const name = matches[idx - 1].value.slice(1, -1);
prev = { ...prev, [name]: curr.value };
}
Expand Down Expand Up @@ -62,6 +62,11 @@ export const parseSelectParam = (s: string, currentPath?: Path): Path[] => {
.map((c) => {
const split = c.split(':');
const hasAlias = split.length > 1;

const aggregateSplit = split[hasAlias ? 1 : 0].split('.');
const hasAggregate =
aggregateSplit.length > 1 && aggregateSplit[1].endsWith('()');

return {
declaration: [currentPath?.declaration, c].filter(Boolean).join('.'),
alias:
Expand All @@ -70,9 +75,13 @@ export const parseSelectParam = (s: string, currentPath?: Path): Path[] => {
.filter(Boolean)
.join('.')
: undefined,
path: [currentPath?.path, split[hasAlias ? 1 : 0]]
path: [
currentPath?.path,
hasAggregate ? aggregateSplit[0] : split[hasAlias ? 1 : 0],
]
.filter(Boolean)
.join('.'),
...(hasAggregate ? { aggregate: aggregateSplit[1].slice(0, -2) } : {}),
};
});

Expand Down
4 changes: 4 additions & 0 deletions packages/postgrest-core/src/lib/query-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export type Path = {
* The full declaration of a column that includes alias, hints and inner joins
*/
declaration: string;
/**
* The aggregate function applied to the path
*/
aggregate?: string;
};

/**
Expand Down
7 changes: 6 additions & 1 deletion packages/postgrest-core/src/mutate-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export type MutateItemCache<KeyType, Type extends Record<string, unknown>> = {
| 'applyFiltersOnPaths'
| 'apply'
| 'hasWildcardPath'
| 'hasAggregatePath'
>;
/**
* Decode a key. Should return null if not a PostgREST key.
Expand Down Expand Up @@ -141,7 +142,11 @@ export const mutateItem = async <KeyType, Type extends Record<string, unknown>>(
const orderBy = key.orderByKey
? parseOrderByKey(key.orderByKey)
: undefined;
if (key.isHead === true || filter.hasWildcardPath) {
if (
key.isHead === true ||
filter.hasWildcardPath ||
filter.hasAggregatePath
) {
// we cannot know whether the new item after mutating still has all paths required for a query if it contains a wildcard,
// because we do not know what columns a table has. we must always revalidate then.
mutations.push(revalidate(k));
Expand Down
6 changes: 5 additions & 1 deletion packages/postgrest-core/src/postgrest-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class PostgrestFilter<Result extends Record<string, unknown>> {
private _filtersFn: FilterFn<Result> | undefined;
private _filterPaths: Path[];
public hasWildcardPath: boolean | undefined;
public hasAggregatePath: boolean | undefined;

constructor(
public readonly params: { filters: FilterDefinitions; paths: Path[] },
Expand All @@ -37,6 +38,7 @@ export class PostgrestFilter<Result extends Record<string, unknown>> {
this.hasWildcardPath = this.params.paths.some((p) =>
p.declaration.endsWith('*'),
);
this.hasAggregatePath = this.params.paths.some((p) => Boolean(p.aggregate));
}

public static fromQuery(query: string, opts?: PostgrestQueryParserOptions) {
Expand Down Expand Up @@ -197,7 +199,9 @@ export class PostgrestFilter<Result extends Record<string, unknown>> {
const filterFn = OPERATOR_MAP[operator];
if (!filterFn)
throw new Error(
`Unable to build filter function for ${JSON.stringify(def)}. Operator ${operator} is not supported.`,
`Unable to build filter function for ${JSON.stringify(
def,
)}. Operator ${operator} is not supported.`,
);

return (obj: object) =>
Expand Down
7 changes: 6 additions & 1 deletion packages/postgrest-core/src/upsert-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export type UpsertItemCache<KeyType, Type extends Record<string, unknown>> = {
| 'applyFiltersOnPaths'
| 'apply'
| 'hasWildcardPath'
| 'hasAggregatePath'
>;
/**
* Decode a key. Should return null if not a PostgREST key.
Expand Down Expand Up @@ -143,7 +144,11 @@ export const upsertItem = async <KeyType, Type extends Record<string, unknown>>(
? parseOrderByKey(key.orderByKey)
: undefined;

if (key.isHead === true || filter.hasWildcardPath) {
if (
key.isHead === true ||
filter.hasWildcardPath ||
filter.hasAggregatePath
) {
// we cannot know whether the new item after merging still has all paths required for a query if it contains a wildcard,
// because we do not know what columns a table has. we must always revalidate then.
mutations.push(revalidate(k));
Expand Down
78 changes: 78 additions & 0 deletions packages/postgrest-core/tests/lib/parse-select-param.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,84 @@ import { describe, expect, it } from 'vitest';
import { parseSelectParam } from '../../src/lib/parse-select-param';

describe('parseSelectParam', () => {
it('should parse aggregates', () => {
expect(parseSelectParam('amount.sum()')).toEqual([
{
alias: undefined,
declaration: 'amount.sum()',
path: 'amount',
aggregate: 'sum',
},
]);
});

it('should parse multiple aggregates with grouping', () => {
expect(parseSelectParam('amount.sum(),amount.avg(),order_date')).toEqual([
{
alias: undefined,
declaration: 'amount.sum()',
path: 'amount',
aggregate: 'sum',
},
{
alias: undefined,
declaration: 'amount.avg()',
path: 'amount',
aggregate: 'avg',
},
{
alias: undefined,
declaration: 'order_date',
path: 'order_date',
},
]);
});

it('should parse aggregates with alias', () => {
expect(
parseSelectParam('amount.sum(),alias:amount.avg(),order_date'),
).toEqual([
{
alias: undefined,
declaration: 'amount.sum()',
path: 'amount',
aggregate: 'sum',
},
{
alias: 'alias',
declaration: 'alias:amount.avg()',
path: 'amount',
aggregate: 'avg',
},
{
alias: undefined,
declaration: 'order_date',
path: 'order_date',
},
]);
});

it('should parse aggregates within embedded resources', () => {
expect(parseSelectParam('state,orders(amount.sum(),order_date)')).toEqual([
{
alias: undefined,
declaration: 'state',
path: 'state',
},
{
alias: undefined,
declaration: 'orders.amount.sum()',
path: 'orders.amount',
aggregate: 'sum',
},
{
alias: undefined,
declaration: 'orders.order_date',
path: 'orders.order_date',
},
]);
});

it('should return input if falsy', () => {
expect(
parseSelectParam(
Expand Down
32 changes: 32 additions & 0 deletions packages/postgrest-core/tests/mutate-item.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ const mutateFnMock = async (
},
getPostgrestFilter() {
return {
get hasAggregatePath(): boolean {
return typeof postgrestFilter.hasAggregatePath === 'boolean'
? postgrestFilter.hasAggregatePath
: false;
},
get hasWildcardPath(): boolean {
return typeof postgrestFilter.hasWildcardPath === 'boolean'
? postgrestFilter.hasWildcardPath
Expand Down Expand Up @@ -137,6 +142,9 @@ const mutateRelationMock = async (
},
getPostgrestFilter() {
return {
get hasAggregatePath(): boolean {
return false;
},
get hasWildcardPath(): boolean {
return false;
},
Expand Down Expand Up @@ -205,6 +213,11 @@ const mutateFnResult = async (
},
getPostgrestFilter() {
return {
get hasAggregatePath(): boolean {
return typeof postgrestFilter.hasAggregatePath === 'boolean'
? postgrestFilter.hasAggregatePath
: false;
},
get hasWildcardPath(): boolean {
return typeof postgrestFilter.hasWildcardPath === 'boolean'
? postgrestFilter.hasWildcardPath
Expand Down Expand Up @@ -347,6 +360,25 @@ describe('mutateItem', () => {
expect(mutate).toHaveBeenCalledTimes(0);
});

it('should revalidate aggregate query', async () => {
const { mutate, revalidate } = await mutateFnMock(
{ id_1: '0', id_2: '0' },
(c) => c,
{},
{
apply: false,
applyFilters: false,
hasPaths: false,
hasWildcardPath: false,
hasAggregatePath: true,
hasFiltersOnPaths: true,
applyFiltersOnPaths: true,
},
);
expect(revalidate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledTimes(0);
});

it('should revalidate isHead query', async () => {
const { mutate, revalidate } = await mutateFnMock(
{ id_1: '0', id_2: '0' },
Expand Down
13 changes: 13 additions & 0 deletions packages/postgrest-core/tests/postgrest-filter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ describe('PostgrestFilter', () => {
).toEqual(true);
});

it('should set has aggregate paths', () => {
expect(
PostgrestFilter.fromQuery(
new PostgrestParser(
createClient('https://localhost', 'test')
.from('contact')
.select('name,city,state,orders(amount.sum(),order_date)')
.eq('username', 'test'),
).queryKey,
).hasAggregatePath,
).toEqual(true);
});

describe('.transform', () => {
it('should transform nested one-to-many relations', () => {
expect(
Expand Down
31 changes: 31 additions & 0 deletions packages/postgrest-core/tests/upsert-item.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ const mutateFnMock = async (
? postgrestFilter.hasWildcardPath
: false;
},
get hasAggregatePath(): boolean {
return typeof postgrestFilter.hasAggregatePath === 'boolean'
? postgrestFilter.hasAggregatePath
: false;
},
denormalize<ItemType>(obj: ItemType): ItemType {
return obj;
},
Expand Down Expand Up @@ -137,6 +142,9 @@ const mutateRelationMock = async (
get hasWildcardPath(): boolean {
return false;
},
get hasAggregatePath(): boolean {
return false;
},
denormalize<RelationType>(obj: RelationType): RelationType {
return obj;
},
Expand Down Expand Up @@ -202,6 +210,11 @@ const mutateFnResult = async (
},
getPostgrestFilter() {
return {
get hasAggregatePath(): boolean {
return typeof postgrestFilter.hasAggregatePath === 'boolean'
? postgrestFilter.hasAggregatePath
: false;
},
get hasWildcardPath(): boolean {
return typeof postgrestFilter.hasWildcardPath === 'boolean'
? postgrestFilter.hasWildcardPath
Expand Down Expand Up @@ -341,6 +354,24 @@ describe('upsertItem', () => {
expect(revalidate).toHaveBeenCalledTimes(1);
});

it('should revalidate aggregate query', async () => {
const { mutate, revalidate } = await mutateFnMock(
{ id_1: '0', id_2: '0', value: 'test' },
{},
{
hasWildcardPath: false,
hasAggregatePath: true,
apply: false,
applyFilters: false,
hasPaths: false,
hasFiltersOnPaths: true,
applyFiltersOnPaths: true,
},
);
expect(mutate).toHaveBeenCalledTimes(0);
expect(revalidate).toHaveBeenCalledTimes(1);
});

it('should revalidate isHead query', async () => {
const { mutate, revalidate } = await mutateFnMock(
{ id_1: '0', id_2: '0', value: 'test' },
Expand Down
Loading

0 comments on commit 1c2dd56

Please sign in to comment.