Skip to content

feat(errors): @constructive-io/errors package + server code normalization & class-based masking - #1415

Merged
pyramation merged 5 commits into
mainfrom
feat/constructive-errors
Jul 28, 2026
Merged

feat(errors): @constructive-io/errors package + server code normalization & class-based masking#1415
pyramation merged 5 commits into
mainfrom
feat/constructive-errors

Conversation

@pyramation

@pyramation pyramation commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces @constructive-io/errors — a zero-dependency, canonical Constructive error system — and makes the GraphQL server, the GraphQL client, and pgpm consume it instead of their own duplicated error catalogs. One package is now the single source of truth for error codes, their public/internal class, message copy (i18n), and cross-source parsing (PostgreSQL, GraphQL, message, SQLSTATE).

packages/errors (new)

  • parse(anyError){ code, context, class, known, sqlState, ... } with precedence:
    1. structured PostgreSQL DETAIL JSON ({code, context, class} from the DB errors.raise_error helper),
    2. GraphQL extensions.code,
    3. leading ALL_CAPS message token (+ positional args),
    4. native SQLSTATE mapping (23505UNIQUE_VIOLATION, etc.).
  • parse() now trusts an explicit producer class (DETAIL.class / extensions.class) over the registry fallback, so a newly-raised code is classified correctly even before a registry refresh. Invalid class values are ignored (fall back to the registry). Unknown codes remain fail-closed → internal.
  • format(code, context, locale) does interpolation + localization; ConstructiveError carries code / message / class / http / context; errors.* factories are derived from the registry.
  • Registry = curated entries + a generated layer of all 289 audited constructive-db codes (82 public / 207 internal), reproducible via scripts/audit-db-errors.py + scripts/generate-registry.py.

Server (graphql/server)

  • maskError normalizes any error into the canonical shape and lifts the real code into extensions.code / class / context (fixes the original bug where DB errors reached clients with empty extensions).
  • Masking is now by registry class, not a hand-maintained allowlist. The ~90-entry SAFE_ERROR_CODES set is deleted; only the 4 GraphQL framework protocol codes remain as an explicit set:
-const SAFE_ERROR_CODES = new Set([...~90 auth/resource/sqlstate codes...]);
-const isPublicCode = (code) => classify(code) === 'public' || SAFE_ERROR_CODES.has(code);
+const GRAPHQL_PROTOCOL_CODES = new Set([
+  'GRAPHQL_VALIDATION_FAILED', 'GRAPHQL_PARSE_FAILED',
+  'PERSISTED_QUERY_NOT_FOUND', 'PERSISTED_QUERY_NOT_SUPPORTED',
+]);
+const isPublicCode = (code) => classify(code) === 'public' || GRAPHQL_PROTOCOL_CODES.has(code);

The 17 genuinely-public auth/resource codes that were only on the old allowlist (e.g. USER_NOT_AUTHENTICATED, ACCOUNT_LOCKED, SSO_*_DISABLED, INVALID_SIGNATURE, UPLOAD_MIMETYPE) were migrated into the curated registry as public, and SQLSTATE constraint errors already resolve to public codes via parse() — so this shrink causes zero masking regression (verified: all 82 former semantic allowlist codes classify public).

Client (graphql/query + graphql/codegen)

  • The legacy DataError system is replaced (no back-compat): DataError, DataErrorType, PG_ERROR_CODES, isDataError, and the message string-matching / constraint regexes are gone. graphql/query/src/client/error.ts is now a thin adapter over @constructive-io/errors:
export function parseGraphQLError(error, locale?) { return toError(error, locale); } // → ConstructiveError
export const createError = { network, timeout, unauthorized, forbidden, notFound, badRequest, graphql, unknown };
export function isRetryable(error) { /* NETWORK_ERROR | TIMEOUT_ERROR | RATE_LIMITED | TOO_MANY_REQUESTS */ }
  • execute.ts keeps its behavior: abort → TIMEOUT_ERROR, fetch failure → NETWORK_ERROR, 401/403/404 → canonical factories, GraphQL/body errors → canonical parser.
  • Added transport/client codes to the registry: NETWORK_ERROR, TIMEOUT_ERROR, BAD_USER_INPUT, VALIDATION_FAILED, UNKNOWN_ERROR.
  • codegen re-exports the canonical client API from @constructive-io/graphql-query.

pgpm (pgpm/types)

  • error-factory re-exports errors / makeError from the package; call sites and messages unchanged.

Notes

  • No backwards-compatibility exports or legacy documentation — the error shape is managed by @constructive-io/errors going forward.
  • Companion constructive-db work (already merged): errors.raise_error(code, context, class) runtime helper (#2510), build-time AST helper (#2512), and funneling generated raises through it (#2537) — these emit the structured DETAIL this parser consumes.
  • Tests: packages/errors (28), graphql/query (18), graphql/server (120), graphql/codegen (358) all pass under Node 22.

Link to Devin session: https://app.devin.ai/sessions/f4d3ce9225894883ba9cfc9ed2f0ba7e
Requested by: @pyramation

Add a standalone, zero-dependency @constructive-io/errors package (canonical registry, parse/format/classify, i18n, type-safe factory), make pgpm consume it, and normalize DB codes into GraphQL extensions with class-based masking.
@pyramation pyramation self-assigned this Jul 22, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Collect every constructive-db EXCEPTION/THROW code (287) into a generated
registry layer with public/internal class, HTTP hint, and message (public copy
seeded from dashboard catalogs). Curated typed entries override generated ones.
Reproducible via scripts/generate-registry.py from a committed audit snapshot.
Re-audit constructive-db (253 commits newer): +18 codes, -16 removed.
Commit the reproducible audit extractor (audit-db-errors.py) and update
the committed inventory snapshot + generated registry accordingly.
- Replace the legacy client DataError system (graphql/query client) with a
  thin adapter over @constructive-io/errors: parseGraphQLError/createError now
  produce a canonical ConstructiveError; drop the duplicated DataErrorType,
  PG_ERROR_CODES and message string-matching. Add toError() to the package.
- parse() now trusts an explicit producer class (DB DETAIL.class /
  server extensions.class) over the registry fallback, so newly-raised codes
  are classified correctly before a registry refresh.
- Add transport/client codes (NETWORK_ERROR, TIMEOUT_ERROR, BAD_USER_INPUT,
  VALIDATION_FAILED, UNKNOWN_ERROR) and 17 public auth/resource codes migrated
  from the server allowlist to the curated registry.
- Server graphile masking: replace the hand-maintained SAFE_ERROR_CODES
  allowlist with registry classify(), keeping only GraphQL framework protocol
  codes as an explicit set.
- Wire @constructive-io/errors into graphql-query; update codegen re-exports,
  tests and README.
@pyramation
pyramation merged commit b5b8117 into main Jul 28, 2026
16 checks passed
@pyramation
pyramation deleted the feat/constructive-errors branch July 28, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant