{"id":15062156,"url":"https://github.com/tilyupo/qustar","last_synced_at":"2025-04-09T06:10:02.523Z","repository":{"id":248683787,"uuid":"829398371","full_name":"tilyupo/qustar","owner":"tilyupo","description":"Query SQL database through an array-like API","archived":false,"fork":false,"pushed_at":"2024-09-16T10:27:54.000Z","size":670,"stargazers_count":321,"open_issues_count":3,"forks_count":5,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-02T04:06:10.930Z","etag":null,"topics":["database","javascript","mariadb","mysql","nodejs","orm","postgres","postgresql","query-builder","qustar","sql","sqlite","typescript"],"latest_commit_sha":null,"homepage":"https://github.com/tilyupo/qustar","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/tilyupo.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-16T10:52:51.000Z","updated_at":"2025-03-21T18:28:44.000Z","dependencies_parsed_at":"2024-08-22T18:44:56.167Z","dependency_job_id":"dfe74033-3352-4a11-bc6f-a97501de75d2","html_url":"https://github.com/tilyupo/qustar","commit_stats":null,"previous_names":["tilyupo/qustar"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tilyupo%2Fqustar","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tilyupo%2Fqustar/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tilyupo%2Fqustar/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tilyupo%2Fqustar/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tilyupo","download_url":"https://codeload.github.com/tilyupo/qustar/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247987285,"owners_count":21028895,"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":["database","javascript","mariadb","mysql","nodejs","orm","postgres","postgresql","query-builder","qustar","sql","sqlite","typescript"],"created_at":"2024-09-24T23:31:13.752Z","updated_at":"2025-04-09T06:10:02.498Z","avatar_url":"https://github.com/tilyupo.png","language":"TypeScript","funding_links":[],"categories":["database","typescript"],"sub_categories":[],"readme":"# Qustar\n\n[![npm version](https://img.shields.io/npm/v/qustar.svg)](https://www.npmjs.com/package/qustar)\n[![MIT license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/tilyupo/qustar/blob/main/LICENSE)\n\nQuery SQL database through an array-like API.\n\n## Features\n\n✅ Expressive AND high-level query builder  \n✅ TypeScript support  \n✅ SQL databases:  \n  ✅ PostgreSQL  \n  ✅ SQLite  \n  ✅ MySQL  \n  ✅ MariaDB  \n  ⬜ SQL Server  \n  ⬜ Oracle  \n✅ Navigation properties  \n✅ Codegen free  \n✅ Surprise free, all queries produce 100% SQL  \n✅ Raw SQL  \n⬜ Migrations  \n⬜ Transactions\n\n## Quick start\n\nTo start using qustar with PostgreSQL (the list of all supported data sources is [available below](#supported-database-drivers)) run the following command:\n\n```sh\nnpm install qustar qustar-pg pg\n```\n\nHere an example usage of qustar:\n\n```ts\nimport {PgConnector} from 'qustar-pg';\nimport {Q} from 'qustar';\n\n// specify a schema\nconst users = Q.table({\n  name: 'users',\n  schema: {\n    // generated is not required during insert\n    id: Q.i32().generated(), // 32 bit integer\n    firstName: Q.string(), // any text\n    lastName: Q.string(),\n    age: Q.i32().null(), // nullable integer\n  },\n});\n\n// compose a query\nconst query = users\n  .orderByDesc(user =\u003e user.createdAt)\n  // map will be translated into 100% SQL, as every other operation\n  .map(user =\u003e ({\n    name: user.firstName.concat(' ', user.lastName),\n    age: user.age,\n  }))\n  .limit(3);\n\n// connect to your database\nconst connector = new PgConnector('postgresql://qustar:passwd@localhost:5432');\n\n// run the query\nconsole.log('users:', await query.fetch(connector));\n```\n\nOutput:\n\n```js\n{ age: 54, name: 'Linus Torvalds' }\n{ age: 29, name: 'Clark Kent' }\n{ age: 18, name: 'John Smith' }\n```\n\nThe query above will be translated to:\n\n```sql\nSELECT\n  \"s1\".\"age\",\n  concat(\"s1\".\"firstName\", ' ', \"s1\".\"lastName\") AS \"name\"\nFROM\n  users AS \"s1\"\nORDER BY\n  (\"s1\".\"createdAt\") DESC\nLIMIT\n  3\n```\n\nInsert/update/delete:\n\n```ts\n// insert\nawait users.insert({firstName: 'New', lastName: 'User'}).execute(connector);\n\n// update\nawait users\n  .filter(user =\u003e user.id.eq(42))\n  .update(user =\u003e ({age: user.age.add(1)}))\n  .execute(connector);\n\n// delete\nawait users.delete(user =\u003e user.id.eq(42)).execute(connector);\n```\n\n## Supported database drivers\n\nTo execute query against a database you need a _connector_. There are many ready to use connectors that wrap existing NodeJS drivers:\n\n- PostgreSQL\n  - [qustar-pg](https://www.npmjs.com/package/qustar-pg)\n- SQLite\n  - [qustar-better-sqlite3](https://www.npmjs.com/package/qustar-better-sqlite3) (recommended)\n  - [qustar-sqlite3](https://www.npmjs.com/package/qustar-sqlite3)\n- MySQL\n  - [qustar-mysql2](https://www.npmjs.com/package/qustar-mysql2)\n- MariaDB\n  - [qustar-mysql2](https://www.npmjs.com/package/qustar-mysql2)\n\nIf you implemented your own connector, let me know and I will add it to the list above!\n\n[//]: # 'todo: add a link to a guide for creating a custom connector'\n\n## Usage\n\nAny query starts from a table or a [raw sql](#raw-sql). We will talk more about raw queries later, for now the basic usage looks like this:\n\n```ts\nimport {Q} from 'qustar';\n\nconst users = Q.table({\n  name: 'users',\n  schema: {\n    id: Q.i32(),\n    age: Q.i32().null(),\n    // ...\n  },\n});\n```\n\nIn qustar you compose a query by calling query methods like `.filter` or `.map`:\n\n```ts\nconst young = users.filter(user =\u003e user.age.lt(18));\nconst youngIds = young.map(user =\u003e user.id);\n\n// or\n\nconst ids = users.filter(user =\u003e user.age.lt(18)).map(user =\u003e user.id);\n```\n\nQueries are immutable, so you can reuse them safely.\n\nFor methods like `.filter` or `.map` you pass a callback which returns an _expression_. Expression represents a condition or operation you wish to do. Expressions are build using methods like `.add` or `.eq`:\n\n```ts\n// for arrays you would write: users.filter(x =\u003e x.age + 1 === x.height - 5)\nconst a = users.filter(user =\u003e user.age.add(1).eq(user.height.sub(5)));\n\n// you can also use Q.eq to achieve the same\nimport {Q} from 'qustar';\n\nconst b = users.map(user =\u003e Q.eq(user.age.add(1), user.height.sub(5));\n```\n\nWe can't use native operators like `+` or `===` because JavaScript doesn't support operator overloading. You can find full list of supported expression operations [here](#expressions).\n\nNow lets talk about queries and expressions.\n\n### Query\n\n#### .filter(condition)\n\n```ts\nconst adults = users\n  // users with age \u003e= 18\n  .filter(user =\u003e /* any expression */ user.age.gte(18));\n```\n\n#### .map(mapper)\n\n```ts\nconst userIds = users.map(user =\u003e user.id);\n\nconst user = users\n  // you can map to an object\n  .map(user =\u003e ({id: user.id, name: user.name}));\n\nconst userInfo = users\n  // you can map to nested objects\n  .map(user =\u003e ({\n    id: user.id,\n    info: {\n      adult: user.age.gte(18),\n      nameLength: user.name.length(),\n    },\n  }));\n```\n\n#### .orderByDesc(selector), .orderByAsc(selector)\n\n```ts\nconst users = users\n  // order by age in ascending order\n  .orderByAsc(user =\u003e user.age)\n  // then order by name in descending order\n  .thenByDesc(user =\u003e user.name);\n```\n\n#### .drop(count), Query.limit(count)\n\n```ts\nconst users = users\n  .orderByAsc(user =\u003e user.id)\n  // skip first ten users\n  .drop(10)\n  // then take only five\n  .limit(5);\n```\n\n#### .slice(start, end)\n\nYou can also use `.slice` method to achieve the same:\n\n```ts\nconst users = users\n  // start = 10, end = 15\n  .slice(10, 15);\n```\n\n#### .{inner,left,right}Join(options)\n\nQustar supports `.innerJoin`, `.leftJoin`, `.rightJoin` and `.fullJoin`:\n\n```ts\nconst bobPosts = posts\n  .innerJoin({\n    right: users,\n    condition: (post, user) =\u003e post.authorId.eq(user.id),\n    select: (post, author) =\u003e ({\n      text: post.text,\n      author: author.name,\n    }),\n  })\n  .filter(({author}) =\u003e author.like('bob%'));\n```\n\n#### .unique()\n\nYou can select distinct rows using `.unique` method:\n\n```ts\nconst names = users.map(user =\u003e user.name).unique();\n```\n\n#### .groupBy(options)\n\n```ts\nconst stats = users.groupBy({\n  by: user =\u003e user.age,\n  select: user =\u003e ({\n    age: user.age,\n    count: Expr.count(1),\n    averageTax: user.salary.mul(user.taxRate).mean(),\n  }),\n});\n```\n\n#### .union(query)\n\n```ts\nconst studentNames = students.map(student =\u003e student.name);\nconst teacherNames = teachers.map(teacher =\u003e teacher.name);\n\nconst uniqueNames = studentNames.union(teacherNames);\n```\n\n#### .unionAll(query)\n\n```ts\nconst studentNames = students.map(student =\u003e student.name);\nconst teacherNames = teachers.map(teacher =\u003e teacher.name);\n\nconst peopleCount = studentNames.unionAll(teacherNames).count();\n```\n\n#### .concat(query)\n\n```ts\nconst studentNames = students.map(student =\u003e student.name);\nconst teacherNames = teachers.map(teacher =\u003e teacher.name);\n\n// concat preserves original ordering\nconst allNames = studentNames.concat(teacherNames);\n```\n\n#### .intersect(query)\n\n```ts\nconst studentNames = students.map(student =\u003e student.name);\nconst teacherNames = teachers.map(teacher =\u003e teacher.name);\n\nconst studentAndTeacherNames = studentNames.intersect(teacherNames);\n```\n\n#### .except(query)\n\n```ts\nconst studentNames = students.map(student =\u003e student.name);\nconst teacherNames = teachers.map(teacher =\u003e teacher.name);\n\nconst studentOnlyNames = studentNames.except(teacherNames);\n```\n\n#### .flatMap(mapper)\n\n```ts\nconst postsWithAuthor = users.flatMap(user =\u003e\n  posts\n    .filter(post =\u003e post.authorId.eq(user.id))\n    .map(post =\u003e ({text: post.text, author: user.name}))\n);\n```\n\n#### .includes(value)\n\n```ts\nconst userExists = users.map(user =\u003e user.id).includes(42);\n```\n\n#### Schema\n\nThe list of supported column types:\n\n- **boolean**: true or false\n- **i8**: 8 bit integer\n- **i16**: 16 bit integer\n- **i32**: 32 bit integer\n- **i64**: 64 bit integer\n- **f32**: 32 bit floating point number\n- **f64**: 64 bit floating point number\n- **string**: variable length string\n\n[//]: '#' 'todo: add ref/back_ref docs'\n\n#### Raw sql\n\nYou can use raw SQL like so:\n\n```ts\nimport {Q, sql} from 'qustar';\n\nconst users = Q.rawQuery({\n  sql: sql`SELECT * from users`,\n  // we must specify schema so qustar knows how to compose a query\n  schema: {\n    id: Q.i32(),\n    age: Q.i32().null(),\n  },\n})\n  .filter(user =\u003e user.age.lte(25))\n  .map(user =\u003e user.id);\n```\n\nYou can also use aliases in a nested query like so:\n\n```ts\nconst postIds = users.flatMap(user =\u003e\n  Q.rawQuery({\n    sql: sql`\n      SELECT\n        id\n      FROM\n        posts p\n      WHERE p.authorId = ${user.id}'\n    })`,\n    schema: {\n      id: Q.i32(),\n    },\n  });\n);\n```\n\nYou can use `Q.rawExpr` for raw SQL in a part of an operation:\n\n```ts\nconst halfIds = users.map(user =\u003e ({\n  halfId: Q.rawExpr({sql: sql`CAST(${user.id} as REAL) / 2`, schema: Q.f32()}),\n  name: user.name,\n}));\n```\n\nThe query above will be translated to:\n\n```sql\nSELECT\n  \"s1\".\"name\",\n  (CAST((\"s1\".\"id\") as REAL) / 2) AS \"halfId\"\nFROM\n  users AS \"s1\"\n```\n\n## License\n\nMIT License, see `LICENSE`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftilyupo%2Fqustar","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftilyupo%2Fqustar","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftilyupo%2Fqustar/lists"}