Skip to content

Commit

Permalink
Added setAggregates function in buildView
Browse files Browse the repository at this point in the history
  • Loading branch information
ardsh committed Oct 21, 2024
1 parent ad80f4d commit ca20dae
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-beers-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sql-api-engine': patch
---

Added setAggregate function in buildView to support grouping by
23 changes: 23 additions & 0 deletions packages/core/engine/__tests__/buildView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ describe('Filters', () => {
)
.addDateFilter('users.created_at')
.addJsonContainsFilter('settings')
.setColumns({
name: sql.fragment`users.first_name || ' ' || users.last_name AS "name"`,
month: sql.fragment`to_char(date_trunc('month', posts.created_at), 'YYYY-MM') AS "month"`
})
.addBooleanFilter(
'isGmail',
table => sql.fragment`${table.users}.email ILIKE '%gmail.com'`,
Expand Down Expand Up @@ -451,5 +455,24 @@ describe('Filters', () => {
postsCount: expect.any(Number)
})
})

it("Can use aggregates", async () => {
const view = userView.setAggregates({
postsCount: sql.fragment`COUNT(posts.text) AS "postsCount"`
})
const data = await view.load({
select: ['postsCount', 'month'],
db,
where: {
"posts.title": {
_is_null: false,
}
}
});
expect(data[0]).toEqual({
month: expect.any(String),
postsCount: expect.any(Number),
});
})
})
})
52 changes: 50 additions & 2 deletions packages/core/engine/buildView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export type BuildView<
>(filters: {
[x in TNewFilterKey]?: (
filter: TNewFilter[x],
allFilters: TFilter & TNewFilter,
allFilters: TFilter & { [y in NoInfer<TNewFilterKey> | x]: any },
context: any
) =>
| Promise<SqlFragment | null | undefined | false>
Expand Down Expand Up @@ -243,6 +243,29 @@ export type BuildView<
>(
args: LoadViewParameters<TFilter, TFilterKey, TFragment, TSelect[]>
) => Promise<readonly TObject[]>
/**
* Use this to specify aggregate functions as columns.
* If any aggregates are ever selected, the group by clause is automatically added.
* And all the non-aggregated columns are grouped by.
* Usage:
* ```ts
* view.setAggregates({
* postsCount: sql.fragment`COUNT(posts.text) AS "postsCount"`
* }).setColumns({
* name: sql.fragment`users.first_name || ' ' || users.last_name AS "name"`,
* month: sql.fragment`DATE_TRUNC('month', posts.created_at) AS "month"`,
* postsCount: sql.fragment`COUNT(*) AS "postsCount"`,
* largePostsCount: sql.fragment`COUNT(*) FILTER (WHERE LENGTH(posts.text) > 100) AS "largePostsCount"`,
* })
* .load({
* // Automatically returns posts grouped by month
* select: ['postsCount', 'month'],
* })
* ```
*/
setAggregates: <TNewAggregates extends string = never>(aggregates: {
[x in TNewAggregates]: SqlFragment
}) => BuildView<TFilter, TFilterKey, TAliases, TColumns | TNewAggregates>
setColumns: <TNewColumns extends string = never>(
columns:
| {
Expand Down Expand Up @@ -475,6 +498,7 @@ export const buildView = (
const context = {} as Record<string, any>
const options = {} as Options
const allColumns = {} as Record<string, SqlFragment | IdentifierSqlToken>
const allAggregates = {} as Record<string, SqlFragment>
const preprocessors = [] as ((
filters: any,
context: any
Expand Down Expand Up @@ -733,8 +757,17 @@ export const buildView = (
sql.fragment`\n, `
)}`
: args.select
let groupBy = args.groupBy ? sql.fragment`GROUP BY ${args.groupBy}` : sql.fragment``;
const selectedAggregates = Array.isArray(args.select) && args.select.map(key => allAggregates[key]).filter(notEmpty) || [];
if (args.groupBy && selectedAggregates.length) {
throw new Error('Cannot specify groupBy when selecting aggregates')
}
if (selectedAggregates.length && Array.isArray(args.select)) {
const nonAggregated = args.select.map((key, idx) => [key, idx]).filter(([key]) => !allAggregates[key]).map(([key, idx]) => sql.fragment([idx + 1]));
groupBy = sql.fragment`GROUP BY ${sql.join(nonAggregated, sql.fragment`, `)}`
}
const query = sql.unsafe`${selectFrag} ${getFromFragment(realContext)} ${whereFragment}
${args.groupBy ? sql.fragment`GROUP BY ${args.groupBy}` : sql.fragment``}
${groupBy}
${args.orderBy ? sql.fragment`ORDER BY ${args.orderBy}` : sql.fragment``}
${
typeof args.take === 'number' && args.take > 0
Expand All @@ -749,6 +782,21 @@ export const buildView = (
`
return db.any(query)
},
setAggregates: (aggregates: Record<string, SqlFragment>) => {
if (aggregates && typeof aggregates === 'object') {
Object.keys(aggregates).forEach(
key => {
if (aggregates[key]) {
allColumns[key] = aggregates[key]
allAggregates[key] = aggregates[key]
}
}
)
} else {
throw new Error('setAggregates must be called with an object')
}
return self
},
setColumns: (columns: string[] | Record<string, SqlFragment>) => {
if (Array.isArray(columns)) {
for (const column of columns) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export type { QueryLoaderPlugin as Plugin } from './plugins/types'

export { sqlWith } from './sqlWith'
export { makeCountLoader } from './makeCountLoader'
export { filters } from '../utils/sqlUtils';
14 changes: 9 additions & 5 deletions packages/core/utils/sqlUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,6 @@ export const genericFilter = (value: any, statement: Fragment) => {
return null
}

const findTableName = (fragment: Fragment) => {
const table = fragment.sql.match(/^\s*FROM\s+(\S+)/i)?.[1]
return table?.replace(/\W+/g, '')?.toLowerCase()
}

export const dateFilter = (
date: z.infer<typeof dateFilterType> | undefined | null,
field: Fragment
Expand Down Expand Up @@ -275,3 +270,12 @@ export const jsonbContainsFilter = (
}
return null
}

export const filters = {
dateFilter: (field: Fragment | string | string[]) => (value: Parameters<typeof dateFilter>[0]) => dateFilter(value, typeof field === 'string' ? sql.identifier([field]) : Array.isArray(field) ? sql.identifier(field) : field),
stringFilter: (field: Fragment | string | string[]) => (value: Parameters<typeof stringFilter>[0]) => stringFilter(value, typeof field === 'string' ? sql.identifier([field]) : Array.isArray(field) ? sql.identifier(field) : field),
comparisonFilter: (field: Fragment | string | string[]) => (value: Parameters<typeof comparisonFilter>[0]) => comparisonFilter(value, typeof field === 'string' ? sql.identifier([field]) : Array.isArray(field) ? sql.identifier(field) : field),
jsonbContainsFilter: (field: Fragment | string | string[]) => (value: Parameters<typeof jsonbContainsFilter>[0]) => jsonbContainsFilter(value, typeof field === 'string' ? sql.identifier([field]) : Array.isArray(field) ? sql.identifier(field) : field),
inArrayFilter: (field: Fragment | string | string[], type='text') => (value: Parameters<typeof arrayFilter>[0]) => arrayFilter(value, typeof field === 'string' ? sql.identifier([field]) : Array.isArray(field) ? sql.identifier(field) : field, type),
booleanFilter: (trueStatement: Fragment | string | string[], falseStatement?: Fragment) => (value: Parameters<typeof booleanFilter>[0]) => booleanFilter(value, typeof trueStatement === 'string' ? sql.identifier([trueStatement]) : Array.isArray(trueStatement) ? sql.identifier(trueStatement) : trueStatement, falseStatement),
}
4 changes: 2 additions & 2 deletions packages/core/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { QuerySqlToken, FragmentSqlToken, SerializableValue } from 'slonik'
import type { QuerySqlToken, FragmentSqlToken, IdentifierSqlToken, SerializableValue } from 'slonik'
import { makeQueryLoader } from '../engine'

export type Fragment = FragmentSqlToken
export type Fragment = FragmentSqlToken | IdentifierSqlToken
export type { QuerySqlToken as Query, SerializableValue }
export type QueryLoader = Pick<
ReturnType<typeof makeQueryLoader>,
Expand Down

0 comments on commit ca20dae

Please sign in to comment.