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
32
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'snode_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"]
}