0.0.52 • Published 11 months ago

qustar v0.0.52

Weekly downloads
-
License
MIT
Repository
github
Last release
11 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

11 months ago

0.0.51

11 months ago

0.0.50

11 months ago

0.0.49

11 months ago

0.0.48

11 months ago

0.0.47

11 months ago

0.0.46

12 months ago

0.0.45

12 months ago

0.0.44

12 months ago

0.0.43

12 months ago

0.0.42

12 months ago

0.0.41

12 months ago

0.0.40

12 months ago

0.0.39

12 months ago

0.0.38

12 months ago

0.0.37

12 months ago

0.0.36

12 months ago

0.0.35

12 months ago

0.0.34

12 months ago

0.0.33

12 months ago

0.0.32

12 months ago

0.0.31

12 months ago

0.0.30

12 months ago

0.0.29

12 months ago

0.0.28

12 months ago

0.0.27

12 months ago

0.0.26

12 months ago

0.0.25

12 months 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