0.0.52 • Published 12 months ago

qustar v0.0.52

Weekly downloads
-
License
MIT
Repository
github
Last release
12 months ago

Qustar

npm version MIT license

Query SQL database through an array-like API.

Features

✅ Expressive AND high-level query builder
✅ TypeScript support
✅ SQL databases:
  ✅ PostgreSQL
  ✅ SQLite
  ✅ MySQL
  ✅ MariaDB
  ⬜ SQL Server
  ⬜ Oracle
✅ Navigation properties
✅ Codegen free
✅ Surprise free, all queries produce 100% SQL
✅ Raw SQL
⬜ Migrations
⬜ Transactions

Quick start

To start using qustar with PostgreSQL (the list of all supported data sources is available below) run the following command:

npm install qustar qustar-pg pg

Here an example usage of qustar:

import {PgConnector} from 'qustar-pg';
import {Q} from 'qustar';

// specify a schema
const users = Q.table({
  name: 'users',
  schema: {
    id: Q.i32(), // 32 bit integer
    firstName: Q.string(), // any text
    lastName: Q.string(),
    age: Q.i32().null(), // nullable integer
  },
});

// compose a query
const query = users
  .orderByDesc(user => user.createdAt)
  // map will be translated into 100% SQL, as every other operation
  .map(user => ({
    name: user.firstName.concat(' ', user.lastName),
    age: user.age,
  }))
  .limit(3);

// connect to your database
const connector = new PgConnector('postgresql://qustar:passwd@localhost:5432');

// run the query
console.log('users:', await query.fetch(connector));

Output:

{ age: 54, name: 'Linus Torvalds' }
{ age: 29, name: 'Clark Kent' }
{ age: 18, name: 'John Smith' }

The query above will be translated to:

SELECT
  "s1"."age",
  concat("s1"."firstName", ' ', "s1"."lastName") AS "name"
FROM
  users AS "s1"
ORDER BY
  ("s1"."createdAt") DESC
LIMIT
  3

Supported database drivers

To execute query against a database you need a connector. There are many ready to use connectors that wrap existing NodeJS drivers:

If you implemented your own connector, let me know and I will add it to the list above!

Usage

Any query starts from a table or a raw sql. We will talk more about raw queries later, for now the basic usage looks like this:

import {Q} from 'qustar';

const users = Q.table({
  name: 'users',
  schema: {
    id: Q.i32(),
    age: Q.i32().null(),
    // ...
  },
});

In qustar you compose a query by calling query methods like .filter or .map:

const young = users.filter(user => user.age.lt(18));
const youngIds = young.map(user => user.id);

// or

const ids = users.filter(user => user.age.lt(18)).map(user => user.id);

Queries are immutable, so you can reuse them safely.

For 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:

// for arrays you would write: users.filter(x => x.age + 1 === x.height - 5)
const a = users.filter(user => user.age.add(1).eq(user.height.sub(5)));

// you can also use Q.eq to achieve the same
import {Q} from 'qustar';

const b = users.map(user => Q.eq(user.age.add(1), user.height.sub(5));

We can't use native operators like + or === because JavaScript doesn't support operator overloading. You can find full list of supported expression operations here.

Now lets talk about queries and expressions.

Query

.filter(condition)

const adults = users
  // users with age >= 18
  .filter(user => /* any expression */ user.age.gte(18));

.map(mapper)

const userIds = users.map(user => user.id);

const user = users
  // you can map to an object
  .map(user => ({id: user.id, name: user.name}));

const userInfo = users
  // you can map to nested objects
  .map(user => ({
    id: user.id,
    info: {
      adult: user.age.gte(18),
      nameLength: user.name.length(),
    },
  }));

.orderByDesc(selector), .orderByDescAsc(selector)

const users = users
  // order by age in ascending order
  .orderByAsc(user => user.age)
  // then order by name in descending order
  .thenByDesc(user => user.name);

.drop(count), Query.limit(count)

const users = users
  .orderByAsc(user => user.id)
  // skip first ten users
  .drop(10)
  // then take only five
  .limit(5);

.slice(start, end)

You can also use .slice method to achieve the same:

const users = users
  // start = 10, end = 15
  .slice(10, 15);

.{inner,left,right}Join(options)

Qustar supports .innerJoin, .leftJoin, .rightJoin and .fullJoin:

const bobPosts = posts
  .innerJoin({
    right: users,
    // condition is optional
    condition: (post, user) => post.authorId.eq(user.id),
    select: (post, author) => ({
      text: post.text,
      author: author.name,
    }),
  })
  .filter(({author}) => author.like('bob%'));

.unique()

You can select distinct rows using .unique method:

const names = users.map(user => user.name).unique();

.groupBy(options)

const stats = users.groupBy({
  by: user => user.age,
  select: user => ({
    age: user.age,
    count: Expr.count(1),
    averageTax: user.salary.mul(user.taxRate).mean(),
  }),
});

.union(query)

const studentNames = students.map(student => student.name);
const teacherNames = teachers.map(teacher => teacher.name);

const uniqueNames = studentNames.union(teacherNames);

.unionAll(query)

const studentNames = students.map(student => student.name);
const teacherNames = teachers.map(teacher => teacher.name);

const peopleCount = studentNames.unionAll(teacherNames).count();

.concat(query)

const studentNames = students.map(student => student.name);
const teacherNames = teachers.map(teacher => teacher.name);

// concat preserves original ordering
const allNames = studentNames.concat(teacherNames);

.intersect(query)

const studentNames = students.map(student => student.name);
const teacherNames = teachers.map(teacher => teacher.name);

const studentAndTeacherNames = studentNames.intersect(teacherNames);

.except(query)

const studentNames = students.map(student => student.name);
const teacherNames = teachers.map(teacher => teacher.name);

const studentOnlyNames = studentNames.except(teacherNames);

.flatMap(mapper)

const postsWithAuthor = users.flatMap(user =>
  posts
    .filter(post => post.authorId.eq(user.id))
    .map(post => ({text: post.text, author: user.name}))
);

.includes(value)

const userExists = users.map(user => user.id).includes(42);

Schema

The list of supported column types:

  • boolean: true or false
  • null: NULL
  • i8: 8 bit integer
  • i16: 16 bit integer
  • i32: 32 bit integer
  • i64: 64 bit integer
  • f32: 32 bit floating point number
  • f64: 64 bit floating point number
  • string: variable length string

Raw sql

You can use raw SQL like so:

import {Q, sql} from 'qustar';

const users = Q.rawQuery({
  sql: sql`SELECT * from users`,
  // we must specify schema so qustar knows how to compose a query
  schema: {
    id: Q.i32(),
    age: Q.i32().null(),
  },
})
  .filter(user => user.age.lte(25))
  .map(user => user.id);

You can also use aliases in a nested query like so:

const postIds = users.flatMap(user =>
  Q.rawQuery({
    sql: sql`
      SELECT
        id
      FROM
        posts p
      WHERE p.authorId = ${user.id}'
    })`,
    schema: {
      id: Q.i32(),
    },
  });
);

You can use Q.rawExpr for raw SQL in a part of an operation:

const halfIds = users.map(user => ({
  halfId: Q.rawExpr({sql: sql`CAST(${user.id} as REAL) / 2`, schema: Q.f32()}),
  name: user.name,
}));

The query above will be translated to:

SELECT
  "s1"."name",
  (CAST(("s1"."id") as REAL) / 2) AS "halfId"
FROM
  users AS "s1"

License

MIT License, see LICENSE.

0.0.52

12 months ago

0.0.51

1 year ago

0.0.50

1 year ago

0.0.49

1 year ago

0.0.48

1 year ago

0.0.47

1 year ago

0.0.46

1 year ago

0.0.45

1 year ago

0.0.44

1 year ago

0.0.43

1 year ago

0.0.42

1 year ago

0.0.41

1 year ago

0.0.40

1 year ago

0.0.39

1 year ago

0.0.38

1 year ago

0.0.37

1 year ago

0.0.36

1 year ago

0.0.35

1 year ago

0.0.34

1 year ago

0.0.33

1 year ago

0.0.32

1 year ago

0.0.31

1 year ago

0.0.30

1 year ago

0.0.29

1 year ago

0.0.28

1 year ago

0.0.27

1 year ago

0.0.26

1 year ago

0.0.25

1 year ago

0.0.24

1 year ago

0.0.23

1 year ago

0.0.22

1 year ago

0.0.21

1 year ago

0.0.20

1 year ago

0.0.19

1 year ago

0.0.18

1 year ago

0.0.17

1 year ago

0.0.16

1 year ago

0.0.15

1 year ago

0.0.14

1 year ago

0.0.13

1 year ago

0.0.11

1 year ago

0.0.10

1 year ago

0.0.9

1 year ago

0.0.8

1 year ago

0.0.7

1 year ago

0.0.6

1 year ago

0.0.5

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago