{"id":14156538,"url":"https://github.com/ardsh/slonik-trpc","last_synced_at":"2025-08-11T17:08:53.370Z","repository":{"id":63589060,"uuid":"568359952","full_name":"ardsh/slonik-trpc","owner":"ardsh","description":"Type-safe efficient query API engine using slonik and zod","archived":false,"fork":false,"pushed_at":"2024-08-29T19:14:49.000Z","size":1903,"stargazers_count":39,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-09T16:26:59.235Z","etag":null,"topics":["api-engine","postgresql","query-builder","slonik","sql","trpc","typescript","zod"],"latest_commit_sha":null,"homepage":"https://ardsh.github.io/slonik-trpc/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ardsh.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2022-11-20T09:36:11.000Z","updated_at":"2025-07-25T23:22:20.000Z","dependencies_parsed_at":"2023-09-21T20:32:29.157Z","dependency_job_id":"b6a9abc6-8c24-4e89-bfff-38d073e7f73a","html_url":"https://github.com/ardsh/slonik-trpc","commit_stats":{"total_commits":75,"total_committers":3,"mean_commits":25.0,"dds":0.3733333333333333,"last_synced_commit":"87677d53db170fe8bf1c99bc3b6fc52aa5d88f8a"},"previous_names":[],"tags_count":54,"template":false,"template_full_name":null,"purl":"pkg:github/ardsh/slonik-trpc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ardsh%2Fslonik-trpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ardsh%2Fslonik-trpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ardsh%2Fslonik-trpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ardsh%2Fslonik-trpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ardsh","download_url":"https://codeload.github.com/ardsh/slonik-trpc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ardsh%2Fslonik-trpc/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":269799407,"owners_count":24477635,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-10T02:00:08.965Z","response_time":71,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-engine","postgresql","query-builder","slonik","sql","trpc","typescript","zod"],"created_at":"2024-08-17T08:06:06.199Z","updated_at":"2025-08-11T17:08:53.338Z","avatar_url":"https://github.com/ardsh.png","language":"TypeScript","funding_links":[],"categories":["typescript"],"sub_categories":[],"readme":"# slonik-trpc\n\n[![Version](https://img.shields.io/npm/v/slonik-trpc?color=success)](https://www.npmjs.com/package/slonik-trpc)\n![Coverage](coverage/badge.svg)\n![License](https://img.shields.io/npm/l/slonik-trpc)\n\n[Convert any SQL query](https://theartofpostgresql.com/blog/2019-09-the-r-in-orm) into an API.\n\n## Getting started\n\nAdd the package and its peer dependencies to your project\n\n```sh\nyarn add slonik-trpc slonik zod\n```\n\n### List of features\n\n- [x] Declarative [filtering API](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/filtering) (filter creation utils included!)\n  - [x] Automatic support for `AND`, `NOT`, `OR` in all the filters\n  - [x] Ability to add [authorization filters](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/authorization) based on user auth context.\n- [x] [Select the fields](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/overfetching) you need, to avoid the overfetching problem.\n  - [x] Fully type-safe, only selected fields are returned in the types. If no selects are specified, every field is returned.\n- [x] Runtime validation of input (completely safe against unsanitized inputs).\n- [x] Optional runtime validation of output (Your zod types can be executed against your result, including transforming the output fields with zod transformers).\n- [x] [Virtual field](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/virtual-columns) declaration in typescript (fully type-safe + with async support).\n- [x] [Declarative sorting](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/sorting) capabilities, with support for custom SQL sorting expressions\n- [x] [DISTINCT ON](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/distinct) support with PostgreSQL, for all sortable columns\n- [x] [Offset-based pagination](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/pagination).\n- [x] [Cursor-based pagination](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/cursor-pagination).\n  - [x] Reverse page support\n- [x] [Plugins](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/plugins)\n\n### Requirements\n\nTypeScript 4.5+\nSlonik 33+\nZod 3+\n\n### Demo\n\n[Basic demo](https://githubbox.com/ardsh/slonik-trpc/tree/main/examples/datagrid-example) of a table with pagination, sorting and filtering in codesandbox.\n\n[Playground](https://sql-api-playground.netlify.app/) where you can see the SQL queries that the engine creates for custom loaders.\n\n## Basic Usage\n\nLet's say you have this SQL query for a list of posts you'd like to create an API for\n\n```sql\nSELECT \"posts\".id\n  , \"users\".\"firstName\" || ' ' || \"users\".\"lastName\" AS \"author\"\n  , \"posts\".\"text\"\n  , EXTRACT(DAYS FROM NOW() - posts.\"created_at\") AS \"age\"\nFROM users\nLEFT JOIN posts\n    ON posts.author = users.id\n```\n\nFirst, get the FROM clause part of the query and build a \"view\" with it.\n\n```ts\nimport { buildView, sql } from 'slonik-trpc';\n\nconst postsView = buildView`\nFROM users\nLEFT JOIN posts\n    ON posts.author = users.id`\n// Allows filtering posts.title: { _ilike: '%hello%' }\n.addStringFilter('posts.title')\n// Allows filter id: [1, 2, 3]\n.addInArrayFilter('id', () =\u003e sql.fragment`posts.id`, 'numeric')\n```\n\nYou can [add filters](#filtering) when building the view. The above filters allow searching posts by their title, or their id.\n\nThen get the SELECT part of the query, and specify the return type with zod.\n\n```ts\nimport { z } from 'zod';\nimport { sql, makeQueryLoader } from 'slonik-trpc';\n\nconst queryType = z.object({\n    id: z.string(),\n    author: z.string(),\n    text: z.string(),\n    age: z.number(),\n});\nconst selectQuery = sql.type(queryType)`\nSELECT \"posts\".id\n  , \"users\".\"firstName\" || ' ' || \"users\".\"lastName\" AS \"author\"\n  , \"posts\".\"text\"\n  , EXTRACT(DAYS FROM NOW() - posts.\"created_at\") AS \"age\"\n`\n\n// This loader is the type-safe API\nconst loader = makeQueryLoader({\n    db: slonikConnection, // Any connection pool will do\n    query: {\n        select: selectQuery,\n        view: postsView,\n    }\n});\n```\n\nNow you can query this loader/API by selecting just the fields you need.\n\n```ts\n// This array is type-safe, only having \"id\", \"text\", and \"age\" fields (no author)\nconst posts = await loader.load({\n    select: [\"id\", \"text\", \"age\"],\n    where: {\n        OR: [{\n            \"posts.title\": \"Lorem Ipsum\",\n        }, {\n            \"posts.id\": [3, 4],\n        }]\n    },\n    take: 200,\n});\n```\n\nRead on for more advanced usage, including how to use sorting, filtering, cursor pagination, authorization checks, creating a tRPC API and more.\n\n### [Documentation](https://ardsh.github.io/slonik-trpc/)\n\nYou can refer to [the documentation](https://ardsh.github.io/slonik-trpc/) for advanced use-cases and tutorials.\n\nRead [slonik documentation](https://github.com/gajus/slonik#user-content-slonik-usage-connection-uri) for how to get started with it, but [you can also use other clients](#i-dont-wanna-switch-database-clients-eg-im-already-using-prisma-client), as long as they accept SQL and return an array of objects.\n\nDatabases other than PostgreSQL aren't supported as of now, but can be made to work in limited use-cases (e.g. sqlite will work by specifying `useSqlite` in options).\n\n## Advanced Usage\n\n### Custom queries\n\nLet's say you want to add a custom sub-query as a field to your relation.\nYou can do this very easily.\n\n```ts\nimport { sql, buildView } from 'slonik-trpc';\nconst query = sql.type(z.object({\n    id: z.string(),\n    name: z.string(),\n    email: z.string(),\n    authoredPosts: z.number(),\n    created_at: z.string()\n}))`SELECT *,\n    -- Count all the posts of the user as a number\n    (SELECT COUNT(*) FROM posts WHERE posts.author = users.id) AS \"authoredPosts\"`;\nconst userView = buildView`FROM users`;\n```\n\nNote this count sub-query will be called only when the authoredPosts selected. Otherwise the database won't execute it, ensuring optimal performance.\n\n### [tRPC API](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/trpc)\n\nUse `loader.getLoadArgs` for getting the tRPC query input zod type.\n\n```ts\ngetUsers: publicProcedure\n    .input(loader.getLoadArgs({\n        disabledFilters: {\n            // OR filters can be disabled on a public API\n            OR: true,\n        }\n    }))\n    .query(({ input, ctx }) =\u003e {\n        return loader.loadPagination({\n            ...input,\n            ctx,\n        });\n    }),\n```\n\nNow it can be called via tRPC:\n\n```ts\nconst users = await client.getUsers.query({\n    select: [\"id\", \"name\", \"email\"], // Will query only these 3 columns, ignoring the rest\n    take: 25,\n    skip: 0,\n});\n```\n\nThis returns a relay-like pagination result, with `nodes` and `pageInfo` keys.\n\n### Filtering\n\nYou can add any filter to the API, by specifying the filter type, and the optional interpreter function that converts the filter input to a SQL fragment, in a declarative way.\n\nHere's an example:\n\n```ts\nbuildView`FROM users`\n    .addInArrayFilter('id')\n    .addStringFilter(['users.name', 'users.profession'])\n    .addComparisonFilter('postsCount', () =\u003e sql.fragment`users.\"authoredPosts\"`)\n    .addDateFilter('createdDate', () =\u003e sql.fragment`users.created_at`)\n    .addJsonContainsFilter('users.settings') // jsonb column\n    .addBooleanFilter('isGmail', () =\u003e sql.fragment`users.email ILIKE '%gmail.com'`)\n```\n\n`AND`, `OR` and `NOT` filters are added automatically, to allow more complex combinations.\n\n```ts\nconst specificUsers = await filtersLoader.load({\n    where: {\n        // users created between yesterday and now\n        createdDate: {\n            _lte: new Date().toISOString(),\n            _gte: new Date(Date.now() - 1000 * 3600 * 24).toISOString(),\n        },\n        \"users.settings\": {\n            // This allows filtering by a nested jsonb/json column\n            notifications: true,\n            // Users whose settings have notifications turned on, and a dark theme\n            theme: 'dark'\n        },\n        \"users.profession\": {\n            _ilike: '%engineer%',\n        },\n        OR: [{\n            // Returns users with these specific ids\n            id: ['x', 'y'],\n        }, {\n            // or non-gmail users.\n            isGmail: false,\n        }, {\n            // Or with less than 20 posts (by inverting the postsCount greater than filter)\n            NOT: {\n                postsCount: {\n                    _gte: 20,\n                },\n            }\n        }]\n    }\n});\n```\n\nThe `AND`, `OR`, and `NOT` can be disabled when calling `getLoadArgs`, and it is recommended to do so for the `OR` filter, which is often computationally expensive and shouldn't be allowed on a public API.\n\n```ts\nloader.getLoadArgs({\n    disabledFilters: {\n        // OR filters should be disabled on a public API\n        OR: true,\n    }\n})\n```\n\nThe `addGenericFilter` method offers more freedom, by letting you specify any valid SQL fragment in response to a value. The filter is then applied for any non-null value.\n\n### Authorization\n\nFor authorization checks, use the `constraints` option to add hardcoded conditions to the query, based on the context.\n\n```ts\nconst userLoader = makeQueryLoader({\n    db,\n    query,\n    filters,\n    constraints(ctx) {\n        if (ctx.role === 'ADMIN') {\n            // Allow admins to query anyone by returning no extra permission rules.\n            return null;\n        } else {\n            // Only allow querying the users in the same org as the logged in user\n            return sql.fragment`users.org_id=${ctx.orgId}`;\n        }\n    }\n});\n```\n\nConstraints are like hardcoded filters that are always applied.\nAny kind of SQL condition will do, and you can also return an array of multiple conditions, they will be joined with `AND`.\n\nYou can also specify constraints when creating a view\n\n```ts\nconst userView = buildView`FROM posts\nLEFT JOIN users\n    ON posts.author = users.id`\n.setConstraints((ctx) =\u003e {\n    return sql.fragment`users.org_id=${ctx.orgId}`;\n})\n```\n\nThese conditions will be added anywhere the view is used.\n\n#### Postprocessing columns\n\nIf you need to check specific columns, you can virtualize them with the same name, and run any checks.\n\n```ts\nconst virtualFieldsLoader = makeQueryLoader({\n    query,\n    virtualFields: {\n        name: {\n            resolve(row, ctx) {\n                if (!ctx.isLoggedIn) {\n                    // Return null if user isn't logged in.\n                    return null;\n                }\n                return row.name;\n            }\n        },\n        email: {\n            resolve(row, ctx) {\n                if (ctx.isAdmin) {\n                    // Only return user emails if current user is admin.\n                    return row.email;\n                }\n                return null;\n            },\n        },\n    }\n});\n```\n\n### Sorting\n\nDefine your sortable column aliases. You can use strings or sql fragments to identify the columns.\n\n```ts\nconst sortableLoader = makeQueryLoader({\n    query,\n    sortableColumns: {\n        id: sql.fragment`users.id`,\n        name: \"name\",\n        // All 3 methods are acceptable as long as the specified column is accessible from the FROM query and non-ambiguous\n        createdAt: [\"users\", \"created_at\"],\n    },\n});\n\nconst sortedByName = await db.any(await sortableLoader.getQuery({\n    orderBy: [\"name\", \"ASC\"],\n}));\n\nconst sortedByNameAndDate = await db.any(await sortableLoader.getQuery({\n    orderBy: [[\"name\", \"DESC\"], [\"id\", \"ASC\"]],\n    take: 5,\n}))\n```\n\nIf you don't specify any sortableColumns your API won't be sortable with orderBy.\n\nIf you specify a negative take, the order will be reversed, this is useful for getting the last page...\n\n```ts\nconst lastPage = await db.any(await sortableLoader.getQuery({\n    orderBy: [[\"name\", \"DESC\"], [\"id\", \"ASC\"]],\n    take: -25,\n}));\n```\n\n### Distinct On\n\nYou can additionally use `sortableColumns` in the `distinctOn` option.\nTo get only posts by distinct authors use this:\n\n```ts\nconst distinctAuthors = await db.any(await sortableLoader.getQuery({\n    distinctOn: [\"name\"],\n    take: 5,\n}))\n```\n\n### Grouping\n\nYou can add a `groupBy` fragment to your query, to be able to use aggregates in your select.\n\nE.g. for getting the user's posts count, you can use\n\n```ts\nconst groupedLoader = makeQueryLoader({\n    query: {\n        select: sql.type(z.object({\n            count: z.number(),\n            name: z.string(),\n        }))`SELECT COUNT(*), users.name`,\n        view: buildView`FROM posts\n            LEFT JOIN users\n                ON posts.author = users.id`\n            .addInArrayFilter('posts.category'),\n        groupBy: sql.fragment`users.name`,\n    },\n});\n```\n\nNow you can easily filter authors by their posts, and still get the posts count.\n\n```ts\nconst bigPostsCount = await sortableLoader.load({\n    where: {\n        'posts.category': [\"typescript\"]\n    },\n});\n// Returns counts of typescript posts for each user.\n```\n\n### Cursor-based pagination\n\nIf you've enabled any sorting columns, you can use cursor-based pagination with `cursur`. Simply specify the cursor of the item you'd like to paginate after.\n\n```ts\nconst usersAfterBob = await db.any(await sortableLoader.getQuery({\n    orderBy: [[\"name\", \"DESC\"], [\"id\", \"ASC\"]],\n    cursor: \"eyJuYW1lIjoiQm9iIiwiaWQiOjQ1fQ==\",\n    take: 5,\n}));\n```\n\nYou'd normally retrieve this cursor from the previous result e.g. `result.pageInfo.endCursor`, if you specify `takeCursors: true`.\n\n#### Manual cursor-based pagination\n\nYou can also do prisma-style cursor pagination by supplying the values yourself.\n\n```ts\nconst usersAfterBob = await db.any(await sortableLoader.getQuery({\n    orderBy: [[\"name\", \"DESC\"], [\"id\", \"ASC\"]],\n    searchAfter: {\n        name: \"Bob\",\n        id: 45,\n    },\n    take: 5,\n}))\n```\n\nThis is equivalent to the above example, since `eyJuYW1lIjoiQm9iIiwiaWQiOjQ1fQ==` base64 decoded is `{\"name\":\"Bob\",\"id\":45}`.\n\n### Virtual fields\n\nVirtual fields are only supported when using `load`/`loadPagination` currently.\n\n```ts\nconst virtualFieldsLoader = makeQueryLoader({\n    query,\n    virtualFields: {\n        fullName: {\n            dependencies: [\"first_name\", \"last_name\"],\n            async resolve(row) {\n                // async code supported\n                return Promise.resolve(row.first_name + row.last_name);\n            },\n        },\n    }\n});\n```\n\nThe virtual fields can then be selected like normal fields.\n\n#### Async loading in virtual fields\n\nIf you need to load remote data in your virtual fields, it's often more efficient to use a batch loader, rather than returning a promise in the resolve function of each row. Use `load` for this purpose.\n\n##### ❌ **⚠️ Avoid This!**\n\n```ts\nvirtualFields: {\n    posts: {\n        dependencies: [\"id\"],\n        // 📉 This can lead to performance issues.\n        resolve: async (row) =\u003e {\n            const posts = await fetchPostsForAuthor(row.id);\n            return posts;\n        }\n    }\n},\n```\n\n##### ✅ Do This Instead!\n\n```ts\nvirtualFields: {\n    posts: {\n        dependencies: [\"id\"],\n        // 🚀 Use the load function to fetch data in batches\n        async load(rows) {\n            const allPosts = await fetchPostsForAuthors(rows.map(row =\u003e row.id));\n            return allPosts;\n        },\n        // ✔️ Use the resolve function to handle synchronous operations.\n        resolve: (row, args, posts) =\u003e {\n            // Only return the posts of each user.\n            return posts.filter(post =\u003e post.authorId = row.id)\n        }\n    }\n},\n```\n\n### Querying a view directly\n\nYou can query a view directly for easier usage.\n\n```ts\nconst usersView = buildView`FROM users`\n    .addInArrayFilter('id')\n    .addStringFilter(['users.name', 'users.profession'])\n    .addComparisonFilter('postsCount', () =\u003e sql.fragment`users.\"authoredPosts\"`)\n    .addDateFilter('createdDate', () =\u003e sql.fragment`users.created_at`)\n    .addJsonContainsFilter('users.settings') // jsonb column\n    .addBooleanFilter('isGmail', () =\u003e sql.fragment`users.email ILIKE '%gmail.com'`)\n    .setColumns([\"name\", \"profession\", \"email\"])\n    .setColumns({\n        createdDate: sql.fragment`users.created_at AS \"createdDate\"`,\n        postsCount: sql.fragment`( SELECT COUNT(*) FROM posts\n        WHERE posts.user_id = users.id) AS \"postsCount\"`,\n    })\n\nconst data = await usersView.load({\n    select: [\"name\", \"profession\", \"postsCount\"],\n    where: {\n        id: [1, 2],\n    },\n    orderBy: sql.fragment`users.name DESC`,\n})\n```\n\nThe idea is you declare all the filters and relevant columns upfront, and then you can reuse them easily.\n\n### Nested arrays and objects\n\nYou can use PostgreSQL functions for creating json arrays and objects in your query. Two such utils are exposed for convenience through `rowToJson` and `rowsToArray`.\n\n```ts\nimport { rowToJson, rowsToArray } from 'slonik-trpc/utils';\n\nconst query = sql.type(z.object({\n    id: z.string(),\n    name: z.string(),\n    email: z.string(),\n    contactInfo: z.object({\n        phoneNumber: z.string(),\n        zip: z.string(),\n        address: z.string(),\n    }),\n    posts: z.array(z.object({\n        text: z.string(),\n        title: z.string(),\n    })),\n}))`SELECT\n    users.id,\n    name,\n    email,\n    ${rowToJson(sql.fragment`\n        SELECT \"phoneNumber\", \"zip\", \"address\"\n        WHERE contact_info.id IS NOT NULL\n    `, 'contactInfo')}\n    ${rowsToArray(sql.fragment`\n        SELECT text, title`, sql.fragment`\n        FROM posts\n        WHERE posts.author = users.id`,\n        'posts'\n    )}\nFROM users\nLEFT JOIN contact_info\nON contact_info.id = users.contact_info`;\n```\n\nIf you wanna prevent conflicts of field names when joining multiple tables, it's recommended to wrap the whole query in a `SELECT * FROM (subQuery) alias`, which has the effect of returning only a single view, thus making conflicts impossible.\n\nRefer to the [minimal-example](https://stackblitz.com/github/ardsh/slonik-trpc/tree/main/examples/minimal-trpc), or [datagrid-example](https://githubbox.com/ardsh/slonik-trpc/tree/main/examples/datagrid-example) for more complete examples.\n\n## FAQ\n\n### I don't wanna switch database clients (e.g. I'm already using Prisma client)\n\nThis is not officially supported, but you can actually provide any function to the `db` loader argument. Then you won't have to setup slonik client and can use another one like prisma client, as long as it's able to send SQL queries.\nSo with prisma you can write the following when creating the loader:\n\n```ts\nexport const loader = makeQueryLoader({\n    query,\n    db: {\n        any: (sql) =\u003e {\n            return prisma.$queryRawUnsafe(sql.sql, ...sql.values);\n        }\n    },\n    // ...\n```\n\nYou still need to install slonik and zod though, for composing query fragments with. Since the SQL string is generated from slonik, it's completely safe from SQL injection, so it's safe to be used with `prisma.$queryRawUnsafe`.\n\nIt is likely you will run into issues doing this though, so using the slonik package as a client is recommended. [Refer to the documentation](https://ardsh.github.io/slonik-trpc/docs/usage-main-features/slonik) for a minimal setup example of slonik client.\n\n### Can I use custom SQL views or sub-queries\n\nYes, any PostgreSQL query is supported. So you can do anything that is allowed in PostgreSQL, e.g. to get postCount of users directly in a sub-query do\n\n```sql\nSELECT id\n  , name\n  , (SELECT COUNT(*) FROM posts WHERE users.id = posts.author_id) AS postCount\n  , email\n  , created_at\nFROM users\n```\n\n### Why do I have to declare both zod types and SQL queries \n\nIt is theoretically possible to infer the zod type from the SQL query you write.\nE.g. [@slonik/typegen](https://github.com/mmkal/slonik-tools/tree/main/packages/typegen) generates typescript types.\n\nLet me know if there already are packages that convert PostgreSQL queries to zod types, we can try to make them compatible and easier to work with the query loaders.\n\nThis is a good issue to contribute on. All help is welcome!\n\n### Can I see the SQL queries that are being executed\n\nIf you mean debugging during runtime, you can do `export DEBUG=slonik-trpc`.\nYou can also run `getQuery` with the same parameters as `load/loadPagination`, and it will return the raw SQL query without executing anything.\n\nThis is useful for many reasons, e.g. if you want to analyze a slow query, you can measure the time it takes to run, and then print out the SQL for any queries that are slower than a threshold.\n\nNote that `loadPagination` will run an extra query for getting the total count, if you specify `takeCount: true`. This is the same query as returned by getQuery, but wrapped in a `SELECT COUNT` query like\n\n```sql\nSELECT COUNT(*) FROM (...) subQuery\n```\n\n### Can this be used as an ORM\n\nMany features of ORMs, such as updating data, aren't supported at all by this package.\n\nHowever, this package can be integrated with any ORM that supports executing raw queries, such as [prisma](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access).\n\nSo you can use them both at the same time, by using slonik-trpc to build a type-safe yet flexible read-only API, and use the ORM for the rest.\n\n## Known issues\n\nIf you're passing a select array conditionally, typescript type inference doesn't allow getting the underlying type correctly. You'll have to use type assertions in that case, use `as any` if one condition has an empty array/undefined to get all the fields returned properly. Otherwise typescript will only return id/name.\n\n```ts\nconst data = await loader.load({\n    select: someCondition ? ['id', 'name'] : [] as any\n})\n```\n\nDo not pass any conditions after the select ... from statement. If you must run custom SQL conditions in your queries, simply wrap them in a sub-query\n\n```ts\nconst query = sql.type(z.object({\n    id: z.string(),\n    name: z.string(),\n    email: z.string(),\n    created_at: z.string()\n}))`SELECT * FROM (\n    SELECT id, name, email, created_at\n    FROM users\n    WHERE users.created_at \u003e NOW() - INTERVAL '1 month'\n    ORDER BY users.created_at\n) x`;\n```\n\n### Inferring returned types in the clientside\n\nDue to [typescript limitations](https://github.com/trpc/trpc/discussions/2150) it's not possible to infer the returned types automatically in the clientside with trpc. So all fields will be selected by default, even if you only select a few.\n\nTo get around this, you can use `InferPayload` and pass the arguments as a generic argument.\n\nserver\n```ts\nimport type { InferPayload, InferArgs } from 'slonik-trpc';\n\nconst postsLoader = makeQueryLoader(...);\n\ntype PostLoader = typeof postsLoader;\n\nexport type {\n    InferPayload, InferArgs, PostLoader\n}\n```\n\nclient\n```ts\nconst getPosts = \u003cTArgs extends InferArgs\u003cPostLoader\u003e\u003e(args: TArgs) =\u003e {\n    return client.loadPosts.query(args) as Promise\u003cInferPayload\u003cPostLoader, TArgs\u003e[]\u003e;\n}\n```\n\nOr with hooks + loadPagination and a useful type annotation to replace the pagination `nodes` key easily.\n\n```ts\nimport type { InferArgs, InferPayload, Post } from '../../server';\n\ntype ReplaceNodes\u003cTResult, TPayload\u003e = TResult extends { nodes?: ArrayLike\u003cany\u003e, hasNextPage?: boolean } ? Omit\u003cTResult, \"nodes\"\u003e \u0026 {\n    nodes: TPayload[]\n} : TResult extends object ? {\n    [K in keyof TResult]: ReplaceNodes\u003cTResult[K], TPayload\u003e\n} : TResult;\n\nconst getPosts = \u003cTArgs extends InferArgs\u003cPostLoader\u003e\u003e(args: TArgs) =\u003e {\n    const result = trpc.loadPosts.useQuery(args);\n    return result as ReplaceNodes\u003ctypeof result, InferPayload\u003cPostLoader, TArgs\u003e\u003e;\n}\n```\n\nYou don't need to do this if your selects are dynamic, in that case simply consider all fields optional.\n\nAlso this is not necessary when querying the loader directly in the serverside, in those cases fields will be automatically inferred depending on your selections.\n\nThere are more advanced patterns to support [relay-like fragment selections](https://dev.to/ardsh/how-to-solve-overfetching-with-trpc-apis-when-rendering-tables-pt-1-fbg), or [cursor pagination](https://ardsh.github.io/slonik-trpc/docs/client-type-safety/cursor-pagination) that allows rendering each table column in a type-safe method.\n\n## 🤝 Contributing\n\nContributions, issues and feature requests are welcome!\u003cbr /\u003eFeel free to check [issues page](issues).\n\n### Testing\n\nExport your postgres database URL, then run `yarn test`\n\n```bash\nexport DATABASE_URL=postgresql://postgres:password@localhost:5432/database\nyarn test\n```\n\n### Ideas\n\n- Prisma generator similar to [zod-prisma](https://github.com/CarterGrimmeisen/zod-prisma) that automatically creates data loaders from prisma schemas.\n- Automatically syncing the zod types with sql, using something like [@slonik/typegen](https://github.com/mmkal/slonik-tools/tree/main/packages/typegen)\n- Wildcards in selects, e.g. `select: [\"name*\"]` to select all fields that start with name.\n- Custom loaders and/or plugins/middlewares for processing query results.\n- Mutations, through an update API builder.\n  - Furthermore, building an entire CRUD tRPC API, by only requiring you to declare small SQL fragments and their zod types. Similar to hasura.\n- ~~Integration with [prisma client raw database access](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access), and other DB clients, separating data loading from query composition.~~ [Done](https://github.com/ardsh/slonik-trpc/blob/main/examples/datagrid-example/src/server/db/loaders/employeeLoader.ts).\n\n## 📝 License\n\nCopyright © 2023 [ardsh](https://github.com/ardsh).\u003cbr /\u003e\nThis project is [MIT](LICENSE) licensed.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fardsh%2Fslonik-trpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fardsh%2Fslonik-trpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fardsh%2Fslonik-trpc/lists"}