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

pg-compose

v1.0.1

Published

<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

Downloads

20

Readme

Table of Contents generated with DocToc

pg-compose

A system for declarative Postgresql migrations, tests, and more.

TODO: Explanation of what this is and what it's for.

Installation

Local installation:

npm install pg-compose --save
yarn add pg-compose

Global installation:

npm install -g pg-compose
yarn global add pg-compose

Basic CLI Usage

Run your migrations:

pg-compose install -d "postgresql://localhost:5432/postgres" -f "./**/*.yaml"

To dry run (just generate the output to a SQL file), pass a -o option:

pg-compose install -d "postgresql://localhost:5432/postgres" -f "./**/*.yaml" -o up.sql

To run your tests:

pg-compose test -d "postgresql://localhost:5432/postgres" -f "./**/*.yaml"

And then to actually run the instance (run your migrations and start graphile worker and scheduler):

SECRET=secret pg-compose run -d "postgresql://localhost:5432/postgres" -f "./**/*.yaml"

Adding Tasks

pg-compose uses graphile-worker to run tasks and graphile-scheduler to schedule its cron jobs.

Since these are written in Javascript / Typescript, you need to pass your task list to pg-compose and re-recreate the CLI:

// your-project/cli.ts
import { makeCli, TaskList, JobHelpers } from "pg-compose";

interface SendWebhookPayload {
  destination: string;
  body: any;
}

const taskList: TaskList = {
  "send-webhook": async (payload: SendWebhookPayload, helpers: JobHelpers) => {
    await request.post(payload.destination).send(payload.body);
  },
};

makeCli(taskList);

Now, you can use your-project/cli.(j|t)s just like you would the pg-compose cli (node cli.js (install|test|run) -d ...).

After and Context Hooks

To enable additional code re-use of Javascript jobs, we added after hooks on top of graphile-worker - this allows you to pass the result of a job in Javascript land to a Postgres function that can take some additional action with the information.

The call signature of the after function is:

select my_after_function(payload json, result json, context json)

Where payload is the initial payload that the job was queued with, result is what was returned by the Javascript task, and context is a JSON blob that you initially passed to the job.

For example, if I wanted to send an email and update the email as sent with the ID of the email as defined by some external system, I could do:

create or replace function
	mark_email_sent(payload json, result json, context json)
	returns void as $$
		update emails_to_send
		set sender_id = result->>'id'
		where id = (context->>'id')::uuid
$$ language sql volatile;

-- and then somewhere else in code...
select graphile_worker.add_job('send-email',
	json_build_object(
		'to': email_to_send.to,
		'from': email_to_send.from,
		'__after': 'mark_email_sent',
		'__context': json_build_object('id', email_to_send.id)
	)
);

Secret Management

We found that we were reproducing a lot of the same encryption / decryption logic to avoid storing customers' API keys or other sensitive information unencrypted in the database, so we created a minimal and secure secret management system that does not require external system dependencies.

When you run pg-compose, you pass it a SECRET - that secret is used for symmetric encryption / decryption of sensitive information.

To set a secret, call:

=# select graphile_secrets.set_secret('my-secret', 'asmdvasdfqy');
 set_secret
------------
 my-secret
(1 row)

As long as your worker is running, if you then try to see the secret you just inserted, you'll see:

=# select * from graphile_secrets.secrets ;
    ref    | encrypted_secret
-----------+------------------
 my-secret | aamsdva0s8fy0asdfa0smvasdmfaseq
(1 row)

=# select * from graphile_secrets.unencrypted_secrets ;
    ref    | encrypted_secret
-----------+------------------
(0 rows)

Even though you passed your unencrypted value through your database, it never touched disk or WAL logs or any of the sensitive places that we don't want unencrypted values to go - it only temporarily touched graphile_secrets.unencrypted_secrets, which is an unlogged table.

To use a secret in a task, pass its reference inside a {"__secret": "my-secret"} json singleton to graphile-worker

select graphile_worker.add_job('send-email',
	json_build_object(
    'to', email_to_send.to,
    'from', email_to_send.from,
    'mailgun_api_key', json_build_object('__secret', 'my-mailgun-api-key')
  )
);

And then your task will be called with its decrypted value:

interface SendEmailPayload {
  from: string;
  to: string;
  mailgun_api_key: string;
}

const sendEmail = async (payload: SendEmailPayload) => {
  const mailgun = mailgun({ apiKey: payload.mailgun_api_key })
  mailgun.messages.send(...)
}

Packaging Your Module

What are packages?

pg-compose module's are package and distributed as ES Modules via NPM. If you add a dependency inside of your module via YAML, like:

kind: Dependency
name: stripe

Because you want extend your customers table to integrate with Stripe:

kind: Table
name: customer
implements:
	- trait: stripe_customer

You need to install the module pgc-stripe to your project, similar to how Babel uses the babel-plugin prefix for all of its plugins.

Preparing a Package

pg-compose modules must export a default asynchronous function which returns a pg-compose module, i.e.:

type ModuleLoader = () => Promise<ModuleI>;

The easiest way to do that is to use the loadYaml function provided by pg-compose, for example:

// your-project/index.js
import { taskList } from "./task-list";
import { loadYaml, ModuleI } from "pg-compose";

// pg-compose modules should export an asynchronous function which resolves to a module
export default async () => {
  const m = await loadYaml({ include: `${__dirname}/**.yaml` });
  m.taskList = taskList;
  return m;
};

Make sure to:

  • Use __dirname in the path to your yaml so that in correctly resolves inside of someone else's node_modules folder, and
  • Copy your YAML files if you have a build setup so that it resolves correctly

Preparing a Typescript Package

If you are building a pg-compose package in Typescript, you need to make sure that you copy any included YAML files to your dist folder. I'd recommend using the npm package copy, and adding a build command like:

{
  "name": "pgc-your-project",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "build": "rm -Rf dist && tsc && copy src/**/*.yaml dist",
  },
  "dependencies": {
    "copy": "^0.3.2",
    ...
  },
  ...
}

to your package.json. This assumes that you are transpiling your Typescript to a dist/folder. For reference, this is the tsconfig.json we use with our pg-compose modules with tasks written in Typescript:

{
  "compilerOptions": {
    "rootDir": "src",
    "declarationDir": "./dist",
    "outDir": "./dist",
    "declaration": true,
    "allowJs": false,
    "target": "es2018",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "pretty": true,
    "importHelpers": true,
    "experimentalDecorators": true,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,
    "strictNullChecks": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "preserveWatchOutput": true,
    "lib": ["es2018", "esnext.asynciterable"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}