Skip to main content

Codecs

A PgCodec ("codec") represents a type (data type) in the database. There are loads of built in codecs for dealing with the built-in types in Postgres made available via the TYPES export, but you can also create your own codecs for other types using the various helpers.

Every codec has a name, which is a handy identifier for you to use to reference it (you can reference codecs from the registry via registry.pgCodecs[name]). A codec also has an sqlType which is an SQL expression giving the name of the type in the database.

Codecs are responsible for performing coercion and validation; they should throw errors if the value supplied is invalid.

TYPES

import { TYPES } from "@dataplan/pg";

const intCodec = TYPES.int;

The TYPES object comprises a number of built-in codecs for common types in your database; the following keys on TYPES represent the similarly named database types (e.g. TYPES.boolean represents the bool type):

  • boolean
  • int2
  • int
  • bigint
  • float4
  • float
  • money
  • numeric
  • char
  • bpchar
  • varchar
  • text
  • name
  • json
  • jsonb
  • xml
  • citext
  • uuid
  • timestamp
  • timestamptz
  • date
  • time
  • timetz
  • inet
  • regproc
  • regprocedure
  • regoper
  • regoperator
  • regclass
  • regtype
  • regrole
  • regnamespace
  • regconfig
  • regdictionary
  • cidr
  • macaddr
  • macaddr8
  • interval
  • bit
  • varbit
  • point
  • line
  • lseg
  • box
  • path
  • polygon
  • circle
  • hstore
  • void

recordCodec(config)

recordCodec is a helper function that returns a PgCodec representing a "composite type" (or "record") - a structured type with attributes. This is most commonly used when defining a table (the "attributes" in this case being which columns it has), but is also useful in other cases.

The record codec config should contain:

  • name: string - the name to use for this codec
  • identifier: SQL - an SQL fragment detailing the name of the type in the database
  • attributes: Record<string, PgCodecAttribute> - the attributes (columns) on this codec; the keys on this object are the attribute names, and the values are objects with the following options:
    • codec: PgCodec - the PgCodec that details the type of this attribute
    • notNull: boolean (optional) - if true, indicates that the column cannot be null
    • hasDefault: boolean (optional) - if true, indicates that the column has a default (and thus may be omitted from INSERT operations)
    • expression(alias: SQL): SQL (optional) - indicates that this attribute is not a real attribute, but instead a computed value that can be computed using the given expression
  • polymorphism (extremely optional) - see polymorphism

Example

const forumCodec = recordCodec({
name: "forums",
identifier: sql`app_public.forums`,
attributes: {
id: {
codec: TYPES.uuid,
notNull: true,
hasDefault: true,
},
name: {
codec: TYPES.citext,
notNull: true,
},
archived_at: {
codec: TYPES.timestamptz,
},
is_archived: {
codec: TYPES.boolean,
expression(alias) {
return sql`${alias}.archived_at is not null`;
},
},
},
});

listOfCodec(innerCodec, config = {})

listOfCodec returns a new codec that represents a list (array) of the given innerCodec. Optionally you may provide details about this codec:

  • identifier - the (SQL) database name for this type

Example

For example, in this hypothetical E-commerce scenario, listOfCodec is used in combination with the $pgSelect.placeholder() method to return a SQL expression that allows the transformed list of $orderIds to be referenced inside the step for selecting the associated order items.

const $customerId = context().get("customerId");
const $orders = orders.find({ customer_id: $customerId });
const $orderIds = applyTransforms(each($orders, ($order) => $order.get("id")));
const $orderItems = registry.pgResources.order_items.find();
$orderItems.where(
sql`${$orderItems}.order_id = ANY (${$orderItems.placeholder(
$orderIds,
listOfCodec(TYPES.uuid),
)})`,
);

rangeOfCodec(innerCodec, name, identifier)

rangeOfCodec returns a new codec that represents a range of the given innerCodec. You must specify the name and identifier to use for this codec.

Custom scalar codecs

Should you need to define more scalar codecs than those available via TYPES, you may create a PgCodec object representing them. The object can have the following properties:

/**
* A codec for a Postgres scalar type, tells us how to convert to-and-from
* Postgres (including changes to the SQL statement itself). Also includes
* metadata about the type.
*/
export interface PgScalarCodec<
TName extends string = string,
TFromPostgres = any,
TFromJavaScript = TFromPostgres,
> {
/**
* Unique name to identify this codec.
*/
name: TName;

/**
* When we have an expression of this type, we can safely cast it within
* Postgres using the cast `(${expression})::${sqlType}` to make the type
* explicit.
*/
sqlType: SQL;

/**
* If this codec came from a specific database, specify the executor here. If
* the codec is used with multiple databases (or if unsure), set this null.
*/
executor: PgExecutor | null;

/**
* Given a value of type TFromJavaScript, returns an `SQL` value to insert into an SQL
* statement.
*
* **IMPORTANT**: nulls must already be handled!
*/
toPg: PgEncode<TFromJavaScript>;

/**
* Given a text value from PostgreSQL, returns the value cast to TCanonical.
*
* **IMPORTANT**: nulls must already be handled!
*/
fromPg: PgDecode<TFromJavaScript, TFromPostgres>;

/**
* We'll append `::text` by default to each selection; however if this type
* needs something special (e.g. `money` should be converted to `numeric`
* before being converted to `text`) then you can provide this custom
* callback to provide your own casting - this could even include function
* calls if you want.
*/
castFromPg?: (fragment: SQL, guaranteedNotNull?: boolean) => SQL;

/**
* If you provide `castFromPg` you probably ought to also specify
* `listCastFromPg` so that a list of this type can be converted properly.
*/
listCastFromPg?: (fragment: SQL, guaranteedNotNull?: boolean) => SQL;

/**
* True if this type is a binary type (e.g. bytea)
*/
isBinary?: boolean;

/**
* True if doing an equality check for this value would have intuitive
* results for a human. E.g. `3.0` and `3.0000` when encoded as `float` are
* the same as a human would expect, so `float` has natural equality. On the
* other hand Postgres sees the `json` `{"a":1}` as different to
* `{ "a": 1 }`), whereas a human would see these as the same JSON objects,
* so `json` does not have natural equality.
*
* Typically true primitives will set this true.
*/
hasNaturalEquality?: boolean;

/**
* True if this type has a natural ordering that would be intuitive for a human.
* For example numbers and text have natural ordering, whereas `{"a":1}` and
* `{ "a": 2 }` are not so obvious. Similarly, a `point` could be ordered in many
* ways relative to another point (x-first, then y; y-first, then x; distance
* from origin first, then angle; etc) so do not have natural order.
*
* Typically true primitives will set this true.
*/
hasNaturalOrdering?: boolean;

/**
* Documentation for the type.
*/
description?: string;

/**
* Arbitrary metadata
*/
extensions?: Partial<PgCodecExtensions>;
}