Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Each module includes its own README with detailed documentation. See individual
- [`@pgpm/encrypted-secrets-table`](https://www.npmjs.com/package/@pgpm/encrypted-secrets-table) - Table-based encrypted secrets

### Utilities
- [`@pgpm/errors`](https://www.npmjs.com/package/@pgpm/errors) - Canonical structured error raising (`errors.raise_error`)
- [`@pgpm/utils`](https://www.npmjs.com/package/@pgpm/utils) - General utility functions
- [`@pgpm/verify`](https://www.npmjs.com/package/@pgpm/verify) - Verification utilities (used by other modules)
- [`@pgpm/inflection`](https://www.npmjs.com/package/@pgpm/inflection) - String inflection utilities
Expand Down
8 changes: 4 additions & 4 deletions packages/achievements/__tests__/achievements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ afterEach(async () => {
});

it('newbie', async () => {
const steps = [
const steps = [
'agree_to_terms',
'accept_cookies',
'accept_privacy',
Expand Down Expand Up @@ -98,7 +98,7 @@ it('newbie', async () => {
});

it('advanced', async () => {
const steps = [
const steps = [
'agree_to_terms',
'accept_cookies',
'accept_privacy',
Expand Down Expand Up @@ -139,7 +139,7 @@ it('advanced', async () => {
});

it('advanced part II', async () => {
const partII = [['apply_for_verifier'], ['create_action', 2]];
const partII = [['apply_for_verifier'], ['create_action', 2]];

for (const [name, required_count = 1] of partII) {
await pg.any(
Expand Down Expand Up @@ -190,7 +190,7 @@ it('advanced part II', async () => {
});

it('advanced part III', async () => {
const partIII = [
const partIII = [
['apply_for_verifier'],
['approved_for_verifier'],
['create_action', 2]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ CREATE FUNCTION app_scope.routing_tables(
apis_table text,
api_schemas_schema text,
api_schemas_table text,
api_modules_schema text,
api_modules_table text,
api_settings_schema text,
api_settings_table text,
cors_settings_schema text,
Expand Down Expand Up @@ -77,7 +75,7 @@ BEGIN
RAISE EXCEPTION 'ROUTING_TABLES_SCOPE_REQUIRED: scope is required (no default scope)';
END IF;

SELECT am.apis_table_id, am.api_schemas_table_id, am.api_modules_table_id,
SELECT am.apis_table_id, am.api_schemas_table_id,
am.api_settings_table_id, am.cors_settings_table_id
INTO api_surface
FROM app_scope.frames(
Expand All @@ -93,20 +91,16 @@ BEGIN
IF FOUND AND api_surface.apis_table_id IS NOT NULL AND api_surface.apis_table_id <> uuid_nil() THEN
SELECT apis_s.schema_name, apis_t.name,
api_schemas_s.schema_name, api_schemas_t.name,
api_modules_s.schema_name, api_modules_t.name,
api_settings_s.schema_name, api_settings_t.name,
cors_settings_s.schema_name, cors_settings_t.name
INTO apis_schema, apis_table,
api_schemas_schema, api_schemas_table,
api_modules_schema, api_modules_table,
api_settings_schema, api_settings_table,
cors_settings_schema, cors_settings_table
FROM metaschema_public.table apis_t
JOIN metaschema_public.schema apis_s ON apis_s.id = apis_t.schema_id
JOIN metaschema_public.table api_schemas_t ON api_schemas_t.id = api_surface.api_schemas_table_id
JOIN metaschema_public.schema api_schemas_s ON api_schemas_s.id = api_schemas_t.schema_id
JOIN metaschema_public.table api_modules_t ON api_modules_t.id = api_surface.api_modules_table_id
JOIN metaschema_public.schema api_modules_s ON api_modules_s.id = api_modules_t.schema_id
JOIN metaschema_public.table api_settings_t ON api_settings_t.id = api_surface.api_settings_table_id
JOIN metaschema_public.schema api_settings_s ON api_settings_s.id = api_settings_t.schema_id
JOIN metaschema_public.table cors_settings_t ON cors_settings_t.id = api_surface.cors_settings_table_id
Expand Down
Binary file modified packages/app-scope/sql/pgpm-app-scope--0.34.0.bundle.tar.gz
Binary file not shown.
8 changes: 1 addition & 7 deletions packages/app-scope/sql/pgpm-app-scope--0.34.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,6 @@ CREATE FUNCTION app_scope.routing_tables(
apis_table text,
api_schemas_schema text,
api_schemas_table text,
api_modules_schema text,
api_modules_table text,
api_settings_schema text,
api_settings_table text,
cors_settings_schema text,
Expand Down Expand Up @@ -394,7 +392,7 @@ BEGIN
RAISE EXCEPTION 'ROUTING_TABLES_SCOPE_REQUIRED: scope is required (no default scope)';
END IF;

SELECT am.apis_table_id, am.api_schemas_table_id, am.api_modules_table_id,
SELECT am.apis_table_id, am.api_schemas_table_id,
am.api_settings_table_id, am.cors_settings_table_id
INTO api_surface
FROM app_scope.frames(
Expand All @@ -410,20 +408,16 @@ BEGIN
IF FOUND AND api_surface.apis_table_id IS NOT NULL AND api_surface.apis_table_id <> uuid_nil() THEN
SELECT apis_s.schema_name, apis_t.name,
api_schemas_s.schema_name, api_schemas_t.name,
api_modules_s.schema_name, api_modules_t.name,
api_settings_s.schema_name, api_settings_t.name,
cors_settings_s.schema_name, cors_settings_t.name
INTO apis_schema, apis_table,
api_schemas_schema, api_schemas_table,
api_modules_schema, api_modules_table,
api_settings_schema, api_settings_table,
cors_settings_schema, cors_settings_table
FROM metaschema_public.table apis_t
JOIN metaschema_public.schema apis_s ON apis_s.id = apis_t.schema_id
JOIN metaschema_public.table api_schemas_t ON api_schemas_t.id = api_surface.api_schemas_table_id
JOIN metaschema_public.schema api_schemas_s ON api_schemas_s.id = api_schemas_t.schema_id
JOIN metaschema_public.table api_modules_t ON api_modules_t.id = api_surface.api_modules_table_id
JOIN metaschema_public.schema api_modules_s ON api_modules_s.id = api_modules_t.schema_id
JOIN metaschema_public.table api_settings_t ON api_settings_t.id = api_surface.api_settings_table_id
JOIN metaschema_public.schema api_settings_s ON api_settings_s.id = api_settings_t.schema_id
JOIN metaschema_public.table cors_settings_t ON cors_settings_t.id = api_surface.cors_settings_table_id
Expand Down
14 changes: 7 additions & 7 deletions packages/base32/__tests__/base32.decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ afterAll(async () => {


it('base32_to_decimal', async () => {
const { base32_to_decimal } = await pg.one(
const { base32_to_decimal } = await pg.one(
`SELECT base32.base32_to_decimal($1::text) AS base32_to_decimal`,
['INQXI===']
);
expect(base32_to_decimal).toEqual(['8', '13', '16', '23', '8', '=', '=', '=']);
});

it('decimal_to_chunks', async () => {
const { decimal_to_chunks } = await pg.one(
const { decimal_to_chunks } = await pg.one(
`SELECT base32.decimal_to_chunks($1::text[]) AS decimal_to_chunks`,
[['8', '13', '16', '23', '8', '=', '=', '=']]
);
Expand All @@ -39,31 +39,31 @@ it('decimal_to_chunks', async () => {
});

it('decode', async () => {
const { decode } = await pg.one(
const { decode } = await pg.one(
`SELECT base32.decode($1::text) AS decode`,
['INQXI']
);
expect(decode).toEqual('Cat');
});

it('zero_fill', async () => {
const { zero_fill } = await pg.one(
const { zero_fill } = await pg.one(
`SELECT base32.zero_fill($1::int, $2::int) AS zero_fill`,
[300, 2]
);
expect(zero_fill).toBe('75');
});

it('zero_fill (-)', async () => {
const { zero_fill } = await pg.one(
const { zero_fill } = await pg.one(
`SELECT base32.zero_fill($1::int, $2::int) AS zero_fill`,
[-300, 2]
);
expect(zero_fill).toBe('1073741749');
});

it('zero_fill (0)', async () => {
const { zero_fill } = await pg.one(
const { zero_fill } = await pg.one(
`SELECT base32.zero_fill($1::int, $2::int) AS zero_fill`,
[-300, 0]
);
Expand All @@ -73,7 +73,7 @@ it('zero_fill (0)', async () => {
cases(
'base32.decode cases',
async (opts: { name: string; result: string }) => {
const { decode } = await pg.one(
const { decode } = await pg.one(
`SELECT base32.decode($1::text) AS decode`,
[opts.name]
);
Expand Down
2 changes: 1 addition & 1 deletion packages/base32/__tests__/base32.encode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ it('to_base32', async () => {
cases(
'base32.encode',
async (opts: { name: string; result: string }) => {
const { encode } = await pg.one(
const { encode } = await pg.one(
`SELECT base32.encode($1::text) AS encode`,
[opts.name]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ DECLARE
BEGIN
-- db_id defaults to the session's database claim; only callers that act on
-- behalf of a different database (e.g. platform-owned births) pass it
-- explicitly. Every job is owned by exactly one database a claim-less
-- session with no explicit db_id fails the jobs.database_id NOT NULL
-- constraint rather than producing an unattributable job.
-- explicitly. Every job is owned by exactly one database, so a claim-less
-- session with no explicit db_id is rejected by the default expression rather
-- than producing an unattributable job.
v_database_id := db_id;
v_actor_id := jwt_public.current_user_id();

Expand Down
Binary file not shown.
6 changes: 3 additions & 3 deletions packages/database-jobs/sql/pgpm-database-jobs--0.34.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -939,9 +939,9 @@ DECLARE
BEGIN
-- db_id defaults to the session's database claim; only callers that act on
-- behalf of a different database (e.g. platform-owned births) pass it
-- explicitly. Every job is owned by exactly one database a claim-less
-- session with no explicit db_id fails the jobs.database_id NOT NULL
-- constraint rather than producing an unattributable job.
-- explicitly. Every job is owned by exactly one database, so a claim-less
-- session with no explicit db_id is rejected by the default expression rather
-- than producing an unattributable job.
v_database_id := db_id;
v_actor_id := jwt_public.current_user_id();

Expand Down
6 changes: 6 additions & 0 deletions packages/errors/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
EXTENSION = errors
DATA = sql/errors--0.34.0.sql

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
29 changes: 29 additions & 0 deletions packages/errors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# @pgpm/errors

Canonical structured error raising for constructive-db.

Exposes a single hard-coded runtime helper, `errors.raise_error`, so every
application error is thrown in one consistent, machine-readable shape instead of
bare `RAISE EXCEPTION` strings.

```sql
PERFORM errors.raise_error(
'ACCOUNT_EXISTS',
jsonb_build_object('email', v_email),
'public'
);
```

This raises an exception whose:

- **MESSAGE** is the bare code (`ACCOUNT_EXISTS`) — message-scanning clients keep working;
- **DETAIL** is a JSON payload `{ "code", "context", "class" }` — the machine-readable
contract consumed by `@constructive-io/errors` on the server/client. Raw `context`
values survive to the client untouched (no server-side English interpolation), which
is what enables i18n on dynamic errors;
- **ERRCODE** is `P0001` (the semantic code rides in `DETAIL`, not SQLSTATE).

`class` is `'public'` (safe to show end users) or `'internal'` (developer/invariant
error, masked in production).

No dynamic SQL: this is a plain static function, not a generated one.
31 changes: 31 additions & 0 deletions packages/errors/deploy/schemas/errors/procedures/raise_error.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-- Deploy schemas/errors/procedures/raise_error to pg

-- requires: schemas/errors/schema

BEGIN;

-- Canonical error raiser. Every application error should funnel through this so
-- clients receive one consistent, machine-readable shape:
-- MESSAGE = code (bare code; message-scanning clients keep working)
-- DETAIL = { code, context, class } (the machine-readable contract)
-- ERRCODE = P0001 (semantic code rides in DETAIL, not SQLSTATE)
-- Raw context values travel untouched (no server-side interpolation), enabling
-- i18n on dynamic errors. class is 'public' (user-facing) or 'internal' (masked).
CREATE FUNCTION errors.raise_error(
code text,
context jsonb DEFAULT '{}'::jsonb,
error_class text DEFAULT 'internal'
) RETURNS void AS $$
BEGIN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = code,
DETAIL = jsonb_build_object(
'code', code,
'context', COALESCE(context, '{}'::jsonb),
'class', error_class
)::text;
END;
$$ LANGUAGE plpgsql;

COMMIT;
14 changes: 14 additions & 0 deletions packages/errors/deploy/schemas/errors/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Deploy schemas/errors/schema to pg

BEGIN;

CREATE SCHEMA errors;

GRANT USAGE ON SCHEMA errors
TO public;

ALTER DEFAULT PRIVILEGES IN SCHEMA errors
GRANT EXECUTE ON FUNCTIONS
TO public;

COMMIT;
7 changes: 7 additions & 0 deletions packages/errors/errors.control
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# errors extension
comment = 'Canonical structured error raising for constructive-db'
default_version = '0.34.0'
module_pathname = '$libdir/errors'
requires = 'plpgsql'
relocatable = false
superuser = false
33 changes: 33 additions & 0 deletions packages/errors/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@pgpm/errors",
"version": "0.34.0",
"description": "Canonical structured error raising for PostgreSQL (errors.raise_error)",
"author": "Dan Lynch <pyramation@gmail.com>",
"contributors": [
"Constructive <developers@constructive.io>"
],
"keywords": [
"postgresql",
"pgpm",
"errors"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"bundle": "pgpm package",
"test": "jest",
"test:watch": "jest --watch"
},
"devDependencies": {
"pgpm": "^5"
},
"repository": {
"type": "git",
"url": "https://github.com/constructive-io/pgpm-modules"
},
"homepage": "https://github.com/constructive-io/pgpm-modules",
"bugs": {
"url": "https://github.com/constructive-io/pgpm-modules/issues"
}
}
6 changes: 6 additions & 0 deletions packages/errors/pgpm.plan
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
%syntax-version=1.0.0
%project=errors
%uri=errors

schemas/errors/schema 2026-07-22T00:00:00Z devin <devin@cognition.ai> # add schemas/errors/schema
schemas/errors/procedures/raise_error [schemas/errors/schema] 2026-07-22T00:00:00Z devin <devin@cognition.ai> # add schemas/errors/procedures/raise_error
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Revert schemas/errors/procedures/raise_error from pg

BEGIN;

DROP FUNCTION errors.raise_error(text, jsonb, text);

COMMIT;
7 changes: 7 additions & 0 deletions packages/errors/revert/schemas/errors/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Revert schemas/errors/schema from pg

BEGIN;

DROP SCHEMA errors;

COMMIT;
Binary file added packages/errors/sql/errors--0.34.0.bundle.tar.gz
Binary file not shown.
24 changes: 24 additions & 0 deletions packages/errors/sql/errors--0.34.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
\echo Use "CREATE EXTENSION errors" to load this file. \quit
CREATE SCHEMA errors;

GRANT USAGE ON SCHEMA errors TO PUBLIC;

ALTER DEFAULT PRIVILEGES IN SCHEMA errors
GRANT EXECUTE ON FUNCTIONS TO PUBLIC;

CREATE FUNCTION errors.raise_error(
code text,
context jsonb DEFAULT '{}'::jsonb,
error_class text DEFAULT 'internal'
) RETURNS void AS $EOFCODE$
BEGIN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = code,
DETAIL = jsonb_build_object(
'code', code,
'context', COALESCE(context, '{}'::jsonb),
'class', error_class
)::text;
END;
$EOFCODE$ LANGUAGE plpgsql;
Loading
Loading