npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

pj

v0.3.5

Published

Create SQL strings for PostgreSQL by interfacing with a javascript API

Downloads

32

Readme

node-pj

WARNING: This package is currently under development! It is essentially vaporware right now. Do not download this code expecting anything to work. This README documents the future capabilities of this package.

Install

$ npm install pj

Features

  • Parameterized queries for values, columns, tables, operators, and functions
  • Function aliasing within select clauses
  • Implicit joins via where directives

Examples

Introduction

This API is designed to allow you to programatically construct SQL queries for PostgreSQL fast and efficiently.

var pj = require('pj');

// connect to database (uses node-postgres on node.js environments)
var db = new pj('blake@/project_db');

// define a relationship that joins two tables
db.relate('retailer.owned_by=owner.id');

// some values to exclude from our query
var exclude_states = ['Hawaii','Alaska'];

db.from('retailers')
    .select('.name','location::geojson', 'email=owner.email_address')
    .where({
        country: 'USA',
		state: pj('!=', exclude_states),
		owner: {
			name: pj('not like', '%o\'connor'),
			phone: /^[0-9]{10}$/,
		}
	})
	.order('name');

will generate SQL equivalent to:

select
    "retailers"."name" as "name",
    ST_AsGeoJSON("location") as "location",
    "owner"."email_address" as "email"
from "retailers"
join "owner" on "retailer"."owned_by"="owner"."id"
where (
    "country" = 'USA'
      and ("state" != 'Hawaii' and "state" != 'Alaska')
      and "owner"."name" not like '%o''connor'
      and "owner"."phone" ~ '^[0-9]{10}$'
) order by "name" asc

Connecting to a database

You can create a new instance of pj to get a handle to your database:

var retailers = new pj('postgres://user@host:5432/database');

// or...

var retailers = new pj({
	user: 'user',
	host: 'host',
	port: 5432,
	database: 'database',
});

You can also use the global pj as a handle to a single database by passing connection parameters to the global option in pj.config:

pj.config({
	global: {
		user: 'blake',
		host: 'localhost', // this is also by default
		port: 5432, // this is also by default
		database: 'my_db',
	},
});

pj.from('my_table')
	...

On the other hand, you may omit all arguments to the pj constructor if you only wish to generate query strings:

var gen = pj();

Select Clause

.select(field_1 [, field_2, ..., field_n)

Where typeof field_ must be either string or object. The string version supports a strict syntax for specifying column names, creating an alias, aggregating rows, calling functions, using SQL operators, and type casting. The following section documents this syntax.

Fields

There are several different ways to access a field, each one has a specific purpose.

Named Identifiers

Using standard PostgreSQL identifier characters in the select-syntax string will yield a named identifier.

	.from('post').select('id')

yields:

	select "id" from "post"

This can be useful for selecting aliases created elsewhere in the statement, but it is prone to ambiguation when multiple tables are joined together. To reference a column when there is more than one table present, you should use absolute references.

Absolute References

Prefixing the column name with a dot will produce an absolute reference to that column by using the name of the primary table specified in the from clause:

	.from('post').select('.id')

yiels:

	select "post"."id" from "post"

It is typically a best practice to use absolute references when joining tables in order to prevent conflicts of columns with the same name from different tables:

	.from('post').select('post.id', 'user.id', 'user.name')

yields (assuming the post <-> user table relationship is defined):

	select "post"."id", "user"."id", "user"."name" from "post" join "user" on "user"."id" = "post"."user_id"

Aliasing

Any field (including the outputs of functions) can be aliased by using the = operator:

	.from('post').select('post_id=post.id')

yields:

	select "post"."id" as "post_id" from "post"

Aliasing the table specified in the from clause will also be reflected in the dotal-prefix syntax:

	.from('p=post').select('.id')

yields:

	select "p"."id" from "post" as "p"

Type Casting

Any field (including the outputs of functions) can be cast into alternate data types by using the :: operator.

Native Data Types

PostgreSQL Data Types that appear after the :: operator will simply cast the preceding field's value to its type:

	.select('post.score::int')

yields:

	select "post"."score"::int

Some native data types also accept arguments:

	.select('post.score::decimal(6,4)')

yields:

	select "post"."score"::decimal(6,4)

Function Aliasing

You can define your own custom 'functions' by aliasing PostgreSQL functions using pj.alias:

	pj.alias(string aliasDef, string outputSub)
	pj.alias(string aliasDef, function outputGen)

You may also define these aliases on a single pj instance. Doing this will prevent other instances created by the same pj object from holding conflicting aliases.

	.alias(string aliasDef, string outputSub)
	.alias(string aliasDef, function outputGen)

These aliases are invoked (and perform their subsequent substitution) by using the :: operator, in the same way that type-casting is done:

	pj.alias('excite', "concat($0, '!')");
	db.from('fruit').select('color::excite');

yields:

	select concat("color", '!') from "fruit"
Parameterized String Substitution

Passing a string into the output parameter will essentially substitue the field and any input arguments into the formatted string:


	// defines a custom function alias, globally
	pj.alias('epoch_ms', 'extract(epoch from $0) * 1000');

	db.select('time::epoch_ms').from('post')

yields:

	select extract(epoch from "time") * 1000 as "time" from "post"
The $0 Parameter

Parameters/variables are prefixed with a $ symbol. $0 references the field preceding the :: operator, although the above function can also be invoked like this:

	.select('epoch_ms(time)').from('post')

yields:

	select extract(epoch from "time") * 1000 as "time" from "post"
Ordinally-named Parameters: Fields

When creating a custom function alias, if the parameter name is an integer then the subsequent replacement will yield the matching field name:

	pj.alias('describeItem($1)', "concat($1, ' is ', $0)");
	db.from('fruit').select('describeItem(type, color)')

yields:

	select concat("type", ' is ', "color") as "describeItem" from "fruit"

The only exception is if the argument is created using the slash-notation, which turns the argument into a single-quoted value.

Textually-named Parameters: Values

Using textually-named parameters will substitute escaped values into the string:

	// specifies an optional parameter where 'UTC' is set as the default value
	pj.alias('epoch_ms($tz=UTC)', "extract(epoch from $0 at time zone $tz)");

	db.select('time::epoch_ms').from('post'); // parenthesis are optional
	db.select('time::epoch_ms()').from('post'); // will also use the default value
	db.select('time::epoch_ms(PST)').from('post'); // note: argument is not enclosed with quotes

yields (respectively):

	select extract(epoch from "time" at time zone 'UTC') as "time" from "post";
	select extract(epoch from "time" at time zone 'UTC') as "time" from "post";
	select extract(epoch from "time" at time zone 'PST') as "time" from "post";
Prefix-named Parameters: SQL Injection

If you wish to inject non-values (such as SQL keywords) into the query, you may prefix the variable name with _:

	pj.alias('addTime($_type,$value)', '$0 + $_type $value');
	db.from('post').select('.time::addTime(interval, 3 hours)');

yields:

	select "post"."time" + interval '3 hours'

Notice how the arguments to the call are not enclosed by any quotes. Arguments are terminated by the ) character; for subtitution strings they are split by , delimiter. Custom function aliases that call pj.alias with a javascript function instead of a string however receive the entire text within the ( ) characters (ie: the text is not split). This allows the handling function to parse the arguments however it wants (albeit devoid of any right parenthesis characters).

TODO: document pj.alias('eg', function(){})

Passing Arguments

In some cases, you may need to opt for this style of passing arguments:

	.select([string alias,] array fields [, string casters])

This style allows you apply different functions to multiple fields, and then apply a function that accepts those expressions as inputs:

	db.select([
		'trajectory::startPoint',
		'trajectory::endPoint'
	], '::distance');

yields:

	select ST_Distance(
		ST_StartPoint("trajectory"),
		ST_EndPoint("trajectory")
	) as "distance";

startPoint, endPoint and distance are PostGIS function aliases that come shipped with pj.

PostGIS Function Aliases

By default, pj defines several functions that alias PostGIS Geometry Functions for constructing, accessing, editing, and outputting geometry data:

For example:

	.from('buildings').select('boundary::rotateX(90deg)::geojson')

yields:

	select ST_AsGeoJSON(ST_RotateX("boundary", pi()*0.5)) as "boundary" from "building"

Here is a table that shows which functions are mapped to by their equivalent alias:

Geometry Constructors

| PostGIS Function | pj Alias Equivalent | | ---------------- | -------------------- | | ST_AsGeoJSON | geojson | | ST_AsKML | kml | | ST_AsText | wkt | | ST_AsEWKT | ewkt | | ST_AsEWKB | ewkb | | ST_AsBinary | binary | | ST_AsGeoHash | geohash | | ST_AsSVG | svg | | ST_AsLatLonText | latlon | | | | | ST_GeomFromText | wkt_geom | | ST_GeomFromWKB | wkb_geom | | ST_GeomFromEWKB | ewkb_geom | | ST_GeomFromEWKT | ewkt_geom | | ST_GeomFromGeoHash | geohash_geom | | ST_GeomFromGML | gml_geom | | ST_GeomFromGeoJSON | geojson_geom | | ST_GeomFromKML | kml_geom | | | | | ST_GeogFromText | wkt_geog | | ST_GeogFromWKB | wkb_geog | | ST_GeogFromEWKB | ewkb_geog | | ST_GeogFromEWKT | ewkt_geog | | | |

Geometry Accessors

| PostGIS Function | pj Alias Equivalent | | ---------------- | -------------------- | | ST_X | x | | ST_Y | y | | ST_Z | z | | ST_M | m | | | | | ST_NumPoints | numPoints | | ST_NPoints | nPoints | | ST_PointN | pointN | | ST_StartPoint | startPoint | | ST_EndPoint | endPoint | | | | | ST_NumInteriorRings | numInteriorRings | | ST_NRings | nRings | | ST_ExteriorRing | exteriorRing | | ST_InteriorRingN | interiorRingN | | | | | ST_NumGeometries | numGeometries | | ST_GeometryN | geometryN |

Geometry Editors

| PostGIS Function | pj Alias Equivalent | | ---------------- | -------------------- | | ST_Scale | scale | | ST_Translate | translate | | ST_Rotate | rotate | | ST_RotateX | rotateX | | ST_RotateY | rotateY | | ST_RotateZ | rotateZ | | ST_Affine | affine | | ST_TransScale | transScale | | | | | ST_Transform | transform | | ST_SetSRID | setSrid |

Selecting Literals (Slash-Notation)

To generate values (single-quoted literals) within the select clause, you can use this special slash-notation:

	.from().select('/This is a string/')

yields:

	select 'This is a string'

Any single-quotes within the / delimiters are escaped:

	.from().select("/This is a 'string'/")

yields:

	select 'This is a ''string'''

The first and last occurences of the / delimiter set the bounds for the value, so you do not need to worry about escaping extra / characters:

	var text = "Don't worry / I'm safe!";
	db.from().select('output=/'+text+'/')

yields:

	select 'Don''t worry / I''m safe!' as "output"

Since this notation only allows you to insert one single-quoted value per javascript string, you can exploit the array fields parameter of the select function to pass multiple single-quoted values to a function:

	db.alias('join($1)', "concat($0, ' ', $1)");
	db.select('message=', [
		'/hello,/',
		'/world!/'
	], '::join')

yields:

	select concat('hello,', ' ', 'world!') as "message";

The slash-notation is convenient for constructing geometry with PostGIS:

	.select([
		'/point(34.72 -118.25)/::wkt_geom',
		'/point(36.12 -119.4)/::wkt_geom'
	], '::distance')

yields:

	select ST_Distance(
		ST_GeomFromText('point(34.72 -118.25)'),
		ST_GeomFromText('point(36.12 -119.4)')
	) as "distance"

Implicit Joins

You can take advantage of fixed relationships by declaring how two tables are related to one another.

	.relate(string relationship)
	.relate(string tableA_dot_columnB, string tableX_dot_columnY)

For example:

	db.relate('post.user_id=user.id');

	db.from('post').where({
		user: {
			name: 'jedi',
		}
	})

yields:

	select * from "post"
		join "user" on "post"."user_id" = "user"."id"
		where "user"."name" = 'jedi';