|
| 1 | +# @pgpm/app-scope |
| 2 | + |
| 3 | +<p align="center" width="100%"> |
| 4 | + <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" /> |
| 5 | +</p> |
| 6 | + |
| 7 | +<p align="center" width="100%"> |
| 8 | + <a href="https://github.com/constructive-io/pgpm-modules/actions/workflows/ci.yml"> |
| 9 | + <img height="20" src="https://github.com/constructive-io/pgpm-modules/actions/workflows/ci.yml/badge.svg" /> |
| 10 | + </a> |
| 11 | + <a href="https://github.com/constructive-io/pgpm-modules/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a> |
| 12 | + <a href="https://www.npmjs.com/package/@pgpm/app-scope"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/pgpm-modules?filename=packages%2Fapp-scope%2Fpackage.json"/></a> |
| 13 | +</p> |
| 14 | + |
| 15 | +Portable scope-chain resolution primitive for PostgreSQL |
| 16 | + |
| 17 | +## Overview |
| 18 | + |
| 19 | +`@pgpm/app-scope` turns "where should I look this up?" into a single, ordered list of **scope frames**. Given an execution database, an execution scope, and (optionally) an entity, it produces the frames — most-specific first — that a nearest-wins lookup should walk. It is a general primitive: function resolution, limits, permissions, or any other scope-aware lookup can consume the same frames. |
| 20 | + |
| 21 | +Every frame is `(scope, lookup_database_id, key_value)`: |
| 22 | + |
| 23 | +- `scope` — the scope name (`team`, `department`, `org`, `app`, `database`, `platform`, …) |
| 24 | +- `lookup_database_id` — the physical database whose per-scope table should be probed for that frame |
| 25 | +- `key_value` — the scope key (entity id, or `database_id` for the synthetic `database` frame, or `NULL` for the global `app`/`platform` frames) |
| 26 | + |
| 27 | +The module reads only the metaschema catalog tables (`@pgpm/metaschema-schema`, `@pgpm/metaschema-modules`) and builds its dynamic lookups with `format('%I')` + `EXECUTE ... USING` — **no AST/deparser runtime**, so it installs into any provisioned database whose catalog is populated. |
| 28 | + |
| 29 | +## Features |
| 30 | + |
| 31 | +- **Full per-database chain** — each database climbs its own `entity → … → org → app`, then falls through to the platform database's `database → org → app → platform`. The platform database is not a special case; it climbs the identical shape with `platform` as the terminal global frame. |
| 32 | +- **Custom entities below org** — an entity scope (e.g. `team`) starts below the organization and climbs its membership tree (`team → department → org`). |
| 33 | +- **Synthetic `database` frame** — `database` is not a real membership scope; it is hardcoded, keyed by `database_id`, and bridged to its owning org through `metaschema_public.database.owner_id`. |
| 34 | +- **Hard contract** — a `NULL` execution scope raises; there is no implicit default. |
| 35 | +- **Portable** — SELECT-only dynamic SQL, no AST/deparser, no core-metaschema runtime functions. |
| 36 | + |
| 37 | +## Installation |
| 38 | + |
| 39 | +If you have `pgpm` installed: |
| 40 | + |
| 41 | +```bash |
| 42 | +pgpm install @pgpm/app-scope |
| 43 | +pgpm deploy |
| 44 | +``` |
| 45 | + |
| 46 | +### Prerequisites |
| 47 | + |
| 48 | +```bash |
| 49 | +# Install pgpm CLI |
| 50 | +npm install -g pgpm |
| 51 | + |
| 52 | +# Start local Postgres (via Docker) and export env vars |
| 53 | +pgpm docker start |
| 54 | +eval "$(pgpm env)" |
| 55 | +``` |
| 56 | + |
| 57 | +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. |
| 58 | +
|
| 59 | +## Usage |
| 60 | + |
| 61 | +### Ordered scope frames |
| 62 | + |
| 63 | +```sql |
| 64 | +-- team execution in a tenant database |
| 65 | +SELECT scope, lookup_database_id, key_value |
| 66 | +FROM app_scope.frames(:tenant_db, 'team', :team_id); |
| 67 | +``` |
| 68 | + |
| 69 | +| Order | Database | Scope | Key | |
| 70 | +| ----: | -------- | ---------- | ------------------- | |
| 71 | +| 0 | tenant | team | team_id | |
| 72 | +| 1 | tenant | department | department_id | |
| 73 | +| 2 | tenant | org | org_id | |
| 74 | +| 3 | tenant | app | NULL | |
| 75 | +| 4 | platform | database | platform_database_id| |
| 76 | +| 5 | platform | org | platform_org_id | |
| 77 | +| 6 | platform | app | NULL | |
| 78 | +| 7 | platform | platform | NULL | |
| 79 | + |
| 80 | +A consumer walks the frames top-to-bottom and takes the first hit. |
| 81 | + |
| 82 | +### Platform database lookup |
| 83 | + |
| 84 | +```sql |
| 85 | +SELECT app_scope.platform_database_id(); |
| 86 | +``` |
| 87 | + |
| 88 | +This is the single sanctioned way to resolve the platform (control-plane) database id; it raises if no platform database is registered. |
| 89 | + |
| 90 | +## API |
| 91 | + |
| 92 | +| Function | Purpose | |
| 93 | +|---|---| |
| 94 | +| `app_scope.frames(database_id, execution_scope, entity_id)` | Ordered scope frames for a lookup (most-specific first) | |
| 95 | +| `app_scope.local_frames(database_id, execution_scope, entity_id)` | One database's local chain (`entity → … → org → app`), no platform terminal | |
| 96 | +| `app_scope.platform_database_id()` | The platform (control-plane) database id | |
| 97 | +| `app_scope.membership_parent(database_id, scope)` | Membership type, parent scope, entity table + owner column for a scope | |
| 98 | +| `app_scope.dyn_lookup_uuid(schema, table, column, id)` | Dynamic owner-FK SELECT used by the membership climb | |
| 99 | + |
| 100 | +## Testing |
| 101 | + |
| 102 | +```bash |
| 103 | +pnpm test |
| 104 | +``` |
| 105 | + |
| 106 | +## Dependencies |
| 107 | + |
| 108 | +- [`@pgpm/metaschema-schema`](https://www.npmjs.com/package/@pgpm/metaschema-schema) |
| 109 | +- [`@pgpm/metaschema-modules`](https://www.npmjs.com/package/@pgpm/metaschema-modules) |
| 110 | +- [`@pgpm/verify`](https://www.npmjs.com/package/@pgpm/verify) (verify scripts) |
| 111 | + |
| 112 | +## Related Tooling |
| 113 | + |
| 114 | +* [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/cli): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. |
| 115 | +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. |
| 116 | +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. |
| 117 | +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. |
| 118 | +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. |
| 119 | +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. |
| 120 | +* [pg-proto-parser](https://github.com/constructive-io/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums. |
| 121 | + |
| 122 | +## Disclaimer |
| 123 | + |
| 124 | +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. |
| 125 | + |
| 126 | +No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value. |
0 commit comments