Skip to content

Commit

Permalink
Added context passing functions in buildView
Browse files Browse the repository at this point in the history
  • Loading branch information
ardsh committed Sep 5, 2024
1 parent ac21376 commit ad80f4d
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 77 deletions.
6 changes: 6 additions & 0 deletions .changeset/heavy-beds-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'sql-api-playground': patch
'sql-api-engine': patch
---

Added ctx in buildView
18 changes: 18 additions & 0 deletions packages/core/engine/__tests__/buildView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ it("Doesn't allow arrays with custom filters", async () => {
'If you specify a mapper function you cannot have multiple filter keys'
)
})

it("Allows using context in view functions", async () => {
const loggedInUserView = buildView`FROM (SELECT * FROM users
WHERE users.id = ${ctx => ctx.user?.id}) users`;
const data = await loggedInUserView.load({
select: sql.fragment`SELECT id`,
db,
ctx: {
user: {
id: 'y'
}
}
})
expect(data).toEqual([{
id: 'y',
}])
});

it('Allows specifying multiple keys', async () => {
const userView = buildView`FROM users`.addStringFilter([
'email',
Expand Down
21 changes: 13 additions & 8 deletions packages/core/engine/buildView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type LoadViewParameters<
groupBy?: SqlFragment
take?: number
skip?: number
ctx?: any
where?: RecursiveFilterConditions<{
[x in TFilterKey]?: TFilter[x]
}>
Expand Down Expand Up @@ -393,7 +394,7 @@ export type BuildView<
ctx: any
) => PromiseOrValue<SqlFragment | SqlFragment[] | null | undefined>
) => BuildView<TFilter, TFilterKey, TAliases, TColumns>
getFromFragment(): FragmentSqlToken
getFromFragment(ctx?: any): FragmentSqlToken
/**
* Returns all filters that have been added to the view
* @param options - Options for configuring the filters
Expand Down Expand Up @@ -460,9 +461,9 @@ type Options = FilterOptions & {

export const buildView = (
parts: readonly string[],
...values: readonly ValueExpression[]
...values: readonly (ValueExpression | ((ctx: Record<string, any>) => ValueExpression))[]
) => {
const fromFragment = sql.fragment(parts, ...values)
const fromFragment = sql.fragment(parts, ...values.map(v => typeof v === 'function' ? v({}) ?? null : v))
if (!fromFragment.sql.match(/^\s*FROM/i)) {
throw new Error('First part of view must be FROM')
}
Expand Down Expand Up @@ -546,9 +547,8 @@ export const buildView = (
: sql.fragment`WHERE TRUE`
}

const getFromFragment = () => {
// return sql.fragment`${fromFragment} ${await getWhereFragment(args.where, args.ctx)}`
return fromFragment
const getFromFragment = (ctx={}) => {
return sql.fragment(parts, ...values.map(v => typeof v === 'function' ? v(ctx) ?? null : v))
}

const addFilter = (
Expand Down Expand Up @@ -715,8 +715,13 @@ export const buildView = (
'Database is not set. Please set the database by calling options({ db: db })'
)
}
if (args.take === 0) return [];
const realContext = {
...context,
...args.ctx,
};
const whereFragment = args.where
? await getWhereFragment(args.where, context, options)
? await getWhereFragment(args.where, realContext, options)
: sql.fragment``
const selectFrag = Array.isArray(args.select)
? sql.fragment`SELECT ${sql.join(
Expand All @@ -728,7 +733,7 @@ export const buildView = (
sql.fragment`\n, `
)}`
: args.select
const query = sql.unsafe`${selectFrag} ${getFromFragment()} ${whereFragment}
const query = sql.unsafe`${selectFrag} ${getFromFragment(realContext)} ${whereFragment}
${args.groupBy ? sql.fragment`GROUP BY ${args.groupBy}` : sql.fragment``}
${args.orderBy ? sql.fragment`ORDER BY ${args.orderBy}` : sql.fragment``}
${
Expand Down
2 changes: 1 addition & 1 deletion packages/core/engine/makeCountLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export function makeCountLoader<
).filter(Boolean),
sql.fragment`,\n `
)}
${view.getFromFragment()}
${view.getFromFragment(args?.ctx)}
${
conditions.length
? sql.fragment`WHERE (${sql.join(
Expand Down
5 changes: 3 additions & 2 deletions packages/core/engine/makeQueryLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,11 +416,11 @@ export function makeQueryLoader<
const queryComponents = options.query
const query = queryComponents.select
const view = queryComponents.view
const fromFragment = view.getFromFragment()
const fromFragment = view.getFromFragment({})
if (query.sql.match(/;\s*$/)) {
// TODO: Add more checks for invalid queries
console.warn(
'Your query includes semicolons at the end. Please refer to the documentation of slonik-trpc, and do not include semicolons in the query:\n ' +
'Your query includes semicolons at the end. Please refer to the documentation of sql-api-engine, and do not include semicolons in the query:\n ' +
query.sql
)
}
Expand Down Expand Up @@ -777,6 +777,7 @@ export function makeQueryLoader<
? sql.fragment`, ${sql.join(extraSelects, sql.fragment`, `)}`
: sql.fragment``

const fromFragment = view.getFromFragment(context)
const baseQuery = sql.type(zodType)`${actualQuery} ${extraSelectFields} ${
fromFragment ?? sql.fragment``
} ${
Expand Down
1 change: 1 addition & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './engine'
4 changes: 2 additions & 2 deletions packages/playground/slonik-trpc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ declare type BuildView<
setConstraints: (
constraints: (ctx: any) => PromiseOrValue<SqlFragment | SqlFragment[] | null | undefined>,
) => BuildView<TFilter, TFilterKey, TAliases>;
getFromFragment(): FragmentSqlToken;
getFromFragment(ctx?: any): FragmentSqlToken;
getFilters<
TInclude extends Extract<TFilterKey, string> | `${string}*` = never,
TExclude extends Extract<TFilterKey, string> | `${string}*` = never,
Expand Down Expand Up @@ -265,7 +265,7 @@ declare type BuildView<

export declare const buildView: (
parts: readonly string[],
...values: readonly ValueExpression[]
...values: readonly (ValueExpression | ((ctx?: any) => ValueExpression))[]
) => BuildView<Record<never, any>, never, '_main', never>;

declare const comparisonFilterType: z.ZodObject<
Expand Down
107 changes: 43 additions & 64 deletions packages/playground/src/components/webcontainer/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,36 @@ export const DEFAULT_DEMO_CODE = `
import { makeQueryLoader, buildView, sql } from 'slonik-trpc';
import { z } from 'zod';
const employee = z.object({
id: z.number(),
firstName: z.string(),
lastName: z.string(),
salary: z.number(),
company: z.string(),
employed_days: z.number(),
startDate: z.string(),
endDate: z.string(),
const postsView = buildView\`
FROM users
LEFT JOIN posts
ON posts.author = users.id\`
// Allows filtering posts.title: { _ilike: '%hello%' }
.addStringFilter('posts.title')
// Allows filter id: [1, 2, 3]
.addInArrayFilter('id', sql.fragment\`posts.id\`, 'numeric')
.addDateFilter('createdDate', sql.fragment\`posts.created_at\`)
.addComparisonFilter('postLength', sql.fragment\`LENGTH(posts.text)\`)
const post = z.object({
id: z.string(),
author: z.string(),
text: z.string(),
age: z.number(),
});
const query = sql.type(employee)\`
SELECT *
\`;
const employeeView = buildView\`FROM (
SELECT
employees.id,
employees.first_name AS "firstName",
employees.last_name AS "lastName",
employee_companies.salary,
companies.name AS company,
(end_date - start_date) AS employed_days,
companies.id AS company_id,
employee_companies.start_date AS "startDate",
employee_companies.end_date AS "endDate"
FROM employees
LEFT JOIN employee_companies
ON employees.id = employee_companies.employee_id
LEFT JOIN companies
ON employee_companies.company_id = companies.id
) employees\`
.options({ orEnabled: true })
.setColumns(["id", "firstName", "lastName", "company_id", "startDate", "endDate", "employed_days", "company", "salary"])
.addInArrayFilter('employeeId', sql.fragment\`employees.id\`, 'numeric')
.addInArrayFilter('companyId', sql.fragment\`employees.company_id\`, 'numeric')
.addDateFilter('employmentStartDate', sql.fragment\`employees."startDate"\`)
.addDateFilter('employmentEndDate', sql.fragment\`employees."endDate"\`)
.addComparisonFilter('employmentSalary', sql.fragment\`employees.salary\`)
;
const postsQuery = sql.type(post)\`
SELECT "posts".id
, "users"."firstName" || ' ' || "users"."lastName" AS "author"
, "posts"."text"
, EXTRACT(DAYS FROM NOW() - posts."created_at") AS "age"
\`;
export const employeeLoader = makeQueryLoader({
export const postsLoader = makeQueryLoader({
query: {
select: query,
view: employeeView,
select: postsQuery,
view: postsView,
},
defaults: {
orderBy: [["id", "ASC"]],
Expand All @@ -55,43 +41,36 @@ export const employeeLoader = makeQueryLoader({
orFilterEnabled: true,
},
sortableColumns: {
id: "id",
name: sql.fragment\`"firstName" || "lastName"\`,
daysEmployed: "employed_days",
startDate: "startDate",
endDate: "endDate",
salary: "salary",
company: "company",
id: ["posts", "id"],
name: sql.fragment\`users."firstName" || users."lastName"\`,
// Can reference FROM tables when using raw sql fragments
createdAt: ["posts", "created_at"],
},
});
employeeLoader.loadPagination({
postsLoader.loadPagination({
where: {
companyId: [126, 145],
employmentStartDate: {
createdDate: {
_gte: "2023-01-01"
},
employmentSalary: {
// Only gets posts after 2023-01-01, shorter than 100k characters
postLength: {
_lt: 100000
},
},
orderBy: [["company", "DESC"], ["salary", "ASC"]],
select: ["id", "firstName", "lastName", "salary", "company"],
// Ordered by author's name + createdAt
orderBy: [["name", "ASC"], ["createdAt", "DESC"]],
// Only takes these fields (the rest aren't queried)
select: ["id", "author", "age"],
// Takes the total count of posts (useful when paginating because only 25 are returned by default)
takeCount: true,
});
const oldEmployees = employeeView.load({
select: ["id", "company_id"],
// Views can be queried directly for faster access (without having to create a loader, yet getting access to all the filters)
const specificPosts = postsView.load({
select: sql.fragment\`SELECT posts.title, posts.text\`,
where: {
OR: [{
employmentStartDate: {
_lt: "2022-01-01",
}
}, {
employmentSalary: {
_gte: 100000,
}
}]
id: [123]
},
});
Expand Down

0 comments on commit ad80f4d

Please sign in to comment.