Skip to content

Commit cfc41f3

Browse files
authored
Merge pull request #101 from constructive-io/feat/app-scope-function-resolution
feat(scope): portable app-scope + function-resolution modules
2 parents 7913f2b + 90592b4 commit cfc41f3

74 files changed

Lines changed: 7082 additions & 7574 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ jobs:
2929
- metaschema-schema
3030
- metaschema-modules
3131
- services
32+
- app-scope
33+
- function-resolution
3234
- jobs
3335
- database-jobs
3436
- types

MODULES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939
- [ ] `packages/metaschema-modules` - Module metadata handling
4040
- [ ] `packages/services` - Services schemas for APIs, sites, and domains
4141

42+
## Scope & Resolution
43+
44+
### Scope-Chain Resolution
45+
- [ ] `packages/app-scope` - Portable scope-chain resolution primitive (ordered scope frames + platform database lookup)
46+
- [ ] `packages/function-resolution` - Cross-scope function-definition resolution and resolver-aware enqueue
47+
4248
## Security & Authentication
4349

4450
### Core Security

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ Each module includes its own README with detailed documentation. See individual
126126
- [`@pgpm/metaschema-modules`](https://www.npmjs.com/package/@pgpm/metaschema-modules) - Module metadata handling
127127
- [`@pgpm/services`](https://www.npmjs.com/package/@pgpm/services) - Services schemas for APIs, sites, and domains
128128

129+
### Scope & Resolution
130+
- [`@pgpm/app-scope`](https://www.npmjs.com/package/@pgpm/app-scope) - Portable scope-chain resolution primitive (ordered scope frames + platform database lookup)
131+
- [`@pgpm/function-resolution`](https://www.npmjs.com/package/@pgpm/function-resolution) - Cross-scope function-definition resolution and resolver-aware enqueue (built on app-scope)
132+
129133
### Security & Authentication
130134
- [`@pgpm/defaults`](https://www.npmjs.com/package/@pgpm/defaults) - Security defaults and configurations
131135
- [`@pgpm/jwt-claims`](https://www.npmjs.com/package/@pgpm/jwt-claims) - JWT claim handling and validation

packages/app-scope/.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__tests__
2+
jest.config.js

packages/app-scope/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
EXTENSION = pgpm-app-scope
2+
DATA = sql/pgpm-app-scope--0.15.5.sql
3+
4+
PG_CONFIG = pg_config
5+
PGXS := $(shell $(PG_CONFIG) --pgxs)
6+
include $(PGXS)

packages/app-scope/README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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

Comments
 (0)