Skip to content

Commit

Permalink
Added preselected columns to buildView load function
Browse files Browse the repository at this point in the history
  • Loading branch information
ardsh committed Jun 26, 2024
1 parent e9f6288 commit fe2bc72
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 54 deletions.
5 changes: 5 additions & 0 deletions .changeset/shiny-beds-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sql-api-engine': patch
---

Added preselected columns to buildView load
35 changes: 32 additions & 3 deletions packages/core/engine/__tests__/buildView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ describe('Filters', () => {
.addJsonContainsFilter('settings')
.addBooleanFilter(
'isGmail',
table => sql.fragment`${table.users}.email ILIKE '%gmail.com'`
table => sql.fragment`${table.users}.email ILIKE '%gmail.com'`,
sql.fragment`users.email NOT ILIKE '%gmail.com'`
)

it('Allows specifying generic filters', async () => {
Expand All @@ -250,7 +251,7 @@ describe('Filters', () => {
it('Allows specifying in array filters', async () => {
const query = await userView.getWhereFragment({
where: {
isGmail: true,
isGmail: false,
postsCount: {
_gte: 30
},
Expand All @@ -276,6 +277,7 @@ describe('Filters', () => {
expect(query.sql).toContain(`"users".id = ANY(`)
expect(query.sql).toContain(`users.id = $`)
expect(query.sql).toContain(`) OR (`)
expect(query.sql).toContain(`NOT ILIKE '%gmail.com'`)
})
it('Allows specifying comparison filters', async () => {
const query = await userView.getWhereFragment({
Expand All @@ -289,12 +291,16 @@ describe('Filters', () => {
{
'posts.title': 'abc'
}
]
],
'users.created_at': {
_is_null: true
}
}
})
expect(query.sql).toContain(`"users".id > `)
expect(query.sql).toContain(`"posts"."title" = `)
expect(query.sql).toContain(`) AND (`)
expect(query.sql).toContain(`IS NULL`)
})
it('json filter handles a combination of different types', async () => {
const settings = {
Expand Down Expand Up @@ -390,5 +396,28 @@ describe('Filters', () => {
count: expect.any(Number)
})
})

it('Can take pre-selected columns', async () => {
const data = await userView
.setColumns(['first_name', 'email'])
.setColumns({
count: sql.fragment`COUNT(*)`,
id: sql.fragment`id`
})
.load({
select: ['count', 'email', 'first_name'],
orderBy: sql.fragment`users.id DESC`,
groupBy: sql.fragment`users.id`,
take: 5,
skip: 2,
db
})
expect(data.length).toBe(5)
expect(data[0]).toEqual({
first_name: expect.any(String),
email: expect.any(String),
count: expect.any(Number)
})
})
})
})
128 changes: 95 additions & 33 deletions packages/core/engine/buildView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ import type { PromiseOrValue } from '../utils'
type LoadViewParameters<
TFilter extends Record<string, any> = Record<never, any>,
TFilterKey extends keyof TFilter = never,
TFragment extends SqlFragment | QuerySqlToken = SqlFragment
TFragment extends SqlFragment | QuerySqlToken = SqlFragment,
TColumns extends string[] = never
> = {
select: TFragment
select: TFragment | TColumns
orderBy?: SqlFragment
groupBy?: SqlFragment
take?: number
Expand Down Expand Up @@ -67,7 +68,8 @@ export type Interpretors<
export type BuildView<
TFilter extends Record<string, any> = Record<never, any>,
TFilterKey extends keyof TFilter = never,
TAliases extends string = '_main'
TAliases extends string = '_main',
TColumns extends string = never
> = {
/**
* Allows adding custom filters to the view
Expand Down Expand Up @@ -98,7 +100,12 @@ export type BuildView<
| null
| undefined
| false
}): BuildView<TNewFilter & TFilter, keyof TNewFilter | TFilterKey, TAliases>
}): BuildView<
TNewFilter & TFilter,
keyof TNewFilter | TFilterKey,
TAliases,
TColumns
>
/**
* Allows filtering by string operators, e.g. "contains", "starts with", "ends with", etc.
* @param field - The name of the filter - Can be a nested field, e.g. "user.name"
Expand All @@ -121,7 +128,8 @@ export type BuildView<
) => BuildView<
TFilter & { [x in TKey]?: z.infer<typeof stringFilterType> },
keyof TFilter | TKey,
TAliases
TAliases,
TColumns
>
/**
* Allows filtering by comparison operators, e.g. "greater than", "less than", "between", "in", etc.
Expand All @@ -142,11 +150,13 @@ export type BuildView<
value?: z.infer<typeof comparisonFilterType>,
allFilters?: TFilter,
ctx?: any
) => SqlFragment)
) => SqlFragment),
type?: string
) => BuildView<
TFilter & { [x in TKey]?: z.infer<typeof comparisonFilterType> },
keyof TFilter | TKey,
TAliases
TAliases,
TColumns
>
/**
* Allows filtering jsonb columns, using the @> operator to check if a JSONB column contains a certain value or structure.
Expand Down Expand Up @@ -183,7 +193,8 @@ export type BuildView<
) => BuildView<
TFilter & { [x in TKey]?: Parameters<typeof jsonbContainsFilter>[0] },
keyof TFilter | TKey,
TAliases
TAliases,
TColumns
>
/**
* Allows filtering by date operators, e.g. "greater than", "less than" etc.
Expand All @@ -205,7 +216,8 @@ export type BuildView<
) => BuildView<
TFilter & { [x in TKey]?: z.infer<typeof dateFilterType> },
keyof TFilter | TKey,
TAliases
TAliases,
TColumns
>
/**
* Loads data from the view
Expand All @@ -221,34 +233,44 @@ export type BuildView<
* */
load: <
TFragment extends SqlFragment | QuerySqlToken,
TObject extends z.AnyZodObject = TFragment extends QuerySqlToken<infer T>
? T
: any
TSelect extends TColumns = never,
TObject extends any = [TSelect] extends [never]
? TFragment extends QuerySqlToken<infer T>
? z.infer<T>
: any
: Record<TSelect, any>
>(
args: LoadViewParameters<TFilter, TFilterKey, TFragment>
) => Promise<readonly z.infer<TObject>[]>
args: LoadViewParameters<TFilter, TFilterKey, TFragment, TSelect[]>
) => Promise<readonly TObject[]>
setColumns: <TNewColumns extends string = never>(
columns:
| {
[x in TNewColumns]: SqlFragment
}
| ArrayLike<TNewColumns>
) => BuildView<TFilter, TFilterKey, TAliases, TColumns | TNewColumns>
/**
* Sets the context for the view. This context can be used in various parts of the view lifecycle, such as in filters or constraints.
* @param ctx - The context object. Each key-value pair in the object sets a context variable.
* @returns The updated BuildView instance with the new context.
* */
context: <TContext extends Record<string, any>>(
ctx?: TContext
) => BuildView<TFilter, TFilterKey, TAliases>
) => BuildView<TFilter, TFilterKey, TAliases, TColumns>
/**
* Sets options for the view. Options can configure various aspects of how the view operates.
* @param opts - The options object. Each key-value pair in the object sets an option.
* @returns The updated BuildView instance with the new options.
* */
options: <TOptions extends Record<string, any>>(
opts?: TOptions
) => BuildView<TFilter, TFilterKey, TAliases>
) => BuildView<TFilter, TFilterKey, TAliases, TColumns>
/**
* Allows preprocessing the filters before they are interpreted
* */
setFilterPreprocess: (
preprocess: (filters: TFilter, context: any) => Promise<TFilter> | TFilter
) => BuildView<TFilter, TFilterKey, TAliases>
) => BuildView<TFilter, TFilterKey, TAliases, TColumns>
/**
* Sets table aliases. By default there's a `_main` alias for the main table that's referenced in the FROM fragment.
*
Expand All @@ -264,7 +286,7 @@ export type BuildView<
* */
setTableAliases: <TNewAliases extends string>(
table: Record<TNewAliases, string | IdentifierSqlToken>
) => BuildView<TFilter, TFilterKey, TAliases | TNewAliases>
) => BuildView<TFilter, TFilterKey, TAliases | TNewAliases, TColumns>
/**
* Allows filtering by boolean operators, e.g. "is true", "is false", "is null", etc.
* @param field - The name of the filter - Can be a nested field, e.g. "user.name"
Expand All @@ -284,11 +306,13 @@ export type BuildView<
value?: boolean,
allFilters?: TFilter,
ctx?: any
) => SqlFragment)
) => SqlFragment),
falseFragment?: SqlFragment
) => BuildView<
TFilter & { [x in TKey]?: boolean },
keyof TFilter | TKey,
TAliases
TAliases,
TColumns
>
/**
* Allows filtering by single or multiple string values
Expand Down Expand Up @@ -320,7 +344,8 @@ export type BuildView<
) => BuildView<
TFilter & { [x in TKey]?: TValue | TValue[] | null },
keyof TFilter | TKey,
TAliases
TAliases,
TColumns
>
/**
* Use this to add a generic filter, that returns a SQL fragment
Expand All @@ -341,7 +366,8 @@ export type BuildView<
) => BuildView<
TFilter & { [x in TKey]?: TNewFilter },
keyof TFilter | TKey,
TAliases
TAliases,
TColumns
>
/**
* Returns the SQL query
Expand All @@ -366,7 +392,7 @@ export type BuildView<
constraints: (
ctx: any
) => PromiseOrValue<SqlFragment | SqlFragment[] | null | undefined>
) => BuildView<TFilter, TFilterKey, TAliases>
) => BuildView<TFilter, TFilterKey, TAliases, TColumns>
getFromFragment(): FragmentSqlToken
/**
* Returns all filters that have been added to the view
Expand Down Expand Up @@ -447,6 +473,7 @@ export const buildView = (
| null
const context = {} as Record<string, any>
const options = {} as Options
const allColumns = {} as Record<string, SqlFragment | IdentifierSqlToken>
const preprocessors = [] as ((
filters: any,
context: any
Expand Down Expand Up @@ -525,7 +552,11 @@ export const buildView = (
}

const addFilter = (
interpreter: (value: any, field: FragmentSqlToken) => any,
interpreter: (
value: any,
field: FragmentSqlToken,
...otherArgs: any[]
) => any,
fields: string | string[],
mapper?:
| SqlFragment
Expand All @@ -534,7 +565,8 @@ export const buildView = (
value?: any,
allFilters?: any,
context?: any
) => SqlFragment)
) => SqlFragment),
...otherArgs: any[]
) => {
if (mapper && Array.isArray(fields) && fields.length > 1) {
throw new Error(
Expand All @@ -559,7 +591,7 @@ export const buildView = (
: config.table && keys.length <= 1
? (sql.identifier([config.table, ...keys.slice(-1)]) as any)
: (sql.identifier([...keys.slice(-2)]) as any)
return interpreter(value, identifier)
return interpreter(value, identifier, ...otherArgs)
}
}
}, {})
Expand Down Expand Up @@ -620,11 +652,19 @@ export const buildView = (
addStringFilter: (keys: string | string[], name?: any) => {
return addFilter(stringFilter, keys, name)
},
addComparisonFilter: (keys: string | string[], name?: any) => {
return addFilter(comparisonFilter, keys, name)
addComparisonFilter: (
keys: string | string[],
name?: any,
...otherArgs: any[]
) => {
return addFilter(comparisonFilter, keys, name, ...otherArgs)
},
addBooleanFilter: (keys: string | string[], name?: any) => {
return addFilter(booleanFilter, keys, name)
addBooleanFilter: (
keys: string | string[],
name?: any,
...otherArgs: any[]
) => {
return addFilter(booleanFilter, keys, name, ...otherArgs)
},
addJsonContainsFilter: (keys: string | string[], name?: any) => {
return addFilter(jsonbContainsFilter, keys, name)
Expand Down Expand Up @@ -678,9 +718,17 @@ export const buildView = (
const whereFragment = args.where
? await getWhereFragment(args.where, context, options)
: sql.fragment``
const query = sql.unsafe`${
args.select
} ${getFromFragment()} ${whereFragment}
const selectFrag = Array.isArray(args.select)
? sql.fragment`SELECT ${sql.join(
args.select
.map(key => allColumns[key])
.filter((frag: any) => {
return frag && (frag.sql || frag.type)
}),
sql.fragment`\n, `
)}`
: args.select
const query = sql.unsafe`${selectFrag} ${getFromFragment()} ${whereFragment}
${args.groupBy ? sql.fragment`GROUP BY ${args.groupBy}` : sql.fragment``}
${args.orderBy ? sql.fragment`ORDER BY ${args.orderBy}` : sql.fragment``}
${
Expand All @@ -696,6 +744,20 @@ export const buildView = (
`
return db.any(query)
},
setColumns: (columns: string[] | Record<string, SqlFragment>) => {
if (Array.isArray(columns)) {
for (const column of columns) {
if (typeof column === 'string') {
allColumns[column] = sql.identifier([column])
}
}
} else if (typeof columns === 'object') {
Object.keys(columns).forEach(
key => columns[key] && (allColumns[key] = columns[key])
)
}
return self
},
getWhereFragment: async (args: any) => {
return getWhereFragment(args.where, args.ctx, args.options)
}
Expand Down
Loading

0 comments on commit fe2bc72

Please sign in to comment.