{"id":16528353,"url":"https://github.com/danvk/crudely-typed","last_synced_at":"2025-09-12T19:31:31.987Z","repository":{"id":39933186,"uuid":"477385879","full_name":"danvk/crudely-typed","owner":"danvk","description":"Simple \"everyday CRUD\" Postgres queries with perfect TypeScript types","archived":false,"fork":false,"pushed_at":"2024-07-03T15:41:20.000Z","size":130,"stargazers_count":36,"open_issues_count":8,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-12-28T02:15:15.821Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/danvk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-04-03T15:40:18.000Z","updated_at":"2024-12-06T20:39:33.000Z","dependencies_parsed_at":"2022-09-18T23:11:51.789Z","dependency_job_id":null,"html_url":"https://github.com/danvk/crudely-typed","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcrudely-typed","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcrudely-typed/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcrudely-typed/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcrudely-typed/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danvk","download_url":"https://codeload.github.com/danvk/crudely-typed/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":232778513,"owners_count":18575242,"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","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":[],"created_at":"2024-10-11T17:39:54.249Z","updated_at":"2025-01-06T19:48:57.132Z","avatar_url":"https://github.com/danvk.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Crudely Typed\n\n[![codecov](https://codecov.io/gh/danvk/crudely-typed/branch/main/graph/badge.svg?token=2C0SU9X0EM)](https://codecov.io/gh/danvk/crudely-typed)\n\nSimple \"everyday CRUD\" Postgres queries with perfect TypeScript types.\nZero dependencies. Designed to work with [pg-to-ts][] and [node-postgres][].\n\nFor more information and demos, check out:\n\n- [TypeScript and SQL: Six Ways to Bridge the Divide][post] (2023 blog post)\n- [TypeScript and the Database: Who Owns the Types?][video] (2022 TS Congress talk; 30 minutes)\n\n## Quickstart\n\nInstall [pg-to-ts][] and this library, and generate a schema file:\n\n    npm install -D pg-to-ts\n    npm install crudely-typed\n    pg-to-ts generate -c $POSTGRES_URL --output src/dbschema.ts\n\nThen generate your queries using a `TypedSQL` instance:\n\n```ts\n// src/demo.ts\nimport {TypedSQL} from 'crudely-typed';\nimport {tables} from './dbschema';\n\nconst typedSql = new TypedSQL(tables);\n\nconst getDocById = typedSql.table('docs').selectByPrimaryKey();\n//    ^? const getDocById: (db: Queryable, where: { id: string }) =\u003e Promise\u003cDoc | null\u003e\n```\n\nCrudely Typed supports the basic create / read / update / delete queries. See\n[API](#api) for details. Crudely Typed is _not_ a full-fledged query builder,\nnor does it aspire to be. See [FAQ](#faq) for more on this.\n\n## API\n\n### TypedSQL\n\nEverything starts with a `TypedSQL` instance, which you construct from the\n`tables` export of a [pg-to-ts][] DB schema. There are many schema generators\nderived from the old SchemaTS project, but crudely-typed specifically requires\npg-to-ts schemas because they have just the type references it needs.\n\n```ts\nimport {TypedSQL} from 'crudely-typed';\nimport {tables} from './dbschema';  // \u003c-- output of pg-to-ts\n\nconst typedSql = new TypedSQL(tables);\n```\n\n### table\n\nFrom a `TypedSQL` instance, you can produce a `TableBuilder` object for any of\nyour tables:\n\n```ts\nconst usersTable = typedSql.table('users');\n```\n\nThe remaining functions in crudely-typed are all defined on this table object.\nEach of the functions comes in regular and `ByPrimaryKey` variants, e.g.\n`table.select()` and `table.selectByPrimaryKey()`.\n\n### table.select\n\n```ts\ntable.select(): (db: Queryable) =\u003e Promise\u003cRow[]\u003e\n```\n\nwith no parameters, this is select all in the order returned by the database.\n\n```ts\ntable.select(options: {\n    where?: (Column | SQLAny\u003cColumn\u003e)[],\n    columns?: Column[],\n    orderBy?: [col: Column, order: 'ASC' | 'DESC'][];\n    limitOne?: boolean;\n    join?: {\n        [resultingColumnName: string]: Column\n    };\n}): (db: Queryable, where: ...) =\u003e Promise\u003c...\u003e\n```\n\nLooking at each option individually:\n\n- `where` adds a `WHERE` clause to the query:\n\n```ts\nconst docsTable = typedSql.table('docs');\nconst getDocsByAuthor = docsTable.select({where: ['author']});\n//    ^? const getDocsByAuthor: (db: Queryable, where: {author: string}) =\u003e Promise\u003cDoc[]\u003e\n```\n\nIf you specify multiple where clauses, they'll be combined with `AND`.\nYou may also specify an `ANY` clause to match one of many values.\nSee [Where clasues](#where-clauses), below.\n\n- `columns` restricts the set of columns that are retrieved (by default all\n  columns are retrieved, i.e. `SELECT *`). You can use this to avoid fetching\n  large, unneeded columns.\n\n```ts\nconst docsTable = typedSql.table('docs');\nconst getTitles = docsTable.select({columns: ['title']});\n//    ^? const getTitles: (db: Queryable) =\u003e Promise\u003c{title: string}[]\u003e\n```\n\n- `orderBy` sorts the output, i.e. it adds an `ORDER BY` clause to the query.\n  Adding an `orderBy` clause does not affect the type of the `select`.\n\n```ts\nconst docsTable = typedSql.table('docs');\nconst getDocs = docsTable.select({orderBy: [['author', 'ASC']]});\n//    ^? const getTitles: (db: Queryable) =\u003e Promise\u003cDoc[]\u003e\n```\n\n- `limitOne` adds a `LIMIT 1` clause to the query, so that it always returns\n  either zero or one row. This changes the return type from `T[]` to `T | null`.\n\n```ts\nconst docsTable = typedSql.table('docs');\nconst getTitle = docsTable.select({where: ['title'], limitOne: true});\n//    ^? const getTitle: (\n//         db: Queryable,\n//         where: {title: string}\n//       ) =\u003e Promise\u003cDoc | null\u003e\n```\n\n- `join` adds 1-1 joins to the query for columns that are foreign keys into\n  other tables. The row from the joined table comes back as an object under\n  the property name that you specify. You may specify multiple joins, though\n  they cannot be nested and they must all be 1-1.\n\n```ts\nconst docsTable = typedSql.table('docs');\nconst getDocs = docsTable.select({\n    join: {\n        author: 'author_id',\n        publisher: 'publisher_id',\n    }\n});\n// ^? const getDocs: (\n//      db: Queryable\n//    ) =\u003e Promise\u003c(Doc \u0026 {author: Author; publisher: Publisher })[]\u003e\n```\n\nYou don't need to specify the joined table or its type; crudely-typed has all\nthe information it needs from the dbschema. If you specify a set of columns to\nselect with `columns`, the foreign key need not be one of those columns.\n\n### table.selectByPrimaryKey\n\nThere's a helper for the common case of selecting by primary key:\n\n```ts\nconst docsTable = typedSql.table('docs');\nconst getDocById = docsTable.selectByPrimaryKey();\n//    ^? const getDocById: (\n//         db: Queryable,\n//         where: { id: string }\n//       ) =\u003e Promise\u003cDoc | null\u003e\n```\n\nThis is exactly equivalent to `docsTable.select({where: ['id'], limitOne: true})`\nbut saves you some typing.\n\nYou may use the `columns` and `join` and with `selectByPrimaryKey`:\n\n```ts\nconst getDocById = docsTable.selectByPrimaryKey({\n    columns: ['title'],\n    join: { author: 'author_id' }\n});\nconst doc = await getDocById(db, {id: 'doc-id'});\n//    ^? const doc: {title: string; author: Author} | null\n```\n\n### table.insert\n\n```ts\ntable.insert(): (db: Queryable, row: RowInput) =\u003e Promise\u003cRow\u003e\n```\n\nThis generates a dynamic `INSERT` query based on the properties of `row`.\nThe `RowInput` type models the required and optional columns in the table.\nIf an optional property is omitted from `row`, then it will be set to its\ndefault value and observable in the returned `Row`. If a required property\nis omitted, you'll get a type error.\n\n```ts\nconst insertDoc = docsTable.insert();\nconst doc = await insertDoc({author: 'Mark Twain', title: 'Huckleberry Finn'});\n//    ^? const doc: Doc\n```\n\nIt's sometimes desirable to prevent certain columns from being set, e.g. the\nprimary key. This can be enforced with the `disallowColumns` option:\n\n```ts\nconst insertDoc = docsTable.insert({ disallowColumns: ['id'] });\n//    ^? const insertDoc: (db: Queryable, row: Omit\u003cDocInput, 'id'\u003e) =\u003e Promise\u003cDoc\u003e\ninsertDoc({id: 'some id'});\n//         ~~ type error!\nconst doc = await insertDoc({author: 'Mark Twain', title: 'Huckleberry Finn'});\n//    ^? const doc: Doc\n```\n\n### table.insertMultiple\n\nThis is indentical to `insert` but allows multiple rows to be inserted with a\nsingle query.\n\n```ts\nconst docsTable = typedSql.table('docs');\nconst insertDocs = docsTable.insertMultiple();\n//    ^? const insertDocs: (\n//         db: Queryable,\n//         rows: readonly DocInput[]\n//       ) =\u003e Promise\u003cRow[]\u003e\nconst docs = await insertDocs([\n    {title: 'Huckleberry Finn', author: 'Mark Twain'},\n    {title: 'Poor Richard', author: 'Ben Franklin'}\n]);\n```\n\n`insertMultiple` also supports `disallowColumns`, just like `insert`.\n\n### table.update\n\n```ts\ntable.update({\n    where?: (Column | SQLAny\u003cColumn\u003e)[],\n    set?: Column[],\n    limitOne?: boolean,\n}): (db: Queryable, where: ..., set: ...) =\u003e Promise\u003c...\u003e\n```\n\nWith a `where` and a `set` clause, this updates specific columns on specific rows. All affected rows are returned:\n\n```ts\nconst docsTable = typedSql.table('docs');\nconst setYear = docsTable.update({where: ['title'], set: ['year']});\n//    ^? const setYear: (\n//         db: Queryable,\n//         where: {title: string},\n//         set: {year: number}\n//       ) =\u003e Promise\u003cDoc[]\u003e\nconst newDocs = setYear(db, {title: 'Huck Finn'}, {year: 1872});\n//    ^? const newDocs: Promise\u003cDoc[]\u003e\n```\n\nWithout a `set` clause, this performs a dynamic update based on the param:\n\n```ts\nconst update = docsTable.update({where: ['title']});\n//    ^? const update: (\n//         db: Queryable,\n//         where: {title: string},\n//         set: Partial\u003cDoc\u003e\n//       ) =\u003e Promise\u003cDoc[]\u003e\nconst newDocs = setYear(db, {title: 'Huck Finn'}, {year: 1872});\n//    ^? const newDocs: Promise\u003cDoc[]\u003e\n```\n\nThe `where` clause can include multiple columns, in which case it operates as\nan `AND`, and can support `ANY` clauses.\nSee [Where clasues](#where-clauses), below.\n\nWithout a `where` clause, this updates all rows in the table.\n\nIf you pass `limitOne: true`, at most one row will be updated and the function\nwill return `T | null` instead of `T[]`:\n\n```ts\nconst update = docsTable.update({where: ['title'], limitOne: true});\n//    ^? const update: (\n//         db: Queryable,\n//         where: {title: string},\n//         set: Partial\u003cDoc\u003e\n//       ) =\u003e Promise\u003cDoc | null\u003e\nconst newDoc = setYear(db, {title: 'Huck Finn'}, {year: 1872});\n//    ^? const newDocs: Promise\u003cDoc\u003e\n```\n\n### table.updateByPrimaryKey\n\nThis is a shortcut for updating a row by its table's primary key:\n\n```ts\nconst update = docsTable.updateByPrimaryKey(set: ['year']);\n//    ^? const update: (\n//         db: Queryable,\n//         where: {id: string},\n//         set: {year: number}\n//       ) =\u003e Promise\u003cDoc | null\u003e\nconst newDoc = setYear(db, {id: 'isbn-123'}, {year: 1872});\n//    ^? const newDoc: Promise\u003cDoc\u003e\n```\n\nIf you pass a `set` option, then this updates a fixed set of columns.\nIf you don't, it's dynamic based on its parameter, just like `update`.\n\n### table.delete\n\n```ts\ntable.delete(options: {\n    where?: (Column | SQLAny\u003cColumn\u003e)[];\n    limitOne?: boolean;\n}): (db: Queryable, where: ...) =\u003e Promise\u003c...\u003e\n```\n\nThe `where` clause for `delete` works exactly as it does for `select`. It may\nbe set to an array of columns or `ANY` clauses. See\n[Where clauses](#where-clauses), below.\n\n```ts\nconst docsTable = typedDb.table('docs');\nconst deleteByTitle = docsTable.delete({ where: ['title'] });\n//    ^? const deleteByTitle: (db: Queryable, where: {title: string}) =\u003e Promise\u003cDoc[]\u003e\n```\n\nThe `delete` function returns the rows that it deletes (if any). As with\n`select`, if you pass `limitOne: true` then it will return `T | null` instead\nof `T[]`:\n\n```ts\nconst docsTable = typedDb.table('docs');\nconst deleteByTitle = docsTable.delete({ where: ['title'], limitOne: true });\n//    ^? const deleteByTitle: (db: Queryable, where: {title: string}) =\u003e Promise\u003cDoc | null\u003e\n```\n\nIf you don't specify a `where` clause or `limitOne`, this acts as \"delete all\".\n\n### table.deleteByPrimaryKey\n\nThis is a helper for the common case where you want to delete rows by their\nprimary key:\n\n```ts\nconst docsTable = typedDb.table('docs');\nconst deleteDoc = docsTable.deleteByPrimaryKey();\n//    ^? const deleteDoc: (db: Queryable, where: {id: string}) =\u003e Promise\u003cDoc | null\u003e\n```\n\nThis is exactly equivalent to `docsTable.delete({ where: ['id'], limitOne: true })`.\n\n## Queryable\n\nAll generated functions take a `Queryable` object as their first parameter:\n\n```ts\ninterface Queryable {\n  query(sql: string, ...args: any[]): Promise\u003cResult\u003e;\n}\n```\n\nThe `Client` and `Pool` classes from `pg` conform to this and can be used with\ncrudely-typed.\n\n## Where clauses\n\nTODO:\n\n- Multiple columns are `AND`ed\n- You can generate `ANY` matchers\n- You can generate a mix\n\n## Joins\n\nTODO\n\n## FAQ\n\n- **Isn't this just a query builder?**\n\n- **Why does crudely-typed generate functions instead of running them?**\n\n- **Can you add support for X?**\n\nProbably not! The goal of this library is to handle the simplest queries for\nyou with perfect types and a minimum of fuss. Supporting every SQL query is\nabsolutely not a goal. At some point you should just write SQL (see below).\n\n- **What should I do for complex queries?**\n\n- **Why not use PgTyped for all my queries?**\n\n- **Why not use an ORM?**\n\n- **What's with the name?**\n\nCRUD is short for Create, Read, Update, Delete. I wanted something that had\n\"crud\" in the name but didn't sound like \"crud\". \"crudely\" fit the bill. It's\nalso a play on [DefinitelyTyped][dt] and is a bit tongue in cheek since the\ntypes in this library are anything but crude.\n\n## How this works\n\nThe `tables` object that `pg-to-ts` outputs includes all the TypeScript types\nand runtime values needed to generate well-typed queries. From there it's just\na bunch of TypeScript generics that should be entirely invisible to you, the\nuser. See [`index.ts`](/src/index.ts) for all the details. The following blog\nposts may be helpful for understanding some of the techniques being used:\n\n- intersect what you have\n- unionize/objectify\n- display of type\n- currying and classes\n\n[pg-to-ts]: https://github.com/danvk/pg-to-ts\n[node-postgres]: https://github.com/brianc/node-postgres\n[dt]: https://github.com/DefinitelyTyped/DefinitelyTyped\n[post]: https://effectivetypescript.com/2023/08/29/sql/\n[video]: https://portal.gitnation.org/contents/typescript-and-the-database-who-owns-the-types\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanvk%2Fcrudely-typed","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanvk%2Fcrudely-typed","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanvk%2Fcrudely-typed/lists"}